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"
44 #include "filesystem.h"
45 #include "scope_guard.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>
71 using std::runtime_error;
75 /** Run a shell command.
76 * @param cmd Command to run (UTF8-encoded).
82 /* We need to use CreateProcessW on Windows so that the UTF-8/16 mess
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) {
94 STARTUPINFOW startup_info;
95 memset (&startup_info, 0, sizeof (startup_info));
96 startup_info.cb = sizeof (startup_info);
97 PROCESS_INFORMATION process_info;
99 /* XXX: this doesn't actually seem to work; failing commands end up with
102 if (CreateProcessW (0, buffer, 0, 0, FALSE, CREATE_NO_WINDOW, 0, 0, &startup_info, &process_info)) {
103 WaitForSingleObject (process_info.hProcess, INFINITE);
105 if (GetExitCodeProcess (process_info.hProcess, &c)) {
108 CloseHandle (process_info.hProcess);
109 CloseHandle (process_info.hThread);
114 cmd += " 2> /dev/null";
115 int const r = system (cmd.c_str ());
116 int const code = WEXITSTATUS (r);
119 throw dcp::MiscError(String::compose("error %1 in %2 within %3", code, cmd, filesystem::current_path().string()));
125 dcp::public_key_digest(RSA* public_key)
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);
132 throw MiscError("Could not convert public key to DER");
135 /* Hash it with SHA1 (without the first 24 bytes, for reasons that are not entirely clear) */
138 if (!SHA1_Init (&context)) {
139 throw dcp::MiscError ("could not init SHA1 context");
142 if (!SHA1_Update(&context, buffer + 24, length - 24)) {
143 throw dcp::MiscError ("could not update SHA1 digest");
146 unsigned char digest[SHA_DIGEST_LENGTH];
147 if (!SHA1_Final (digest, &context)) {
148 throw dcp::MiscError ("could not finish SHA1 digest");
151 char digest_base64[64];
152 string dig = Kumu::base64encode (digest, SHA_DIGEST_LENGTH, digest_base64, 64);
153 return escape_digest(dig);
158 dcp::escape_digest(string digest)
159 #ifdef LIBDCP_WINDOWS
160 boost::replace_all(digest, "/", "\\/");
162 boost::replace_all(digest, "/", "\\\\/");
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.
174 dcp::public_key_digest(boost::filesystem::path private_key_file)
176 auto private_key_string = dcp::file_to_string(private_key_file);
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");
183 dcp::ScopeGuard sg_private_key_bio([private_key_bio]() { BIO_free(private_key_bio); });
185 /* Extract private key */
186 auto private_key = PEM_read_bio_PrivateKey(private_key_bio, nullptr, nullptr, nullptr);
188 throw MiscError("Could not read private key");
190 dcp::ScopeGuard sg_private_key([private_key]() { EVP_PKEY_free(private_key); });
192 /* Get public key from private key */
193 auto public_key = EVP_PKEY_get1_RSA(private_key);
195 throw MiscError("Could not obtain public key");
197 dcp::ScopeGuard sg_public_key([public_key]() { RSA_free(public_key); });
199 return public_key_digest(public_key);
203 CertificateChain::CertificateChain (
204 boost::filesystem::path openssl,
205 int validity_in_days,
207 string organisational_unit,
208 string root_common_name,
209 string intermediate_common_name,
210 string leaf_common_name
213 auto directory = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path ();
214 filesystem::create_directories(directory);
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.
221 boost::filesystem::current_path(directory);
223 string quoted_openssl = "\"" + openssl.string() + "\"";
225 command (quoted_openssl + " genrsa -out ca.key 2048");
228 ofstream f ("ca.cnf");
230 << "distinguished_name = req_distinguished_name\n"
231 << "x509_extensions = v3_ca\n"
232 << "string_mask = nombstr\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";
244 string const ca_subject = "/O=" + organisation +
245 "/OU=" + organisational_unit +
246 "/CN=" + root_common_name +
247 "/dnQualifier=" + public_key_digest ("ca.key");
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
259 command (quoted_openssl + " genrsa -out intermediate.key 2048");
262 ofstream f ("intermediate.cnf");
264 << "distinguished_name = req_distinguished_name\n"
265 << "x509_extensions = v3_ca\n"
266 << "string_mask = nombstr\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";
278 string const inter_subject = "/O=" + organisation +
279 "/OU=" + organisational_unit +
280 "/CN=" + intermediate_common_name +
281 "/dnQualifier=" + public_key_digest ("intermediate.key");
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
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
300 command (quoted_openssl + " genrsa -out leaf.key 2048");
303 ofstream f ("leaf.cnf");
305 << "distinguished_name = req_distinguished_name\n"
306 << "x509_extensions = v3_ca\n"
307 << "string_mask = nombstr\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";
319 string const leaf_subject = "/O=" + organisation +
320 "/OU=" + organisational_unit +
321 "/CN=" + leaf_common_name +
322 "/dnQualifier=" + public_key_digest ("leaf.key");
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
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
341 /* Use boost:: rather than dcp:: here so we don't force UNC into the current path if it
342 * wasn't there before.
344 boost::filesystem::current_path(cwd);
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")));
350 _key = dcp::file_to_string (directory / "leaf.key");
352 filesystem::remove_all(directory);
356 CertificateChain::CertificateChain (string s)
361 s = c.read_string (s);
362 _certificates.push_back (c);
363 } catch (MiscError& e) {
364 /* Failed to read a certificate, just stop */
369 /* This will throw an exception if the chain cannot be ordered */
375 CertificateChain::root () const
377 DCP_ASSERT (!_certificates.empty());
378 return root_to_leaf().front();
383 CertificateChain::leaf () const
385 DCP_ASSERT (!_certificates.empty());
386 return root_to_leaf().back();
390 CertificateChain::List
391 CertificateChain::leaf_to_root () const
393 auto l = root_to_leaf ();
394 std::reverse (l.begin(), l.end());
399 CertificateChain::List
400 CertificateChain::unordered () const
402 return _certificates;
407 CertificateChain::add (Certificate c)
409 _certificates.push_back (c);
414 CertificateChain::remove (Certificate c)
416 auto i = std::find(_certificates.begin(), _certificates.end(), c);
417 if (i != _certificates.end()) {
418 _certificates.erase (i);
424 CertificateChain::remove (int i)
426 auto j = _certificates.begin ();
427 while (j != _certificates.end () && i > 0) {
432 if (j != _certificates.end ()) {
433 _certificates.erase (j);
439 CertificateChain::chain_valid () const
441 return chain_valid (_certificates);
445 /** @param error if non-null, filled with an error if a certificate in the list has a
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.
451 CertificateChain::chain_valid(List const & chain, string* error) const
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
460 auto store = X509_STORE_new ();
462 throw MiscError ("could not create X509 store");
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);
473 /* Verify each one */
474 for (auto i = chain.begin(); i != chain.end(); ++i) {
478 if (j == chain.end ()) {
482 auto ctx = X509_STORE_CTX_new ();
484 X509_STORE_free (store);
485 throw MiscError ("could not create X509 store context");
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");
495 int const v = X509_verify_cert (ctx);
498 X509_STORE_free (store);
500 *error = X509_verify_cert_error_string(X509_STORE_CTX_get_error(ctx));
502 X509_STORE_CTX_free(ctx);
506 X509_STORE_CTX_free(ctx);
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.
512 if (j->issuer() != i->subject() || j->subject() == i->subject()) {
513 X509_STORE_free (store);
519 X509_STORE_free (store);
526 CertificateChain::private_key_valid () const
528 if (_certificates.empty ()) {
536 auto bio = BIO_new_mem_buf (const_cast<char *> (_key->c_str ()), -1);
538 throw MiscError ("could not create memory BIO");
541 auto private_key = PEM_read_bio_RSAPrivateKey (bio, 0, 0, 0);
546 auto public_key = leaf().public_key ();
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) {
556 bool const valid = !BN_cmp (private_key_n, public_key_n);
558 bool const valid = !BN_cmp (private_key->n, public_key->n);
567 CertificateChain::valid (string* reason) const
571 } catch (CertificateChainError& e) {
573 *reason = "certificates do not form a chain";
578 if (!private_key_valid ()) {
580 *reason = "private key does not exist, or does not match leaf certificate";
589 CertificateChain::List
590 CertificateChain::root_to_leaf () const
592 auto rtl = _certificates;
593 std::sort (rtl.begin(), rtl.end());
596 if (chain_valid(rtl, &error)) {
599 } while (std::next_permutation (rtl.begin(), rtl.end()));
601 throw CertificateChainError(error.empty() ? string{"certificate chain is not consistent"} : error);
606 CertificateChain::sign (xmlpp::Element* parent, Standard standard) const
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());
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");
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");
632 if (standard == Standard::INTEROP) {
633 signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#rsa-sha1");
635 signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
638 auto reference = signed_info->add_child("Reference", "dsig");
639 reference->set_attribute ("URI", "");
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"
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");
650 signature->add_child("SignatureValue", "dsig");
651 signature->add_child("KeyInfo", "dsig");
652 add_signature_value (signature, "dsig", true);
657 CertificateChain::add_signature_value (xmlpp::Element* parent, string ns, bool add_indentation) const
659 cxml::Node cp (parent);
660 auto key_info = cp.node_child("KeyInfo")->node();
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);
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 ());
672 data->add_child("X509Certificate", ns)->add_child_text (i.certificate());
675 auto signature_context = xmlSecDSigCtxCreate (0);
676 if (signature_context == 0) {
677 throw MiscError ("could not create signature context");
680 signature_context->signKey = xmlSecCryptoAppKeyLoadMemory (
681 reinterpret_cast<const unsigned char *> (_key->c_str()), _key->size(), xmlSecKeyDataFormatPem, 0, 0, 0
684 if (signature_context->signKey == 0) {
685 throw runtime_error ("could not read private key");
688 if (add_indentation) {
691 int const r = xmlSecDSigCtxSign (signature_context, parent->cobj ());
693 throw MiscError (String::compose ("could not sign (%1)", r));
696 xmlSecDSigCtxDestroy (signature_context);
701 CertificateChain::chain () const
704 for (auto const& i: root_to_leaf()) {
705 o += i.certificate(true);