2 Copyright (C) 2013-2021 Carl Hetherington <cth@carlh.net>
4 This file is part of libdcp.
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.
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.
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/>.
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
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.
35 /** @file src/certificate_chain.cc
36 * @brief CertificateChain class
40 #include "certificate_chain.h"
41 #include "compose.hpp"
42 #include "dcp_assert.h"
43 #include "exceptions.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>
69 using std::runtime_error;
73 /** Run a shell command.
74 * @param cmd Command to run (UTF8-encoded).
80 /* We need to use CreateProcessW on Windows so that the UTF-8/16 mess
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) {
92 STARTUPINFOW startup_info;
93 memset (&startup_info, 0, sizeof (startup_info));
94 startup_info.cb = sizeof (startup_info);
95 PROCESS_INFORMATION process_info;
97 /* XXX: this doesn't actually seem to work; failing commands end up with
100 if (CreateProcessW (0, buffer, 0, 0, FALSE, CREATE_NO_WINDOW, 0, 0, &startup_info, &process_info)) {
101 WaitForSingleObject (process_info.hProcess, INFINITE);
103 if (GetExitCodeProcess (process_info.hProcess, &c)) {
106 CloseHandle (process_info.hProcess);
107 CloseHandle (process_info.hThread);
112 cmd += " 2> /dev/null";
113 int const r = system (cmd.c_str ());
114 int const code = WEXITSTATUS (r);
117 throw dcp::MiscError (String::compose ("error %1 in %2 within %3", code, cmd, boost::filesystem::current_path().string()));
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.
128 public_key_digest (boost::filesystem::path private_key, boost::filesystem::path openssl)
130 boost::filesystem::path public_name = private_key.string() + ".public";
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()));
135 /* Read in the public key from the file */
138 ifstream f (public_name.string().c_str());
140 throw dcp::MiscError ("public key not found");
147 if (line.length() >= 10 && line.substr(0, 10) == "-----BEGIN") {
149 } else if (line.length() >= 8 && line.substr(0, 8) == "-----END") {
156 /* Decode the base64 of the public key */
158 unsigned char buffer[512];
159 int const N = dcp::base64_decode (pub, buffer, 1024);
161 /* Hash it with SHA1 (without the first 24 bytes, for reasons that are not entirely clear) */
164 if (!SHA1_Init (&context)) {
165 throw dcp::MiscError ("could not init SHA1 context");
168 if (!SHA1_Update (&context, buffer + 24, N - 24)) {
169 throw dcp::MiscError ("could not update SHA1 digest");
172 unsigned char digest[SHA_DIGEST_LENGTH];
173 if (!SHA1_Final (digest, &context)) {
174 throw dcp::MiscError ("could not finish SHA1 digest");
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, "/", "\\/");
182 boost::replace_all (dig, "/", "\\\\/");
188 CertificateChain::CertificateChain (
189 boost::filesystem::path openssl,
190 int validity_in_days,
192 string organisational_unit,
193 string root_common_name,
194 string intermediate_common_name,
195 string leaf_common_name
198 auto directory = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path ();
199 boost::filesystem::create_directories (directory);
201 auto const cwd = boost::filesystem::current_path ();
202 boost::filesystem::current_path (directory);
204 string quoted_openssl = "\"" + openssl.string() + "\"";
206 command (quoted_openssl + " genrsa -out ca.key 2048");
209 ofstream f ("ca.cnf");
211 << "distinguished_name = req_distinguished_name\n"
212 << "x509_extensions = v3_ca\n"
213 << "string_mask = nombstr\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";
225 string const ca_subject = "/O=" + organisation +
226 "/OU=" + organisational_unit +
227 "/CN=" + root_common_name +
228 "/dnQualifier=" + public_key_digest ("ca.key", openssl);
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
240 command (quoted_openssl + " genrsa -out intermediate.key 2048");
243 ofstream f ("intermediate.cnf");
245 << "distinguished_name = req_distinguished_name\n"
246 << "x509_extensions = v3_ca\n"
247 << "string_mask = nombstr\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";
259 string const inter_subject = "/O=" + organisation +
260 "/OU=" + organisational_unit +
261 "/CN=" + intermediate_common_name +
262 "/dnQualifier=" + public_key_digest ("intermediate.key", openssl);
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
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
281 command (quoted_openssl + " genrsa -out leaf.key 2048");
284 ofstream f ("leaf.cnf");
286 << "distinguished_name = req_distinguished_name\n"
287 << "x509_extensions = v3_ca\n"
288 << "string_mask = nombstr\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";
300 string const leaf_subject = "/O=" + organisation +
301 "/OU=" + organisational_unit +
302 "/CN=" + leaf_common_name +
303 "/dnQualifier=" + public_key_digest ("leaf.key", openssl);
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
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
322 boost::filesystem::current_path (cwd);
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")));
328 _key = dcp::file_to_string (directory / "leaf.key");
330 boost::filesystem::remove_all (directory);
334 CertificateChain::CertificateChain (string s)
339 s = c.read_string (s);
340 _certificates.push_back (c);
341 } catch (MiscError& e) {
342 /* Failed to read a certificate, just stop */
347 /* This will throw an exception if the chain cannot be ordered */
353 CertificateChain::root () const
355 DCP_ASSERT (!_certificates.empty());
356 return root_to_leaf().front();
361 CertificateChain::leaf () const
363 DCP_ASSERT (!_certificates.empty());
364 return root_to_leaf().back();
368 CertificateChain::List
369 CertificateChain::leaf_to_root () const
371 auto l = root_to_leaf ();
372 std::reverse (l.begin(), l.end());
377 CertificateChain::List
378 CertificateChain::unordered () const
380 return _certificates;
385 CertificateChain::add (Certificate c)
387 _certificates.push_back (c);
392 CertificateChain::remove (Certificate c)
394 auto i = std::find(_certificates.begin(), _certificates.end(), c);
395 if (i != _certificates.end()) {
396 _certificates.erase (i);
402 CertificateChain::remove (int i)
404 auto j = _certificates.begin ();
405 while (j != _certificates.end () && i > 0) {
410 if (j != _certificates.end ()) {
411 _certificates.erase (j);
417 CertificateChain::chain_valid () const
419 return chain_valid (_certificates);
423 /** @param error if non-null, filled with an error if a certificate in the list has a
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.
429 CertificateChain::chain_valid(List const & chain, string* error) const
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
438 auto store = X509_STORE_new ();
440 throw MiscError ("could not create X509 store");
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);
451 /* Verify each one */
452 for (auto i = chain.begin(); i != chain.end(); ++i) {
456 if (j == chain.end ()) {
460 auto ctx = X509_STORE_CTX_new ();
462 X509_STORE_free (store);
463 throw MiscError ("could not create X509 store context");
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");
473 int const v = X509_verify_cert (ctx);
476 X509_STORE_free (store);
478 *error = X509_verify_cert_error_string(X509_STORE_CTX_get_error(ctx));
480 X509_STORE_CTX_free(ctx);
484 X509_STORE_CTX_free(ctx);
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.
490 if (j->issuer() != i->subject() || j->subject() == i->subject()) {
491 X509_STORE_free (store);
497 X509_STORE_free (store);
504 CertificateChain::private_key_valid () const
506 if (_certificates.empty ()) {
514 auto bio = BIO_new_mem_buf (const_cast<char *> (_key->c_str ()), -1);
516 throw MiscError ("could not create memory BIO");
519 auto private_key = PEM_read_bio_RSAPrivateKey (bio, 0, 0, 0);
524 auto public_key = leaf().public_key ();
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) {
534 bool const valid = !BN_cmp (private_key_n, public_key_n);
536 bool const valid = !BN_cmp (private_key->n, public_key->n);
545 CertificateChain::valid (string* reason) const
549 } catch (CertificateChainError& e) {
551 *reason = "certificates do not form a chain";
556 if (!private_key_valid ()) {
558 *reason = "private key does not exist, or does not match leaf certificate";
567 CertificateChain::List
568 CertificateChain::root_to_leaf () const
570 auto rtl = _certificates;
571 std::sort (rtl.begin(), rtl.end());
574 if (chain_valid(rtl, &error)) {
577 } while (std::next_permutation (rtl.begin(), rtl.end()));
579 throw CertificateChainError(error.empty() ? string{"certificate chain is not consistent"} : error);
584 CertificateChain::sign (xmlpp::Element* parent, Standard standard) const
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());
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");
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");
610 if (standard == Standard::INTEROP) {
611 signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#rsa-sha1");
613 signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
616 auto reference = signed_info->add_child("Reference", "dsig");
617 reference->set_attribute ("URI", "");
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"
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");
628 signature->add_child("SignatureValue", "dsig");
629 signature->add_child("KeyInfo", "dsig");
630 add_signature_value (signature, "dsig", true);
635 CertificateChain::add_signature_value (xmlpp::Element* parent, string ns, bool add_indentation) const
637 cxml::Node cp (parent);
638 auto key_info = cp.node_child("KeyInfo")->node();
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);
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 ());
650 data->add_child("X509Certificate", ns)->add_child_text (i.certificate());
653 auto signature_context = xmlSecDSigCtxCreate (0);
654 if (signature_context == 0) {
655 throw MiscError ("could not create signature context");
658 signature_context->signKey = xmlSecCryptoAppKeyLoadMemory (
659 reinterpret_cast<const unsigned char *> (_key->c_str()), _key->size(), xmlSecKeyDataFormatPem, 0, 0, 0
662 if (signature_context->signKey == 0) {
663 throw runtime_error ("could not read private key");
666 if (add_indentation) {
669 int const r = xmlSecDSigCtxSign (signature_context, parent->cobj ());
671 throw MiscError (String::compose ("could not sign (%1)", r));
674 xmlSecDSigCtxDestroy (signature_context);
679 CertificateChain::chain () const
682 for (auto const& i: root_to_leaf()) {
683 o += i.certificate(true);