Add wrappers around boost::filesystem methods that handle the
[libdcp.git] / src / certificate_chain.cc
1 /*
2     Copyright (C) 2013-2021 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
35 /** @file  src/certificate_chain.cc
36  *  @brief CertificateChain class
37  */
38
39
40 #include "certificate_chain.h"
41 #include "compose.hpp"
42 #include "dcp_assert.h"
43 #include "exceptions.h"
44 #include "filesystem.h"
45 #include "util.h"
46 #include "warnings.h"
47 #include <asdcp/KM_util.h>
48 #include <libcxml/cxml.h>
49 LIBDCP_DISABLE_WARNINGS
50 #include <libxml++/libxml++.h>
51 LIBDCP_ENABLE_WARNINGS
52 #include <xmlsec/xmldsig.h>
53 #include <xmlsec/dl.h>
54 #include <xmlsec/app.h>
55 #include <xmlsec/crypto.h>
56 #include <openssl/sha.h>
57 #include <openssl/bio.h>
58 #include <openssl/evp.h>
59 #include <openssl/pem.h>
60 #include <openssl/rsa.h>
61 #include <boost/algorithm/string.hpp>
62 #include <fstream>
63 #include <iostream>
64
65
66 using std::string;
67 using std::ofstream;
68 using std::ifstream;
69 using std::runtime_error;
70 using namespace dcp;
71
72
73 /** Run a shell command.
74  *  @param cmd Command to run (UTF8-encoded).
75  */
76 static void
77 command (string cmd)
78 {
79 #ifdef LIBDCP_WINDOWS
80         /* We need to use CreateProcessW on Windows so that the UTF-8/16 mess
81            is handled correctly.
82         */
83         int const wn = MultiByteToWideChar (CP_UTF8, 0, cmd.c_str(), -1, 0, 0);
84         auto buffer = new wchar_t[wn];
85         if (MultiByteToWideChar (CP_UTF8, 0, cmd.c_str(), -1, buffer, wn) == 0) {
86                 delete[] buffer;
87                 return;
88         }
89
90         int code = 1;
91
92         STARTUPINFOW startup_info;
93         memset (&startup_info, 0, sizeof (startup_info));
94         startup_info.cb = sizeof (startup_info);
95         PROCESS_INFORMATION process_info;
96
97         /* XXX: this doesn't actually seem to work; failing commands end up with
98            a return code of 0
99         */
100         if (CreateProcessW (0, buffer, 0, 0, FALSE, CREATE_NO_WINDOW, 0, 0, &startup_info, &process_info)) {
101                 WaitForSingleObject (process_info.hProcess, INFINITE);
102                 DWORD c;
103                 if (GetExitCodeProcess (process_info.hProcess, &c)) {
104                         code = c;
105                 }
106                 CloseHandle (process_info.hProcess);
107                 CloseHandle (process_info.hThread);
108         }
109
110         delete[] buffer;
111 #else
112         cmd += " 2> /dev/null";
113         int const r = system (cmd.c_str ());
114         int const code = WEXITSTATUS (r);
115 #endif
116         if (code) {
117                 throw dcp::MiscError(String::compose("error %1 in %2 within %3", code, cmd, filesystem::current_path().string()));
118         }
119 }
120
121
122 /** Extract a public key from a private key and create a SHA1 digest of it.
123  *  @param private_key Private key
124  *  @param openssl openssl binary name (or full path if openssl is not on the system path).
125  *  @return SHA1 digest of corresponding public key, with escaped / characters.
126  */
127 static string
128 public_key_digest (boost::filesystem::path private_key, boost::filesystem::path openssl)
129 {
130         boost::filesystem::path public_name = private_key.string() + ".public";
131
132         /* Create the public key from the private key */
133         command (String::compose("\"%1\" rsa -outform PEM -pubout -in %2 -out %3", openssl.string(), private_key.string(), public_name.string()));
134
135         /* Read in the public key from the file */
136
137         string pub;
138         ifstream f (public_name.string().c_str());
139         if (!f.good ()) {
140                 throw dcp::MiscError ("public key not found");
141         }
142
143         bool read = false;
144         while (f.good ()) {
145                 string line;
146                 getline (f, line);
147                 if (line.length() >= 10 && line.substr(0, 10) == "-----BEGIN") {
148                         read = true;
149                 } else if (line.length() >= 8 && line.substr(0, 8) == "-----END") {
150                         break;
151                 } else if (read) {
152                         pub += line;
153                 }
154         }
155
156         /* Decode the base64 of the public key */
157
158         unsigned char buffer[512];
159         int const N = dcp::base64_decode (pub, buffer, 1024);
160
161         /* Hash it with SHA1 (without the first 24 bytes, for reasons that are not entirely clear) */
162
163         SHA_CTX context;
164         if (!SHA1_Init (&context)) {
165                 throw dcp::MiscError ("could not init SHA1 context");
166         }
167
168         if (!SHA1_Update (&context, buffer + 24, N - 24)) {
169                 throw dcp::MiscError ("could not update SHA1 digest");
170         }
171
172         unsigned char digest[SHA_DIGEST_LENGTH];
173         if (!SHA1_Final (digest, &context)) {
174                 throw dcp::MiscError ("could not finish SHA1 digest");
175         }
176
177         char digest_base64[64];
178         string dig = Kumu::base64encode (digest, SHA_DIGEST_LENGTH, digest_base64, 64);
179 #ifdef LIBDCP_WINDOWS
180         boost::replace_all (dig, "/", "\\/");
181 #else
182         boost::replace_all (dig, "/", "\\\\/");
183 #endif
184         return dig;
185 }
186
187
188 CertificateChain::CertificateChain (
189         boost::filesystem::path openssl,
190         int validity_in_days,
191         string organisation,
192         string organisational_unit,
193         string root_common_name,
194         string intermediate_common_name,
195         string leaf_common_name
196         )
197 {
198         auto directory = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path ();
199         filesystem::create_directories(directory);
200
201         auto const cwd = boost::filesystem::current_path();
202         /* On Windows we will use cmd.exe here, and that doesn't work with UNC paths, so make sure
203          * we don't use our own filesystem::current_path() as it will make the current working
204          * directory a UNC path.
205          */
206         boost::filesystem::current_path(directory);
207
208         string quoted_openssl = "\"" + openssl.string() + "\"";
209
210         command (quoted_openssl + " genrsa -out ca.key 2048");
211
212         {
213                 ofstream f ("ca.cnf");
214                 f << "[ req ]\n"
215                   << "distinguished_name = req_distinguished_name\n"
216                   << "x509_extensions   = v3_ca\n"
217                   << "string_mask = nombstr\n"
218                   << "[ v3_ca ]\n"
219                   << "basicConstraints = critical,CA:true,pathlen:3\n"
220                   << "keyUsage = keyCertSign,cRLSign\n"
221                   << "subjectKeyIdentifier = hash\n"
222                   << "authorityKeyIdentifier = keyid:always,issuer:always\n"
223                   << "[ req_distinguished_name ]\n"
224                   << "O = Unique organization name\n"
225                   << "OU = Organization unit\n"
226                   << "CN = Entity and dnQualifier\n";
227         }
228
229         string const ca_subject = "/O=" + organisation +
230                 "/OU=" + organisational_unit +
231                 "/CN=" + root_common_name +
232                 "/dnQualifier=" + public_key_digest ("ca.key", openssl);
233
234         {
235                 command (
236                         String::compose (
237                                 "%1 req -new -x509 -sha256 -config ca.cnf -days %2 -set_serial 5"
238                                 " -subj \"%3\" -key ca.key -outform PEM -out ca.self-signed.pem",
239                                 quoted_openssl, validity_in_days, ca_subject
240                                 )
241                         );
242         }
243
244         command (quoted_openssl + " genrsa -out intermediate.key 2048");
245
246         {
247                 ofstream f ("intermediate.cnf");
248                 f << "[ default ]\n"
249                   << "distinguished_name = req_distinguished_name\n"
250                   << "x509_extensions = v3_ca\n"
251                   << "string_mask = nombstr\n"
252                   << "[ v3_ca ]\n"
253                   << "basicConstraints = critical,CA:true,pathlen:2\n"
254                   << "keyUsage = keyCertSign,cRLSign\n"
255                   << "subjectKeyIdentifier = hash\n"
256                   << "authorityKeyIdentifier = keyid:always,issuer:always\n"
257                   << "[ req_distinguished_name ]\n"
258                   << "O = Unique organization name\n"
259                   << "OU = Organization unit\n"
260                   << "CN = Entity and dnQualifier\n";
261         }
262
263         string const inter_subject = "/O=" + organisation +
264                 "/OU=" + organisational_unit +
265                 "/CN=" + intermediate_common_name +
266                 "/dnQualifier=" + public_key_digest ("intermediate.key", openssl);
267
268         {
269                 command (
270                         String::compose (
271                                 "%1 req -new -config intermediate.cnf -days %2 -subj \"%3\" -key intermediate.key -out intermediate.csr",
272                                 quoted_openssl, validity_in_days - 1, inter_subject
273                                 )
274                         );
275         }
276
277         command (
278                 String::compose (
279                         "%1 x509 -req -sha256 -days %2 -CA ca.self-signed.pem -CAkey ca.key -set_serial 6"
280                         " -in intermediate.csr -extfile intermediate.cnf -extensions v3_ca -out intermediate.signed.pem",
281                         quoted_openssl, validity_in_days - 1
282                         )
283                 );
284
285         command (quoted_openssl + " genrsa -out leaf.key 2048");
286
287         {
288                 ofstream f ("leaf.cnf");
289                 f << "[ default ]\n"
290                   << "distinguished_name = req_distinguished_name\n"
291                   << "x509_extensions   = v3_ca\n"
292                   << "string_mask = nombstr\n"
293                   << "[ v3_ca ]\n"
294                   << "basicConstraints = critical,CA:false\n"
295                   << "keyUsage = digitalSignature,keyEncipherment\n"
296                   << "subjectKeyIdentifier = hash\n"
297                   << "authorityKeyIdentifier = keyid,issuer:always\n"
298                   << "[ req_distinguished_name ]\n"
299                   << "O = Unique organization name\n"
300                   << "OU = Organization unit\n"
301                   << "CN = Entity and dnQualifier\n";
302         }
303
304         string const leaf_subject = "/O=" + organisation +
305                 "/OU=" + organisational_unit +
306                 "/CN=" + leaf_common_name +
307                 "/dnQualifier=" + public_key_digest ("leaf.key", openssl);
308
309         {
310                 command (
311                         String::compose (
312                                 "%1 req -new -config leaf.cnf -days %2 -subj \"%3\" -key leaf.key -outform PEM -out leaf.csr",
313                                 quoted_openssl, validity_in_days - 2, leaf_subject
314                                 )
315                         );
316         }
317
318         command (
319                 String::compose (
320                         "%1 x509 -req -sha256 -days %2 -CA intermediate.signed.pem -CAkey intermediate.key"
321                         " -set_serial 7 -in leaf.csr -extfile leaf.cnf -extensions v3_ca -out leaf.signed.pem",
322                         quoted_openssl, validity_in_days - 2
323                         )
324                 );
325
326         /* Use boost:: rather than dcp:: here so we don't force UNC into the current path if it
327          * wasn't there before.
328          */
329         boost::filesystem::current_path(cwd);
330
331         _certificates.push_back (dcp::Certificate(dcp::file_to_string(directory / "ca.self-signed.pem")));
332         _certificates.push_back (dcp::Certificate(dcp::file_to_string(directory / "intermediate.signed.pem")));
333         _certificates.push_back (dcp::Certificate(dcp::file_to_string(directory / "leaf.signed.pem")));
334
335         _key = dcp::file_to_string (directory / "leaf.key");
336
337         filesystem::remove_all(directory);
338 }
339
340
341 CertificateChain::CertificateChain (string s)
342 {
343         while (true) {
344                 try {
345                         Certificate c;
346                         s = c.read_string (s);
347                         _certificates.push_back (c);
348                 } catch (MiscError& e) {
349                         /* Failed to read a certificate, just stop */
350                         break;
351                 }
352         }
353
354         /* This will throw an exception if the chain cannot be ordered */
355         leaf_to_root ();
356 }
357
358
359 Certificate
360 CertificateChain::root () const
361 {
362         DCP_ASSERT (!_certificates.empty());
363         return root_to_leaf().front();
364 }
365
366
367 Certificate
368 CertificateChain::leaf () const
369 {
370         DCP_ASSERT (!_certificates.empty());
371         return root_to_leaf().back();
372 }
373
374
375 CertificateChain::List
376 CertificateChain::leaf_to_root () const
377 {
378         auto l = root_to_leaf ();
379         std::reverse (l.begin(), l.end());
380         return l;
381 }
382
383
384 CertificateChain::List
385 CertificateChain::unordered () const
386 {
387         return _certificates;
388 }
389
390
391 void
392 CertificateChain::add (Certificate c)
393 {
394         _certificates.push_back (c);
395 }
396
397
398 void
399 CertificateChain::remove (Certificate c)
400 {
401         auto i = std::find(_certificates.begin(), _certificates.end(), c);
402         if (i != _certificates.end()) {
403                 _certificates.erase (i);
404         }
405 }
406
407
408 void
409 CertificateChain::remove (int i)
410 {
411         auto j = _certificates.begin ();
412         while (j != _certificates.end () && i > 0) {
413                 --i;
414                 ++j;
415         }
416
417         if (j != _certificates.end ()) {
418                 _certificates.erase (j);
419         }
420 }
421
422
423 bool
424 CertificateChain::chain_valid () const
425 {
426         return chain_valid (_certificates);
427 }
428
429
430 /** @param error if non-null, filled with an error if a certificate in the list has a
431  *  a problem.
432  *  @return true if all the given certificates verify OK, and are in the correct order in the list
433  *  (root to leaf).  false if any certificate has a problem, or the order is wrong.
434  */
435 bool
436 CertificateChain::chain_valid(List const & chain, string* error) const
437 {
438         /* Here I am taking a chain of certificates A/B/C/D and checking validity of B wrt A,
439            C wrt B and D wrt C.  It also appears necessary to check the issuer of B/C/D matches
440            the subject of A/B/C; I don't understand why.  I'm sure there's a better way of doing
441            this with OpenSSL but the documentation does not appear not likely to reveal it
442            any time soon.
443         */
444
445         auto store = X509_STORE_new ();
446         if (!store) {
447                 throw MiscError ("could not create X509 store");
448         }
449
450         /* Put all the certificates into the store */
451         for (auto const& i: chain) {
452                 if (!X509_STORE_add_cert(store, i.x509())) {
453                         X509_STORE_free(store);
454                         return false;
455                 }
456         }
457
458         /* Verify each one */
459         for (auto i = chain.begin(); i != chain.end(); ++i) {
460
461                 auto j = i;
462                 ++j;
463                 if (j == chain.end ()) {
464                         break;
465                 }
466
467                 auto ctx = X509_STORE_CTX_new ();
468                 if (!ctx) {
469                         X509_STORE_free (store);
470                         throw MiscError ("could not create X509 store context");
471                 }
472
473                 X509_STORE_set_flags (store, 0);
474                 if (!X509_STORE_CTX_init (ctx, store, j->x509(), 0)) {
475                         X509_STORE_CTX_free (ctx);
476                         X509_STORE_free (store);
477                         throw MiscError ("could not initialise X509 store context");
478                 }
479
480                 int const v = X509_verify_cert (ctx);
481
482                 if (v != 1) {
483                         X509_STORE_free (store);
484                         if (error) {
485                                 *error = X509_verify_cert_error_string(X509_STORE_CTX_get_error(ctx));
486                         }
487                         X509_STORE_CTX_free(ctx);
488                         return false;
489                 }
490
491                 X509_STORE_CTX_free(ctx);
492
493                 /* I don't know why OpenSSL doesn't check this stuff
494                    in verify_cert, but without these checks the
495                    certificates_validation8 test fails.
496                 */
497                 if (j->issuer() != i->subject() || j->subject() == i->subject()) {
498                         X509_STORE_free (store);
499                         return false;
500                 }
501
502         }
503
504         X509_STORE_free (store);
505
506         return true;
507 }
508
509
510 bool
511 CertificateChain::private_key_valid () const
512 {
513         if (_certificates.empty ()) {
514                 return true;
515         }
516
517         if (!_key) {
518                 return false;
519         }
520
521         auto bio = BIO_new_mem_buf (const_cast<char *> (_key->c_str ()), -1);
522         if (!bio) {
523                 throw MiscError ("could not create memory BIO");
524         }
525
526         auto private_key = PEM_read_bio_RSAPrivateKey (bio, 0, 0, 0);
527         if (!private_key) {
528                 return false;
529         }
530
531         auto public_key = leaf().public_key ();
532
533 #if OPENSSL_VERSION_NUMBER > 0x10100000L
534         BIGNUM const * private_key_n;
535         RSA_get0_key(private_key, &private_key_n, 0, 0);
536         BIGNUM const * public_key_n;
537         RSA_get0_key(public_key, &public_key_n, 0, 0);
538         if (!private_key_n || !public_key_n) {
539                 return false;
540         }
541         bool const valid = !BN_cmp (private_key_n, public_key_n);
542 #else
543         bool const valid = !BN_cmp (private_key->n, public_key->n);
544 #endif
545         BIO_free (bio);
546
547         return valid;
548 }
549
550
551 bool
552 CertificateChain::valid (string* reason) const
553 {
554         try {
555                 root_to_leaf ();
556         } catch (CertificateChainError& e) {
557                 if (reason) {
558                         *reason = "certificates do not form a chain";
559                 }
560                 return false;
561         }
562
563         if (!private_key_valid ()) {
564                 if (reason) {
565                         *reason = "private key does not exist, or does not match leaf certificate";
566                 }
567                 return false;
568         }
569
570         return true;
571 }
572
573
574 CertificateChain::List
575 CertificateChain::root_to_leaf () const
576 {
577         auto rtl = _certificates;
578         std::sort (rtl.begin(), rtl.end());
579         string error;
580         do {
581                 if (chain_valid(rtl, &error)) {
582                         return rtl;
583                 }
584         } while (std::next_permutation (rtl.begin(), rtl.end()));
585
586         throw CertificateChainError(error.empty() ? string{"certificate chain is not consistent"} : error);
587 }
588
589
590 void
591 CertificateChain::sign (xmlpp::Element* parent, Standard standard) const
592 {
593         /* <Signer> */
594
595         parent->add_child_text("  ");
596         auto signer = parent->add_child("Signer");
597         signer->set_namespace_declaration ("http://www.w3.org/2000/09/xmldsig#", "dsig");
598         auto data = signer->add_child("X509Data", "dsig");
599         auto serial_element = data->add_child("X509IssuerSerial", "dsig");
600         serial_element->add_child("X509IssuerName", "dsig")->add_child_text (leaf().issuer());
601         serial_element->add_child("X509SerialNumber", "dsig")->add_child_text (leaf().serial());
602         data->add_child("X509SubjectName", "dsig")->add_child_text (leaf().subject());
603
604         indent (signer, 2);
605
606         /* <Signature> */
607
608         parent->add_child_text("\n  ");
609         auto signature = parent->add_child("Signature");
610         signature->set_namespace_declaration ("http://www.w3.org/2000/09/xmldsig#", "dsig");
611         signature->set_namespace ("dsig");
612         parent->add_child_text("\n");
613
614         auto signed_info = signature->add_child ("SignedInfo", "dsig");
615         signed_info->add_child("CanonicalizationMethod", "dsig")->set_attribute ("Algorithm", "http://www.w3.org/TR/2001/REC-xml-c14n-20010315");
616
617         if (standard == Standard::INTEROP) {
618                 signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#rsa-sha1");
619         } else {
620                 signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
621         }
622
623         auto reference = signed_info->add_child("Reference", "dsig");
624         reference->set_attribute ("URI", "");
625
626         auto transforms = reference->add_child("Transforms", "dsig");
627         transforms->add_child("Transform", "dsig")->set_attribute (
628                 "Algorithm", "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
629                 );
630
631         reference->add_child("DigestMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#sha1");
632         /* This will be filled in by the signing later */
633         reference->add_child("DigestValue", "dsig");
634
635         signature->add_child("SignatureValue", "dsig");
636         signature->add_child("KeyInfo", "dsig");
637         add_signature_value (signature, "dsig", true);
638 }
639
640
641 void
642 CertificateChain::add_signature_value (xmlpp::Element* parent, string ns, bool add_indentation) const
643 {
644         cxml::Node cp (parent);
645         auto key_info = cp.node_child("KeyInfo")->node();
646
647         /* Add the certificate chain to the KeyInfo child node of parent */
648         for (auto const& i: leaf_to_root()) {
649                 auto data = key_info->add_child("X509Data", ns);
650
651                 {
652                         auto serial = data->add_child("X509IssuerSerial", ns);
653                         serial->add_child("X509IssuerName", ns)->add_child_text (i.issuer ());
654                         serial->add_child("X509SerialNumber", ns)->add_child_text (i.serial ());
655                 }
656
657                 data->add_child("X509Certificate", ns)->add_child_text (i.certificate());
658         }
659
660         auto signature_context = xmlSecDSigCtxCreate (0);
661         if (signature_context == 0) {
662                 throw MiscError ("could not create signature context");
663         }
664
665         signature_context->signKey = xmlSecCryptoAppKeyLoadMemory (
666                 reinterpret_cast<const unsigned char *> (_key->c_str()), _key->size(), xmlSecKeyDataFormatPem, 0, 0, 0
667                 );
668
669         if (signature_context->signKey == 0) {
670                 throw runtime_error ("could not read private key");
671         }
672
673         if (add_indentation) {
674                 indent (parent, 2);
675         }
676         int const r = xmlSecDSigCtxSign (signature_context, parent->cobj ());
677         if (r < 0) {
678                 throw MiscError (String::compose ("could not sign (%1)", r));
679         }
680
681         xmlSecDSigCtxDestroy (signature_context);
682 }
683
684
685 string
686 CertificateChain::chain () const
687 {
688         string o;
689         for (auto const& i: root_to_leaf()) {
690                 o += i.certificate(true);
691         }
692
693         return o;
694 }