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)
160 boost::replace_all(digest, "/", "\\/");
161 boost::replace_all(digest, "+", "\\+");
166 /** Extract a public key from a private key and create a SHA1 digest of it.
167 * @param private_key_file Private key filename
168 * @param openssl openssl binary name (or full path if openssl is not on the system path).
169 * @return SHA1 digest of corresponding public key, with escaped / characters.
172 dcp::public_key_digest(boost::filesystem::path private_key_file)
174 auto private_key_string = dcp::file_to_string(private_key_file);
176 /* Read private key into memory */
177 auto private_key_bio = BIO_new_mem_buf(const_cast<char*>(private_key_string.c_str()), -1);
178 if (!private_key_bio) {
179 throw MiscError("Could not create memory BIO");
181 dcp::ScopeGuard sg_private_key_bio([private_key_bio]() { BIO_free(private_key_bio); });
183 /* Extract private key */
184 auto private_key = PEM_read_bio_PrivateKey(private_key_bio, nullptr, nullptr, nullptr);
186 throw MiscError("Could not read private key");
188 dcp::ScopeGuard sg_private_key([private_key]() { EVP_PKEY_free(private_key); });
190 /* Get public key from private key */
191 auto public_key = EVP_PKEY_get1_RSA(private_key);
193 throw MiscError("Could not obtain public key");
195 dcp::ScopeGuard sg_public_key([public_key]() { RSA_free(public_key); });
197 return public_key_digest(public_key);
201 CertificateChain::CertificateChain (
202 boost::filesystem::path openssl,
203 int validity_in_days,
205 string organisational_unit,
206 string root_common_name,
207 string intermediate_common_name,
208 string leaf_common_name
211 auto directory = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path ();
212 filesystem::create_directories(directory);
214 auto const cwd = boost::filesystem::current_path();
215 /* On Windows we will use cmd.exe here, and that doesn't work with UNC paths, so make sure
216 * we don't use our own filesystem::current_path() as it will make the current working
217 * directory a UNC path.
219 boost::filesystem::current_path(directory);
221 string quoted_openssl = "\"" + openssl.string() + "\"";
223 command (quoted_openssl + " genrsa -out ca.key 2048");
226 ofstream f ("ca.cnf");
228 << "distinguished_name = req_distinguished_name\n"
229 << "x509_extensions = v3_ca\n"
230 << "string_mask = nombstr\n"
232 << "basicConstraints = critical,CA:true,pathlen:3\n"
233 << "keyUsage = keyCertSign,cRLSign\n"
234 << "subjectKeyIdentifier = hash\n"
235 << "authorityKeyIdentifier = keyid:always,issuer:always\n"
236 << "[ req_distinguished_name ]\n"
237 << "O = Unique organization name\n"
238 << "OU = Organization unit\n"
239 << "CN = Entity and dnQualifier\n";
242 string const ca_subject = "/O=" + organisation +
243 "/OU=" + organisational_unit +
244 "/CN=" + root_common_name +
245 "/dnQualifier=" + public_key_digest ("ca.key");
250 "%1 req -new -x509 -sha256 -config ca.cnf -days %2 -set_serial 5"
251 " -subj \"%3\" -key ca.key -outform PEM -out ca.self-signed.pem",
252 quoted_openssl, validity_in_days, ca_subject
257 command (quoted_openssl + " genrsa -out intermediate.key 2048");
260 ofstream f ("intermediate.cnf");
262 << "distinguished_name = req_distinguished_name\n"
263 << "x509_extensions = v3_ca\n"
264 << "string_mask = nombstr\n"
266 << "basicConstraints = critical,CA:true,pathlen:2\n"
267 << "keyUsage = keyCertSign,cRLSign\n"
268 << "subjectKeyIdentifier = hash\n"
269 << "authorityKeyIdentifier = keyid:always,issuer:always\n"
270 << "[ req_distinguished_name ]\n"
271 << "O = Unique organization name\n"
272 << "OU = Organization unit\n"
273 << "CN = Entity and dnQualifier\n";
276 string const inter_subject = "/O=" + organisation +
277 "/OU=" + organisational_unit +
278 "/CN=" + intermediate_common_name +
279 "/dnQualifier=" + public_key_digest ("intermediate.key");
284 "%1 req -new -config intermediate.cnf -days %2 -subj \"%3\" -key intermediate.key -out intermediate.csr",
285 quoted_openssl, validity_in_days - 1, inter_subject
292 "%1 x509 -req -sha256 -days %2 -CA ca.self-signed.pem -CAkey ca.key -set_serial 6"
293 " -in intermediate.csr -extfile intermediate.cnf -extensions v3_ca -out intermediate.signed.pem",
294 quoted_openssl, validity_in_days - 1
298 command (quoted_openssl + " genrsa -out leaf.key 2048");
301 ofstream f ("leaf.cnf");
303 << "distinguished_name = req_distinguished_name\n"
304 << "x509_extensions = v3_ca\n"
305 << "string_mask = nombstr\n"
307 << "basicConstraints = critical,CA:false\n"
308 << "keyUsage = digitalSignature,keyEncipherment\n"
309 << "subjectKeyIdentifier = hash\n"
310 << "authorityKeyIdentifier = keyid,issuer:always\n"
311 << "[ req_distinguished_name ]\n"
312 << "O = Unique organization name\n"
313 << "OU = Organization unit\n"
314 << "CN = Entity and dnQualifier\n";
317 string const leaf_subject = "/O=" + organisation +
318 "/OU=" + organisational_unit +
319 "/CN=" + leaf_common_name +
320 "/dnQualifier=" + public_key_digest ("leaf.key");
325 "%1 req -new -config leaf.cnf -days %2 -subj \"%3\" -key leaf.key -outform PEM -out leaf.csr",
326 quoted_openssl, validity_in_days - 2, leaf_subject
333 "%1 x509 -req -sha256 -days %2 -CA intermediate.signed.pem -CAkey intermediate.key"
334 " -set_serial 7 -in leaf.csr -extfile leaf.cnf -extensions v3_ca -out leaf.signed.pem",
335 quoted_openssl, validity_in_days - 2
339 /* Use boost:: rather than dcp:: here so we don't force UNC into the current path if it
340 * wasn't there before.
342 boost::filesystem::current_path(cwd);
344 _certificates.push_back (dcp::Certificate(dcp::file_to_string(directory / "ca.self-signed.pem")));
345 _certificates.push_back (dcp::Certificate(dcp::file_to_string(directory / "intermediate.signed.pem")));
346 _certificates.push_back (dcp::Certificate(dcp::file_to_string(directory / "leaf.signed.pem")));
348 _key = dcp::file_to_string (directory / "leaf.key");
350 filesystem::remove_all(directory);
354 CertificateChain::CertificateChain (string s)
359 s = c.read_string (s);
360 _certificates.push_back (c);
361 } catch (MiscError& e) {
362 /* Failed to read a certificate, just stop */
367 /* This will throw an exception if the chain cannot be ordered */
373 CertificateChain::root () const
375 DCP_ASSERT (!_certificates.empty());
376 return root_to_leaf().front();
381 CertificateChain::leaf () const
383 DCP_ASSERT (!_certificates.empty());
384 return root_to_leaf().back();
388 CertificateChain::List
389 CertificateChain::leaf_to_root () const
391 auto l = root_to_leaf ();
392 std::reverse (l.begin(), l.end());
397 CertificateChain::List
398 CertificateChain::unordered () const
400 return _certificates;
405 CertificateChain::add (Certificate c)
407 _certificates.push_back (c);
412 CertificateChain::remove (Certificate c)
414 auto i = std::find(_certificates.begin(), _certificates.end(), c);
415 if (i != _certificates.end()) {
416 _certificates.erase (i);
422 CertificateChain::remove (int i)
424 auto j = _certificates.begin ();
425 while (j != _certificates.end () && i > 0) {
430 if (j != _certificates.end ()) {
431 _certificates.erase (j);
437 CertificateChain::chain_valid () const
439 return chain_valid (_certificates);
443 /** @param error if non-null, filled with an error if a certificate in the list has a
445 * @return true if all the given certificates verify OK, and are in the correct order in the list
446 * (root to leaf). false if any certificate has a problem, or the order is wrong.
449 CertificateChain::chain_valid(List const & chain, string* error) const
451 /* Here I am taking a chain of certificates A/B/C/D and checking validity of B wrt A,
452 C wrt B and D wrt C. It also appears necessary to check the issuer of B/C/D matches
453 the subject of A/B/C; I don't understand why. I'm sure there's a better way of doing
454 this with OpenSSL but the documentation does not appear not likely to reveal it
458 auto store = X509_STORE_new ();
460 throw MiscError ("could not create X509 store");
463 /* Put all the certificates into the store */
464 for (auto const& i: chain) {
465 if (!X509_STORE_add_cert(store, i.x509())) {
466 X509_STORE_free(store);
471 /* Verify each one */
472 for (auto i = chain.begin(); i != chain.end(); ++i) {
476 if (j == chain.end ()) {
480 auto ctx = X509_STORE_CTX_new ();
482 X509_STORE_free (store);
483 throw MiscError ("could not create X509 store context");
486 X509_STORE_set_flags (store, 0);
487 if (!X509_STORE_CTX_init (ctx, store, j->x509(), 0)) {
488 X509_STORE_CTX_free (ctx);
489 X509_STORE_free (store);
490 throw MiscError ("could not initialise X509 store context");
493 int const v = X509_verify_cert (ctx);
496 X509_STORE_free (store);
498 *error = X509_verify_cert_error_string(X509_STORE_CTX_get_error(ctx));
500 X509_STORE_CTX_free(ctx);
504 X509_STORE_CTX_free(ctx);
506 /* I don't know why OpenSSL doesn't check this stuff
507 in verify_cert, but without these checks the
508 certificates_validation8 test fails.
510 if (j->issuer() != i->subject() || j->subject() == i->subject()) {
511 X509_STORE_free (store);
517 X509_STORE_free (store);
524 CertificateChain::private_key_valid () const
526 if (_certificates.empty ()) {
534 auto bio = BIO_new_mem_buf (const_cast<char *> (_key->c_str ()), -1);
536 throw MiscError ("could not create memory BIO");
539 auto private_key = PEM_read_bio_RSAPrivateKey (bio, 0, 0, 0);
544 auto public_key = leaf().public_key ();
546 #if OPENSSL_VERSION_NUMBER > 0x10100000L
547 BIGNUM const * private_key_n;
548 RSA_get0_key(private_key, &private_key_n, 0, 0);
549 BIGNUM const * public_key_n;
550 RSA_get0_key(public_key, &public_key_n, 0, 0);
551 if (!private_key_n || !public_key_n) {
554 bool const valid = !BN_cmp (private_key_n, public_key_n);
556 bool const valid = !BN_cmp (private_key->n, public_key->n);
565 CertificateChain::valid (string* reason) const
569 } catch (CertificateChainError& e) {
571 *reason = "certificates do not form a chain";
576 if (!private_key_valid ()) {
578 *reason = "private key does not exist, or does not match leaf certificate";
587 CertificateChain::List
588 CertificateChain::root_to_leaf () const
590 auto rtl = _certificates;
591 std::sort (rtl.begin(), rtl.end());
594 if (chain_valid(rtl, &error)) {
597 } while (std::next_permutation (rtl.begin(), rtl.end()));
599 throw CertificateChainError(error.empty() ? string{"certificate chain is not consistent"} : error);
604 CertificateChain::sign (xmlpp::Element* parent, Standard standard) const
608 parent->add_child_text(" ");
609 auto signer = parent->add_child("Signer");
610 signer->set_namespace_declaration ("http://www.w3.org/2000/09/xmldsig#", "dsig");
611 auto data = signer->add_child("X509Data", "dsig");
612 auto serial_element = data->add_child("X509IssuerSerial", "dsig");
613 serial_element->add_child("X509IssuerName", "dsig")->add_child_text (leaf().issuer());
614 serial_element->add_child("X509SerialNumber", "dsig")->add_child_text (leaf().serial());
615 data->add_child("X509SubjectName", "dsig")->add_child_text (leaf().subject());
621 parent->add_child_text("\n ");
622 auto signature = parent->add_child("Signature");
623 signature->set_namespace_declaration ("http://www.w3.org/2000/09/xmldsig#", "dsig");
624 signature->set_namespace ("dsig");
625 parent->add_child_text("\n");
627 auto signed_info = signature->add_child ("SignedInfo", "dsig");
628 signed_info->add_child("CanonicalizationMethod", "dsig")->set_attribute ("Algorithm", "http://www.w3.org/TR/2001/REC-xml-c14n-20010315");
630 if (standard == Standard::INTEROP) {
631 signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#rsa-sha1");
633 signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
636 auto reference = signed_info->add_child("Reference", "dsig");
637 reference->set_attribute ("URI", "");
639 auto transforms = reference->add_child("Transforms", "dsig");
640 transforms->add_child("Transform", "dsig")->set_attribute (
641 "Algorithm", "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
644 reference->add_child("DigestMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#sha1");
645 /* This will be filled in by the signing later */
646 reference->add_child("DigestValue", "dsig");
648 signature->add_child("SignatureValue", "dsig");
649 signature->add_child("KeyInfo", "dsig");
650 add_signature_value (signature, "dsig", true);
655 CertificateChain::add_signature_value (xmlpp::Element* parent, string ns, bool add_indentation) const
657 cxml::Node cp (parent);
658 auto key_info = cp.node_child("KeyInfo")->node();
660 /* Add the certificate chain to the KeyInfo child node of parent */
661 for (auto const& i: leaf_to_root()) {
662 auto data = key_info->add_child("X509Data", ns);
665 auto serial = data->add_child("X509IssuerSerial", ns);
666 serial->add_child("X509IssuerName", ns)->add_child_text (i.issuer ());
667 serial->add_child("X509SerialNumber", ns)->add_child_text (i.serial ());
670 data->add_child("X509Certificate", ns)->add_child_text (i.certificate());
673 auto signature_context = xmlSecDSigCtxCreate (0);
674 if (signature_context == 0) {
675 throw MiscError ("could not create signature context");
678 signature_context->signKey = xmlSecCryptoAppKeyLoadMemory (
679 reinterpret_cast<const unsigned char *> (_key->c_str()), _key->size(), xmlSecKeyDataFormatPem, 0, 0, 0
682 if (signature_context->signKey == 0) {
683 throw runtime_error ("could not read private key");
686 if (add_indentation) {
689 int const r = xmlSecDSigCtxSign (signature_context, parent->cobj ());
691 throw MiscError (String::compose ("could not sign (%1)", r));
694 xmlSecDSigCtxDestroy (signature_context);
699 CertificateChain::chain () const
702 for (auto const& i: root_to_leaf()) {
703 o += i.certificate(true);