Cleanup: sorting.
[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
475                 if (v != 1) {
476                         X509_STORE_free (store);
477                         if (error) {
478                                 *error = X509_verify_cert_error_string(X509_STORE_CTX_get_error(ctx));
479                         }
480                         X509_STORE_CTX_free(ctx);
481                         return false;
482                 }
483
484                 X509_STORE_CTX_free(ctx);
485
486                 /* I don't know why OpenSSL doesn't check this stuff
487                    in verify_cert, but without these checks the
488                    certificates_validation8 test fails.
489                 */
490                 if (j->issuer() != i->subject() || j->subject() == i->subject()) {
491                         X509_STORE_free (store);
492                         return false;
493                 }
494
495         }
496
497         X509_STORE_free (store);
498
499         return true;
500 }
501
502
503 bool
504 CertificateChain::private_key_valid () const
505 {
506         if (_certificates.empty ()) {
507                 return true;
508         }
509
510         if (!_key) {
511                 return false;
512         }
513
514         auto bio = BIO_new_mem_buf (const_cast<char *> (_key->c_str ()), -1);
515         if (!bio) {
516                 throw MiscError ("could not create memory BIO");
517         }
518
519         auto private_key = PEM_read_bio_RSAPrivateKey (bio, 0, 0, 0);
520         if (!private_key) {
521                 return false;
522         }
523
524         auto public_key = leaf().public_key ();
525
526 #if OPENSSL_VERSION_NUMBER > 0x10100000L
527         BIGNUM const * private_key_n;
528         RSA_get0_key(private_key, &private_key_n, 0, 0);
529         BIGNUM const * public_key_n;
530         RSA_get0_key(public_key, &public_key_n, 0, 0);
531         if (!private_key_n || !public_key_n) {
532                 return false;
533         }
534         bool const valid = !BN_cmp (private_key_n, public_key_n);
535 #else
536         bool const valid = !BN_cmp (private_key->n, public_key->n);
537 #endif
538         BIO_free (bio);
539
540         return valid;
541 }
542
543
544 bool
545 CertificateChain::valid (string* reason) const
546 {
547         try {
548                 root_to_leaf ();
549         } catch (CertificateChainError& e) {
550                 if (reason) {
551                         *reason = "certificates do not form a chain";
552                 }
553                 return false;
554         }
555
556         if (!private_key_valid ()) {
557                 if (reason) {
558                         *reason = "private key does not exist, or does not match leaf certificate";
559                 }
560                 return false;
561         }
562
563         return true;
564 }
565
566
567 CertificateChain::List
568 CertificateChain::root_to_leaf () const
569 {
570         auto rtl = _certificates;
571         std::sort (rtl.begin(), rtl.end());
572         string error;
573         do {
574                 if (chain_valid(rtl, &error)) {
575                         return rtl;
576                 }
577         } while (std::next_permutation (rtl.begin(), rtl.end()));
578
579         throw CertificateChainError(error.empty() ? string{"certificate chain is not consistent"} : error);
580 }
581
582
583 void
584 CertificateChain::sign (xmlpp::Element* parent, Standard standard) const
585 {
586         /* <Signer> */
587
588         parent->add_child_text("  ");
589         auto signer = parent->add_child("Signer");
590         signer->set_namespace_declaration ("http://www.w3.org/2000/09/xmldsig#", "dsig");
591         auto data = signer->add_child("X509Data", "dsig");
592         auto serial_element = data->add_child("X509IssuerSerial", "dsig");
593         serial_element->add_child("X509IssuerName", "dsig")->add_child_text (leaf().issuer());
594         serial_element->add_child("X509SerialNumber", "dsig")->add_child_text (leaf().serial());
595         data->add_child("X509SubjectName", "dsig")->add_child_text (leaf().subject());
596
597         indent (signer, 2);
598
599         /* <Signature> */
600
601         parent->add_child_text("\n  ");
602         auto signature = parent->add_child("Signature");
603         signature->set_namespace_declaration ("http://www.w3.org/2000/09/xmldsig#", "dsig");
604         signature->set_namespace ("dsig");
605         parent->add_child_text("\n");
606
607         auto signed_info = signature->add_child ("SignedInfo", "dsig");
608         signed_info->add_child("CanonicalizationMethod", "dsig")->set_attribute ("Algorithm", "http://www.w3.org/TR/2001/REC-xml-c14n-20010315");
609
610         if (standard == Standard::INTEROP) {
611                 signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#rsa-sha1");
612         } else {
613                 signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
614         }
615
616         auto reference = signed_info->add_child("Reference", "dsig");
617         reference->set_attribute ("URI", "");
618
619         auto transforms = reference->add_child("Transforms", "dsig");
620         transforms->add_child("Transform", "dsig")->set_attribute (
621                 "Algorithm", "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
622                 );
623
624         reference->add_child("DigestMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#sha1");
625         /* This will be filled in by the signing later */
626         reference->add_child("DigestValue", "dsig");
627
628         signature->add_child("SignatureValue", "dsig");
629         signature->add_child("KeyInfo", "dsig");
630         add_signature_value (signature, "dsig", true);
631 }
632
633
634 void
635 CertificateChain::add_signature_value (xmlpp::Element* parent, string ns, bool add_indentation) const
636 {
637         cxml::Node cp (parent);
638         auto key_info = cp.node_child("KeyInfo")->node();
639
640         /* Add the certificate chain to the KeyInfo child node of parent */
641         for (auto const& i: leaf_to_root()) {
642                 auto data = key_info->add_child("X509Data", ns);
643
644                 {
645                         auto serial = data->add_child("X509IssuerSerial", ns);
646                         serial->add_child("X509IssuerName", ns)->add_child_text (i.issuer ());
647                         serial->add_child("X509SerialNumber", ns)->add_child_text (i.serial ());
648                 }
649
650                 data->add_child("X509Certificate", ns)->add_child_text (i.certificate());
651         }
652
653         auto signature_context = xmlSecDSigCtxCreate (0);
654         if (signature_context == 0) {
655                 throw MiscError ("could not create signature context");
656         }
657
658         signature_context->signKey = xmlSecCryptoAppKeyLoadMemory (
659                 reinterpret_cast<const unsigned char *> (_key->c_str()), _key->size(), xmlSecKeyDataFormatPem, 0, 0, 0
660                 );
661
662         if (signature_context->signKey == 0) {
663                 throw runtime_error ("could not read private key");
664         }
665
666         if (add_indentation) {
667                 indent (parent, 2);
668         }
669         int const r = xmlSecDSigCtxSign (signature_context, parent->cobj ());
670         if (r < 0) {
671                 throw MiscError (String::compose ("could not sign (%1)", r));
672         }
673
674         xmlSecDSigCtxDestroy (signature_context);
675 }
676
677
678 string
679 CertificateChain::chain () const
680 {
681         string o;
682         for (auto const& i: root_to_leaf()) {
683                 o += i.certificate(true);
684         }
685
686         return o;
687 }