Don't unconditionally clear _fonts when inspecting assets (DoM #2536).
[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 "util.h"
45 #include "warnings.h"
46 #include <asdcp/KM_util.h>
47 #include <libcxml/cxml.h>
48 LIBDCP_DISABLE_WARNINGS
49 #include <libxml++/libxml++.h>
50 LIBDCP_ENABLE_WARNINGS
51 #include <xmlsec/xmldsig.h>
52 #include <xmlsec/dl.h>
53 #include <xmlsec/app.h>
54 #include <xmlsec/crypto.h>
55 #include <openssl/sha.h>
56 #include <openssl/bio.h>
57 #include <openssl/evp.h>
58 #include <openssl/pem.h>
59 #include <openssl/rsa.h>
60 #include <boost/filesystem.hpp>
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, boost::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         boost::filesystem::create_directories (directory);
200
201         auto const cwd = boost::filesystem::current_path ();
202         boost::filesystem::current_path (directory);
203
204         string quoted_openssl = "\"" + openssl.string() + "\"";
205
206         command (quoted_openssl + " genrsa -out ca.key 2048");
207
208         {
209                 ofstream f ("ca.cnf");
210                 f << "[ req ]\n"
211                   << "distinguished_name = req_distinguished_name\n"
212                   << "x509_extensions   = v3_ca\n"
213                   << "string_mask = nombstr\n"
214                   << "[ v3_ca ]\n"
215                   << "basicConstraints = critical,CA:true,pathlen:3\n"
216                   << "keyUsage = keyCertSign,cRLSign\n"
217                   << "subjectKeyIdentifier = hash\n"
218                   << "authorityKeyIdentifier = keyid:always,issuer:always\n"
219                   << "[ req_distinguished_name ]\n"
220                   << "O = Unique organization name\n"
221                   << "OU = Organization unit\n"
222                   << "CN = Entity and dnQualifier\n";
223         }
224
225         string const ca_subject = "/O=" + organisation +
226                 "/OU=" + organisational_unit +
227                 "/CN=" + root_common_name +
228                 "/dnQualifier=" + public_key_digest ("ca.key", openssl);
229
230         {
231                 command (
232                         String::compose (
233                                 "%1 req -new -x509 -sha256 -config ca.cnf -days %2 -set_serial 5"
234                                 " -subj \"%3\" -key ca.key -outform PEM -out ca.self-signed.pem",
235                                 quoted_openssl, validity_in_days, ca_subject
236                                 )
237                         );
238         }
239
240         command (quoted_openssl + " genrsa -out intermediate.key 2048");
241
242         {
243                 ofstream f ("intermediate.cnf");
244                 f << "[ default ]\n"
245                   << "distinguished_name = req_distinguished_name\n"
246                   << "x509_extensions = v3_ca\n"
247                   << "string_mask = nombstr\n"
248                   << "[ v3_ca ]\n"
249                   << "basicConstraints = critical,CA:true,pathlen:2\n"
250                   << "keyUsage = keyCertSign,cRLSign\n"
251                   << "subjectKeyIdentifier = hash\n"
252                   << "authorityKeyIdentifier = keyid:always,issuer:always\n"
253                   << "[ req_distinguished_name ]\n"
254                   << "O = Unique organization name\n"
255                   << "OU = Organization unit\n"
256                   << "CN = Entity and dnQualifier\n";
257         }
258
259         string const inter_subject = "/O=" + organisation +
260                 "/OU=" + organisational_unit +
261                 "/CN=" + intermediate_common_name +
262                 "/dnQualifier=" + public_key_digest ("intermediate.key", openssl);
263
264         {
265                 command (
266                         String::compose (
267                                 "%1 req -new -config intermediate.cnf -days %2 -subj \"%3\" -key intermediate.key -out intermediate.csr",
268                                 quoted_openssl, validity_in_days - 1, inter_subject
269                                 )
270                         );
271         }
272
273         command (
274                 String::compose (
275                         "%1 x509 -req -sha256 -days %2 -CA ca.self-signed.pem -CAkey ca.key -set_serial 6"
276                         " -in intermediate.csr -extfile intermediate.cnf -extensions v3_ca -out intermediate.signed.pem",
277                         quoted_openssl, validity_in_days - 1
278                         )
279                 );
280
281         command (quoted_openssl + " genrsa -out leaf.key 2048");
282
283         {
284                 ofstream f ("leaf.cnf");
285                 f << "[ default ]\n"
286                   << "distinguished_name = req_distinguished_name\n"
287                   << "x509_extensions   = v3_ca\n"
288                   << "string_mask = nombstr\n"
289                   << "[ v3_ca ]\n"
290                   << "basicConstraints = critical,CA:false\n"
291                   << "keyUsage = digitalSignature,keyEncipherment\n"
292                   << "subjectKeyIdentifier = hash\n"
293                   << "authorityKeyIdentifier = keyid,issuer:always\n"
294                   << "[ req_distinguished_name ]\n"
295                   << "O = Unique organization name\n"
296                   << "OU = Organization unit\n"
297                   << "CN = Entity and dnQualifier\n";
298         }
299
300         string const leaf_subject = "/O=" + organisation +
301                 "/OU=" + organisational_unit +
302                 "/CN=" + leaf_common_name +
303                 "/dnQualifier=" + public_key_digest ("leaf.key", openssl);
304
305         {
306                 command (
307                         String::compose (
308                                 "%1 req -new -config leaf.cnf -days %2 -subj \"%3\" -key leaf.key -outform PEM -out leaf.csr",
309                                 quoted_openssl, validity_in_days - 2, leaf_subject
310                                 )
311                         );
312         }
313
314         command (
315                 String::compose (
316                         "%1 x509 -req -sha256 -days %2 -CA intermediate.signed.pem -CAkey intermediate.key"
317                         " -set_serial 7 -in leaf.csr -extfile leaf.cnf -extensions v3_ca -out leaf.signed.pem",
318                         quoted_openssl, validity_in_days - 2
319                         )
320                 );
321
322         boost::filesystem::current_path (cwd);
323
324         _certificates.push_back (dcp::Certificate(dcp::file_to_string(directory / "ca.self-signed.pem")));
325         _certificates.push_back (dcp::Certificate(dcp::file_to_string(directory / "intermediate.signed.pem")));
326         _certificates.push_back (dcp::Certificate(dcp::file_to_string(directory / "leaf.signed.pem")));
327
328         _key = dcp::file_to_string (directory / "leaf.key");
329
330         boost::filesystem::remove_all (directory);
331 }
332
333
334 CertificateChain::CertificateChain (string s)
335 {
336         while (true) {
337                 try {
338                         Certificate c;
339                         s = c.read_string (s);
340                         _certificates.push_back (c);
341                 } catch (MiscError& e) {
342                         /* Failed to read a certificate, just stop */
343                         break;
344                 }
345         }
346
347         /* This will throw an exception if the chain cannot be ordered */
348         leaf_to_root ();
349 }
350
351
352 Certificate
353 CertificateChain::root () const
354 {
355         DCP_ASSERT (!_certificates.empty());
356         return root_to_leaf().front();
357 }
358
359
360 Certificate
361 CertificateChain::leaf () const
362 {
363         DCP_ASSERT (!_certificates.empty());
364         return root_to_leaf().back();
365 }
366
367
368 CertificateChain::List
369 CertificateChain::leaf_to_root () const
370 {
371         auto l = root_to_leaf ();
372         std::reverse (l.begin(), l.end());
373         return l;
374 }
375
376
377 CertificateChain::List
378 CertificateChain::unordered () const
379 {
380         return _certificates;
381 }
382
383
384 void
385 CertificateChain::add (Certificate c)
386 {
387         _certificates.push_back (c);
388 }
389
390
391 void
392 CertificateChain::remove (Certificate c)
393 {
394         auto i = std::find(_certificates.begin(), _certificates.end(), c);
395         if (i != _certificates.end()) {
396                 _certificates.erase (i);
397         }
398 }
399
400
401 void
402 CertificateChain::remove (int i)
403 {
404         auto j = _certificates.begin ();
405         while (j != _certificates.end () && i > 0) {
406                 --i;
407                 ++j;
408         }
409
410         if (j != _certificates.end ()) {
411                 _certificates.erase (j);
412         }
413 }
414
415
416 bool
417 CertificateChain::chain_valid () const
418 {
419         return chain_valid (_certificates);
420 }
421
422
423 /** @param error if non-null, filled with an error if a certificate in the list has a
424  *  a problem.
425  *  @return true if all the given certificates verify OK, and are in the correct order in the list
426  *  (root to leaf).  false if any certificate has a problem, or the order is wrong.
427  */
428 bool
429 CertificateChain::chain_valid(List const & chain, string* error) const
430 {
431         /* Here I am taking a chain of certificates A/B/C/D and checking validity of B wrt A,
432            C wrt B and D wrt C.  It also appears necessary to check the issuer of B/C/D matches
433            the subject of A/B/C; I don't understand why.  I'm sure there's a better way of doing
434            this with OpenSSL but the documentation does not appear not likely to reveal it
435            any time soon.
436         */
437
438         auto store = X509_STORE_new ();
439         if (!store) {
440                 throw MiscError ("could not create X509 store");
441         }
442
443         /* Put all the certificates into the store */
444         for (auto const& i: chain) {
445                 if (!X509_STORE_add_cert(store, i.x509())) {
446                         X509_STORE_free(store);
447                         return false;
448                 }
449         }
450
451         /* Verify each one */
452         for (auto i = chain.begin(); i != chain.end(); ++i) {
453
454                 auto j = i;
455                 ++j;
456                 if (j == chain.end ()) {
457                         break;
458                 }
459
460                 auto ctx = X509_STORE_CTX_new ();
461                 if (!ctx) {
462                         X509_STORE_free (store);
463                         throw MiscError ("could not create X509 store context");
464                 }
465
466                 X509_STORE_set_flags (store, 0);
467                 if (!X509_STORE_CTX_init (ctx, store, j->x509(), 0)) {
468                         X509_STORE_CTX_free (ctx);
469                         X509_STORE_free (store);
470                         throw MiscError ("could not initialise X509 store context");
471                 }
472
473                 int const v = X509_verify_cert (ctx);
474                 X509_STORE_CTX_free (ctx);
475
476                 if (v != 1) {
477                         X509_STORE_free (store);
478                         if (error) {
479                                 *error = X509_verify_cert_error_string(X509_STORE_CTX_get_error(ctx));
480                         }
481                         return false;
482                 }
483
484                 /* I don't know why OpenSSL doesn't check this stuff
485                    in verify_cert, but without these checks the
486                    certificates_validation8 test fails.
487                 */
488                 if (j->issuer() != i->subject() || j->subject() == i->subject()) {
489                         X509_STORE_free (store);
490                         return false;
491                 }
492
493         }
494
495         X509_STORE_free (store);
496
497         return true;
498 }
499
500
501 bool
502 CertificateChain::private_key_valid () const
503 {
504         if (_certificates.empty ()) {
505                 return true;
506         }
507
508         if (!_key) {
509                 return false;
510         }
511
512         auto bio = BIO_new_mem_buf (const_cast<char *> (_key->c_str ()), -1);
513         if (!bio) {
514                 throw MiscError ("could not create memory BIO");
515         }
516
517         auto private_key = PEM_read_bio_RSAPrivateKey (bio, 0, 0, 0);
518         if (!private_key) {
519                 return false;
520         }
521
522         auto public_key = leaf().public_key ();
523
524 #if OPENSSL_VERSION_NUMBER > 0x10100000L
525         BIGNUM const * private_key_n;
526         RSA_get0_key(private_key, &private_key_n, 0, 0);
527         BIGNUM const * public_key_n;
528         RSA_get0_key(public_key, &public_key_n, 0, 0);
529         if (!private_key_n || !public_key_n) {
530                 return false;
531         }
532         bool const valid = !BN_cmp (private_key_n, public_key_n);
533 #else
534         bool const valid = !BN_cmp (private_key->n, public_key->n);
535 #endif
536         BIO_free (bio);
537
538         return valid;
539 }
540
541
542 bool
543 CertificateChain::valid (string* reason) const
544 {
545         try {
546                 root_to_leaf ();
547         } catch (CertificateChainError& e) {
548                 if (reason) {
549                         *reason = "certificates do not form a chain";
550                 }
551                 return false;
552         }
553
554         if (!private_key_valid ()) {
555                 if (reason) {
556                         *reason = "private key does not exist, or does not match leaf certificate";
557                 }
558                 return false;
559         }
560
561         return true;
562 }
563
564
565 CertificateChain::List
566 CertificateChain::root_to_leaf () const
567 {
568         auto rtl = _certificates;
569         std::sort (rtl.begin(), rtl.end());
570         string error;
571         do {
572                 if (chain_valid(rtl, &error)) {
573                         return rtl;
574                 }
575         } while (std::next_permutation (rtl.begin(), rtl.end()));
576
577         throw CertificateChainError(error.empty() ? string{"certificate chain is not consistent"} : error);
578 }
579
580
581 void
582 CertificateChain::sign (xmlpp::Element* parent, Standard standard) const
583 {
584         /* <Signer> */
585
586         parent->add_child_text("  ");
587         auto signer = parent->add_child("Signer");
588         signer->set_namespace_declaration ("http://www.w3.org/2000/09/xmldsig#", "dsig");
589         auto data = signer->add_child("X509Data", "dsig");
590         auto serial_element = data->add_child("X509IssuerSerial", "dsig");
591         serial_element->add_child("X509IssuerName", "dsig")->add_child_text (leaf().issuer());
592         serial_element->add_child("X509SerialNumber", "dsig")->add_child_text (leaf().serial());
593         data->add_child("X509SubjectName", "dsig")->add_child_text (leaf().subject());
594
595         indent (signer, 2);
596
597         /* <Signature> */
598
599         parent->add_child_text("\n  ");
600         auto signature = parent->add_child("Signature");
601         signature->set_namespace_declaration ("http://www.w3.org/2000/09/xmldsig#", "dsig");
602         signature->set_namespace ("dsig");
603         parent->add_child_text("\n");
604
605         auto signed_info = signature->add_child ("SignedInfo", "dsig");
606         signed_info->add_child("CanonicalizationMethod", "dsig")->set_attribute ("Algorithm", "http://www.w3.org/TR/2001/REC-xml-c14n-20010315");
607
608         if (standard == Standard::INTEROP) {
609                 signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#rsa-sha1");
610         } else {
611                 signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
612         }
613
614         auto reference = signed_info->add_child("Reference", "dsig");
615         reference->set_attribute ("URI", "");
616
617         auto transforms = reference->add_child("Transforms", "dsig");
618         transforms->add_child("Transform", "dsig")->set_attribute (
619                 "Algorithm", "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
620                 );
621
622         reference->add_child("DigestMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#sha1");
623         /* This will be filled in by the signing later */
624         reference->add_child("DigestValue", "dsig");
625
626         signature->add_child("SignatureValue", "dsig");
627         signature->add_child("KeyInfo", "dsig");
628         add_signature_value (signature, "dsig", true);
629 }
630
631
632 void
633 CertificateChain::add_signature_value (xmlpp::Element* parent, string ns, bool add_indentation) const
634 {
635         cxml::Node cp (parent);
636         auto key_info = cp.node_child("KeyInfo")->node();
637
638         /* Add the certificate chain to the KeyInfo child node of parent */
639         for (auto const& i: leaf_to_root()) {
640                 auto data = key_info->add_child("X509Data", ns);
641
642                 {
643                         auto serial = data->add_child("X509IssuerSerial", ns);
644                         serial->add_child("X509IssuerName", ns)->add_child_text (i.issuer ());
645                         serial->add_child("X509SerialNumber", ns)->add_child_text (i.serial ());
646                 }
647
648                 data->add_child("X509Certificate", ns)->add_child_text (i.certificate());
649         }
650
651         auto signature_context = xmlSecDSigCtxCreate (0);
652         if (signature_context == 0) {
653                 throw MiscError ("could not create signature context");
654         }
655
656         signature_context->signKey = xmlSecCryptoAppKeyLoadMemory (
657                 reinterpret_cast<const unsigned char *> (_key->c_str()), _key->size(), xmlSecKeyDataFormatPem, 0, 0, 0
658                 );
659
660         if (signature_context->signKey == 0) {
661                 throw runtime_error ("could not read private key");
662         }
663
664         if (add_indentation) {
665                 indent (parent, 2);
666         }
667         int const r = xmlSecDSigCtxSign (signature_context, parent->cobj ());
668         if (r < 0) {
669                 throw MiscError (String::compose ("could not sign (%1)", r));
670         }
671
672         xmlSecDSigCtxDestroy (signature_context);
673 }
674
675
676 string
677 CertificateChain::chain () const
678 {
679         string o;
680         for (auto const& i: root_to_leaf()) {
681                 o += i.certificate(true);
682         }
683
684         return o;
685 }