Allow reading of certificate chains from strings.
[libdcp.git] / src / certificate_chain.cc
1 /*
2     Copyright (C) 2013-2016 Carl Hetherington <cth@carlh.net>
3
4     This file is part of libdcp.
5
6     libdcp is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     libdcp is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with libdcp.  If not, see <http://www.gnu.org/licenses/>.
18
19     In addition, as a special exception, the copyright holders give
20     permission to link the code of portions of this program with the
21     OpenSSL library under certain conditions as described in each
22     individual source file, and distribute linked combinations
23     including the two.
24
25     You must obey the GNU General Public License in all respects
26     for all of the code used other than OpenSSL.  If you modify
27     file(s) with this exception, you may extend this exception to your
28     version of the file(s), but you are not obligated to do so.  If you
29     do not wish to do so, delete this exception statement from your
30     version.  If you delete this exception statement from all source
31     files in the program, then also delete it here.
32 */
33
34 /** @file  src/signer_chain.cc
35  *  @brief Functions to make signer chains.
36  */
37
38 #include "certificate_chain.h"
39 #include "exceptions.h"
40 #include "util.h"
41 #include "dcp_assert.h"
42 #include "compose.hpp"
43 #include <asdcp/KM_util.h>
44 #include <libcxml/cxml.h>
45 #include <libxml++/libxml++.h>
46 #include <xmlsec/xmldsig.h>
47 #include <xmlsec/dl.h>
48 #include <xmlsec/app.h>
49 #include <xmlsec/crypto.h>
50 #include <openssl/sha.h>
51 #include <openssl/bio.h>
52 #include <openssl/evp.h>
53 #include <openssl/pem.h>
54 #include <boost/filesystem.hpp>
55 #include <boost/algorithm/string.hpp>
56 #include <boost/foreach.hpp>
57 #include <fstream>
58 #include <iostream>
59
60 using std::string;
61 using std::ofstream;
62 using std::ifstream;
63 using std::runtime_error;
64 using namespace dcp;
65
66 /** Run a shell command.
67  *  @param cmd Command to run (UTF8-encoded).
68  */
69 static void
70 command (string cmd)
71 {
72 #ifdef LIBDCP_WINDOWS
73         /* We need to use CreateProcessW on Windows so that the UTF-8/16 mess
74            is handled correctly.
75         */
76         int const wn = MultiByteToWideChar (CP_UTF8, 0, cmd.c_str(), -1, 0, 0);
77         wchar_t* buffer = new wchar_t[wn];
78         if (MultiByteToWideChar (CP_UTF8, 0, cmd.c_str(), -1, buffer, wn) == 0) {
79                 delete[] buffer;
80                 return;
81         }
82
83         int code = 1;
84
85         STARTUPINFOW startup_info;
86         memset (&startup_info, 0, sizeof (startup_info));
87         startup_info.cb = sizeof (startup_info);
88         PROCESS_INFORMATION process_info;
89
90         /* XXX: this doesn't actually seem to work; failing commands end up with
91            a return code of 0
92         */
93         if (CreateProcessW (0, buffer, 0, 0, FALSE, CREATE_NO_WINDOW, 0, 0, &startup_info, &process_info)) {
94                 WaitForSingleObject (process_info.hProcess, INFINITE);
95                 DWORD c;
96                 if (GetExitCodeProcess (process_info.hProcess, &c)) {
97                         code = c;
98                 }
99                 CloseHandle (process_info.hProcess);
100                 CloseHandle (process_info.hThread);
101         }
102
103         delete[] buffer;
104 #else
105         cmd += " 2> /dev/null";
106         int const r = system (cmd.c_str ());
107         int const code = WEXITSTATUS (r);
108 #endif
109         if (code) {
110                 throw dcp::MiscError (String::compose ("error %1 in %2 within %3", code, cmd, boost::filesystem::current_path().string()));
111         }
112 }
113
114 /** Extract a public key from a private key and create a SHA1 digest of it.
115  *  @param private_key Private key
116  *  @param openssl openssl binary name (or full path if openssl is not on the system path).
117  *  @return SHA1 digest of corresponding public key, with escaped / characters.
118  */
119 static string
120 public_key_digest (boost::filesystem::path private_key, boost::filesystem::path openssl)
121 {
122         boost::filesystem::path public_name = private_key.string() + ".public";
123
124         /* Create the public key from the private key */
125         command (String::compose("\"%1\" rsa -outform PEM -pubout -in %2 -out %3", openssl.string(), private_key.string(), public_name.string ()));
126
127         /* Read in the public key from the file */
128
129         string pub;
130         ifstream f (public_name.string().c_str ());
131         if (!f.good ()) {
132                 throw dcp::MiscError ("public key not found");
133         }
134
135         bool read = false;
136         while (f.good ()) {
137                 string line;
138                 getline (f, line);
139                 if (line.length() >= 10 && line.substr(0, 10) == "-----BEGIN") {
140                         read = true;
141                 } else if (line.length() >= 8 && line.substr(0, 8) == "-----END") {
142                         break;
143                 } else if (read) {
144                         pub += line;
145                 }
146         }
147
148         /* Decode the base64 of the public key */
149
150         unsigned char buffer[512];
151         int const N = dcp::base64_decode (pub, buffer, 1024);
152
153         /* Hash it with SHA1 (without the first 24 bytes, for reasons that are not entirely clear) */
154
155         SHA_CTX context;
156         if (!SHA1_Init (&context)) {
157                 throw dcp::MiscError ("could not init SHA1 context");
158         }
159
160         if (!SHA1_Update (&context, buffer + 24, N - 24)) {
161                 throw dcp::MiscError ("could not update SHA1 digest");
162         }
163
164         unsigned char digest[SHA_DIGEST_LENGTH];
165         if (!SHA1_Final (digest, &context)) {
166                 throw dcp::MiscError ("could not finish SHA1 digest");
167         }
168
169         char digest_base64[64];
170         string dig = Kumu::base64encode (digest, SHA_DIGEST_LENGTH, digest_base64, 64);
171 #ifdef LIBDCP_WINDOWS
172         boost::replace_all (dig, "/", "\\/");
173 #else
174         boost::replace_all (dig, "/", "\\\\/");
175 #endif
176         return dig;
177 }
178
179 CertificateChain::CertificateChain (
180         boost::filesystem::path openssl,
181         string organisation,
182         string organisational_unit,
183         string root_common_name,
184         string intermediate_common_name,
185         string leaf_common_name
186         )
187 {
188         boost::filesystem::path directory = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path ();
189         boost::filesystem::create_directories (directory);
190
191         boost::filesystem::path const cwd = boost::filesystem::current_path ();
192         boost::filesystem::current_path (directory);
193
194         string quoted_openssl = "\"" + openssl.string() + "\"";
195
196         command (quoted_openssl + " genrsa -out ca.key 2048");
197
198         {
199                 ofstream f ("ca.cnf");
200                 f << "[ req ]\n"
201                   << "distinguished_name = req_distinguished_name\n"
202                   << "x509_extensions   = v3_ca\n"
203                   << "[ v3_ca ]\n"
204                   << "basicConstraints = critical,CA:true,pathlen:3\n"
205                   << "keyUsage = keyCertSign,cRLSign\n"
206                   << "subjectKeyIdentifier = hash\n"
207                   << "authorityKeyIdentifier = keyid:always,issuer:always\n"
208                   << "[ req_distinguished_name ]\n"
209                   << "O = Unique organization name\n"
210                   << "OU = Organization unit\n"
211                   << "CN = Entity and dnQualifier\n";
212         }
213
214         string const ca_subject = "/O=" + organisation +
215                 "/OU=" + organisational_unit +
216                 "/CN=" + root_common_name +
217                 "/dnQualifier=" + public_key_digest ("ca.key", openssl);
218
219         {
220                 command (
221                         String::compose (
222                                 "%1 req -new -x509 -sha256 -config ca.cnf -days 3650 -set_serial 5"
223                                 " -subj \"%2\" -key ca.key -outform PEM -out ca.self-signed.pem",
224                                 quoted_openssl, ca_subject
225                                 )
226                         );
227         }
228
229         command (quoted_openssl + " genrsa -out intermediate.key 2048");
230
231         {
232                 ofstream f ("intermediate.cnf");
233                 f << "[ default ]\n"
234                   << "distinguished_name = req_distinguished_name\n"
235                   << "x509_extensions = v3_ca\n"
236                   << "[ v3_ca ]\n"
237                   << "basicConstraints = critical,CA:true,pathlen:2\n"
238                   << "keyUsage = keyCertSign,cRLSign\n"
239                   << "subjectKeyIdentifier = hash\n"
240                   << "authorityKeyIdentifier = keyid:always,issuer:always\n"
241                   << "[ req_distinguished_name ]\n"
242                   << "O = Unique organization name\n"
243                   << "OU = Organization unit\n"
244                   << "CN = Entity and dnQualifier\n";
245         }
246
247         string const inter_subject = "/O=" + organisation +
248                 "/OU=" + organisational_unit +
249                 "/CN=" + intermediate_common_name +
250                 "/dnQualifier=" + public_key_digest ("intermediate.key", openssl);
251
252         {
253                 command (
254                         String::compose (
255                                 "%1 req -new -config intermediate.cnf -days 3649 -subj \"%2\" -key intermediate.key -out intermediate.csr",
256                                 quoted_openssl, inter_subject
257                                 )
258                         );
259         }
260
261         command (
262                 quoted_openssl +
263                 " x509 -req -sha256 -days 3649 -CA ca.self-signed.pem -CAkey ca.key -set_serial 6"
264                 " -in intermediate.csr -extfile intermediate.cnf -extensions v3_ca -out intermediate.signed.pem"
265                 );
266
267         command (quoted_openssl + " genrsa -out leaf.key 2048");
268
269         {
270                 ofstream f ("leaf.cnf");
271                 f << "[ default ]\n"
272                   << "distinguished_name = req_distinguished_name\n"
273                   << "x509_extensions   = v3_ca\n"
274                   << "[ v3_ca ]\n"
275                   << "basicConstraints = critical,CA:false\n"
276                   << "keyUsage = digitalSignature,keyEncipherment\n"
277                   << "subjectKeyIdentifier = hash\n"
278                   << "authorityKeyIdentifier = keyid,issuer:always\n"
279                   << "[ req_distinguished_name ]\n"
280                   << "O = Unique organization name\n"
281                   << "OU = Organization unit\n"
282                   << "CN = Entity and dnQualifier\n";
283         }
284
285         string const leaf_subject = "/O=" + organisation +
286                 "/OU=" + organisational_unit +
287                 "/CN=" + leaf_common_name +
288                 "/dnQualifier=" + public_key_digest ("leaf.key", openssl);
289
290         {
291                 command (
292                         String::compose (
293                                 "%1 req -new -config leaf.cnf -days 3648 -subj \"%2\" -key leaf.key -outform PEM -out leaf.csr",
294                                 quoted_openssl, leaf_subject
295                                 )
296                         );
297         }
298
299         command (
300                 quoted_openssl +
301                 " x509 -req -sha256 -days 3648 -CA intermediate.signed.pem -CAkey intermediate.key"
302                 " -set_serial 7 -in leaf.csr -extfile leaf.cnf -extensions v3_ca -out leaf.signed.pem"
303                 );
304
305         boost::filesystem::current_path (cwd);
306
307         _certificates.push_back (dcp::Certificate (dcp::file_to_string (directory / "ca.self-signed.pem")));
308         _certificates.push_back (dcp::Certificate (dcp::file_to_string (directory / "intermediate.signed.pem")));
309         _certificates.push_back (dcp::Certificate (dcp::file_to_string (directory / "leaf.signed.pem")));
310
311         _key = dcp::file_to_string (directory / "leaf.key");
312
313         boost::filesystem::remove_all (directory);
314 }
315
316 CertificateChain::CertificateChain (string s)
317 {
318         while (true) {
319                 try {
320                         Certificate c;
321                         s = c.read_string (s);
322                         _certificates.push_back (c);
323                 } catch (MiscError& e) {
324                         /* Failed to read a certificate, just stop */
325                         break;
326                 }
327         }
328
329         if (!attempt_reorder ()) {
330                 throw MiscError ("could not find certificate chain order");
331         }
332 }
333
334 /** @return Root certificate */
335 Certificate
336 CertificateChain::root () const
337 {
338         DCP_ASSERT (!_certificates.empty());
339         return _certificates.front ();
340 }
341
342 /** @return Leaf certificate */
343 Certificate
344 CertificateChain::leaf () const
345 {
346         DCP_ASSERT (_certificates.size() >= 2);
347         return _certificates.back ();
348 }
349
350 /** @return Certificates in order from root to leaf */
351 CertificateChain::List
352 CertificateChain::root_to_leaf () const
353 {
354         return _certificates;
355 }
356
357 /** @return Certificates in order from leaf to root */
358 CertificateChain::List
359 CertificateChain::leaf_to_root () const
360 {
361         List c = _certificates;
362         c.reverse ();
363         return c;
364 }
365
366 /** Add a certificate to the end of the chain.
367  *  @param c Certificate to add.
368  */
369 void
370 CertificateChain::add (Certificate c)
371 {
372         _certificates.push_back (c);
373 }
374
375 /** Remove a certificate from the chain.
376  *  @param c Certificate to remove.
377  */
378 void
379 CertificateChain::remove (Certificate c)
380 {
381         _certificates.remove (c);
382 }
383
384 /** Remove the i'th certificate in the list, as listed
385  *  from root to leaf.
386  */
387 void
388 CertificateChain::remove (int i)
389 {
390         List::iterator j = _certificates.begin ();
391         while (j != _certificates.end () && i > 0) {
392                 --i;
393                 ++j;
394         }
395
396         if (j != _certificates.end ()) {
397                 _certificates.erase (j);
398         }
399 }
400
401 /** Check to see if the chain is valid (i.e. root signs the intermediate, intermediate
402  *  signs the leaf and so on) and that the private key (if there is one) matches the
403  *  leaf certificate.
404  *  @return true if it's ok, false if not.
405  */
406 bool
407 CertificateChain::valid () const
408 {
409         /* Check the certificate chain */
410
411         X509_STORE* store = X509_STORE_new ();
412         if (!store) {
413                 return false;
414         }
415
416         for (List::const_iterator i = _certificates.begin(); i != _certificates.end(); ++i) {
417
418                 List::const_iterator j = i;
419                 ++j;
420                 if (j ==  _certificates.end ()) {
421                         break;
422                 }
423
424                 if (!X509_STORE_add_cert (store, i->x509 ())) {
425                         X509_STORE_free (store);
426                         return false;
427                 }
428
429                 X509_STORE_CTX* ctx = X509_STORE_CTX_new ();
430                 if (!ctx) {
431                         X509_STORE_free (store);
432                         return false;
433                 }
434
435                 X509_STORE_set_flags (store, 0);
436                 if (!X509_STORE_CTX_init (ctx, store, j->x509 (), 0)) {
437                         X509_STORE_CTX_free (ctx);
438                         X509_STORE_free (store);
439                         return false;
440                 }
441
442                 int v = X509_verify_cert (ctx);
443                 X509_STORE_CTX_free (ctx);
444
445                 if (v == 0) {
446                         X509_STORE_free (store);
447                         return false;
448                 }
449         }
450
451         X509_STORE_free (store);
452
453         /* Check that the leaf certificate matches the private key, if there is one */
454
455         if (!_key) {
456                 return true;
457         }
458
459         BIO* bio = BIO_new_mem_buf (const_cast<char *> (_key->c_str ()), -1);
460         if (!bio) {
461                 throw MiscError ("could not create memory BIO");
462         }
463
464         RSA* private_key = PEM_read_bio_RSAPrivateKey (bio, 0, 0, 0);
465         RSA* public_key = leaf().public_key ();
466         bool const valid = !BN_cmp (private_key->n, public_key->n);
467         BIO_free (bio);
468
469         return valid;
470 }
471
472 /** @return true if the chain is now in order from root to leaf,
473  *  false if no correct order was found.
474  */
475 bool
476 CertificateChain::attempt_reorder ()
477 {
478         List original = _certificates;
479         _certificates.sort ();
480         do {
481                 if (valid ()) {
482                         return true;
483                 }
484         } while (std::next_permutation (_certificates.begin(), _certificates.end ()));
485
486         _certificates = original;
487         return false;
488 }
489
490 /** Add a &lt;Signer&gt; and &lt;ds:Signature&gt; nodes to an XML node.
491  *  @param parent XML node to add to.
492  *  @param standard INTEROP or SMPTE.
493  */
494 void
495 CertificateChain::sign (xmlpp::Element* parent, Standard standard) const
496 {
497         /* <Signer> */
498
499         xmlpp::Element* signer = parent->add_child("Signer");
500         xmlpp::Element* data = signer->add_child("X509Data", "dsig");
501         xmlpp::Element* serial_element = data->add_child("X509IssuerSerial", "dsig");
502         serial_element->add_child("X509IssuerName", "dsig")->add_child_text (leaf().issuer());
503         serial_element->add_child("X509SerialNumber", "dsig")->add_child_text (leaf().serial());
504         data->add_child("X509SubjectName", "dsig")->add_child_text (leaf().subject());
505
506         /* <Signature> */
507
508         xmlpp::Element* signature = parent->add_child("Signature", "dsig");
509
510         xmlpp::Element* signed_info = signature->add_child ("SignedInfo", "dsig");
511         signed_info->add_child("CanonicalizationMethod", "dsig")->set_attribute ("Algorithm", "http://www.w3.org/TR/2001/REC-xml-c14n-20010315");
512
513         if (standard == INTEROP) {
514                 signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#rsa-sha1");
515         } else {
516                 signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
517         }
518
519         xmlpp::Element* reference = signed_info->add_child("Reference", "dsig");
520         reference->set_attribute ("URI", "");
521
522         xmlpp::Element* transforms = reference->add_child("Transforms", "dsig");
523         transforms->add_child("Transform", "dsig")->set_attribute (
524                 "Algorithm", "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
525                 );
526
527         reference->add_child("DigestMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#sha1");
528         /* This will be filled in by the signing later */
529         reference->add_child("DigestValue", "dsig");
530
531         signature->add_child("SignatureValue", "dsig");
532         signature->add_child("KeyInfo", "dsig");
533         add_signature_value (signature, "dsig");
534 }
535
536
537 /** Sign an XML node.
538  *
539  *  @param parent Node to sign.
540  *  @param ns Namespace to use for the signature XML nodes.
541  */
542 void
543 CertificateChain::add_signature_value (xmlpp::Node* parent, string ns) const
544 {
545         cxml::Node cp (parent);
546         xmlpp::Node* key_info = cp.node_child("KeyInfo")->node ();
547
548         /* Add the certificate chain to the KeyInfo child node of parent */
549         BOOST_FOREACH (Certificate const & i, leaf_to_root ()) {
550                 xmlpp::Element* data = key_info->add_child("X509Data", ns);
551
552                 {
553                         xmlpp::Element* serial = data->add_child("X509IssuerSerial", ns);
554                         serial->add_child("X509IssuerName", ns)->add_child_text (i.issuer ());
555                         serial->add_child("X509SerialNumber", ns)->add_child_text (i.serial ());
556                 }
557
558                 data->add_child("X509Certificate", ns)->add_child_text (i.certificate());
559         }
560
561         xmlSecDSigCtxPtr signature_context = xmlSecDSigCtxCreate (0);
562         if (signature_context == 0) {
563                 throw MiscError ("could not create signature context");
564         }
565
566         signature_context->signKey = xmlSecCryptoAppKeyLoadMemory (
567                 reinterpret_cast<const unsigned char *> (_key->c_str()), _key->size(), xmlSecKeyDataFormatPem, 0, 0, 0
568                 );
569
570         if (signature_context->signKey == 0) {
571                 throw runtime_error ("could not read private key");
572         }
573
574         /* XXX: set key name to the PEM string: this can't be right! */
575         if (xmlSecKeySetName (signature_context->signKey, reinterpret_cast<const xmlChar *> (_key->c_str())) < 0) {
576                 throw MiscError ("could not set key name");
577         }
578
579         int const r = xmlSecDSigCtxSign (signature_context, parent->cobj ());
580         if (r < 0) {
581                 throw MiscError (String::compose ("could not sign (%1)", r));
582         }
583
584         xmlSecDSigCtxDestroy (signature_context);
585 }
586
587 string
588 CertificateChain::chain () const
589 {
590         string o;
591         BOOST_FOREACH (Certificate const &i, root_to_leaf ()) {
592                 o += i.certificate(true);
593         }
594
595         return o;
596 }