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