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"
47 #include <asdcp/KM_util.h>
48 #include <libcxml/cxml.h>
49 LIBDCP_DISABLE_WARNINGS
50 #include <libxml++/libxml++.h>
51 LIBDCP_ENABLE_WARNINGS
52 #include <xmlsec/xmldsig.h>
53 #include <xmlsec/dl.h>
54 #include <xmlsec/app.h>
55 #include <xmlsec/crypto.h>
56 #include <openssl/sha.h>
57 #include <openssl/bio.h>
58 #include <openssl/evp.h>
59 #include <openssl/pem.h>
60 #include <openssl/rsa.h>
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, 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 dcp::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 filesystem::create_directories(directory);
201 auto const cwd = boost::filesystem::current_path();
202 /* On Windows we will use cmd.exe here, and that doesn't work with UNC paths, so make sure
203 * we don't use our own filesystem::current_path() as it will make the current working
204 * directory a UNC path.
206 boost::filesystem::current_path(directory);
208 string quoted_openssl = "\"" + openssl.string() + "\"";
210 command (quoted_openssl + " genrsa -out ca.key 2048");
213 ofstream f ("ca.cnf");
215 << "distinguished_name = req_distinguished_name\n"
216 << "x509_extensions = v3_ca\n"
217 << "string_mask = nombstr\n"
219 << "basicConstraints = critical,CA:true,pathlen:3\n"
220 << "keyUsage = keyCertSign,cRLSign\n"
221 << "subjectKeyIdentifier = hash\n"
222 << "authorityKeyIdentifier = keyid:always,issuer:always\n"
223 << "[ req_distinguished_name ]\n"
224 << "O = Unique organization name\n"
225 << "OU = Organization unit\n"
226 << "CN = Entity and dnQualifier\n";
229 string const ca_subject = "/O=" + organisation +
230 "/OU=" + organisational_unit +
231 "/CN=" + root_common_name +
232 "/dnQualifier=" + public_key_digest ("ca.key", openssl);
237 "%1 req -new -x509 -sha256 -config ca.cnf -days %2 -set_serial 5"
238 " -subj \"%3\" -key ca.key -outform PEM -out ca.self-signed.pem",
239 quoted_openssl, validity_in_days, ca_subject
244 command (quoted_openssl + " genrsa -out intermediate.key 2048");
247 ofstream f ("intermediate.cnf");
249 << "distinguished_name = req_distinguished_name\n"
250 << "x509_extensions = v3_ca\n"
251 << "string_mask = nombstr\n"
253 << "basicConstraints = critical,CA:true,pathlen:2\n"
254 << "keyUsage = keyCertSign,cRLSign\n"
255 << "subjectKeyIdentifier = hash\n"
256 << "authorityKeyIdentifier = keyid:always,issuer:always\n"
257 << "[ req_distinguished_name ]\n"
258 << "O = Unique organization name\n"
259 << "OU = Organization unit\n"
260 << "CN = Entity and dnQualifier\n";
263 string const inter_subject = "/O=" + organisation +
264 "/OU=" + organisational_unit +
265 "/CN=" + intermediate_common_name +
266 "/dnQualifier=" + public_key_digest ("intermediate.key", openssl);
271 "%1 req -new -config intermediate.cnf -days %2 -subj \"%3\" -key intermediate.key -out intermediate.csr",
272 quoted_openssl, validity_in_days - 1, inter_subject
279 "%1 x509 -req -sha256 -days %2 -CA ca.self-signed.pem -CAkey ca.key -set_serial 6"
280 " -in intermediate.csr -extfile intermediate.cnf -extensions v3_ca -out intermediate.signed.pem",
281 quoted_openssl, validity_in_days - 1
285 command (quoted_openssl + " genrsa -out leaf.key 2048");
288 ofstream f ("leaf.cnf");
290 << "distinguished_name = req_distinguished_name\n"
291 << "x509_extensions = v3_ca\n"
292 << "string_mask = nombstr\n"
294 << "basicConstraints = critical,CA:false\n"
295 << "keyUsage = digitalSignature,keyEncipherment\n"
296 << "subjectKeyIdentifier = hash\n"
297 << "authorityKeyIdentifier = keyid,issuer:always\n"
298 << "[ req_distinguished_name ]\n"
299 << "O = Unique organization name\n"
300 << "OU = Organization unit\n"
301 << "CN = Entity and dnQualifier\n";
304 string const leaf_subject = "/O=" + organisation +
305 "/OU=" + organisational_unit +
306 "/CN=" + leaf_common_name +
307 "/dnQualifier=" + public_key_digest ("leaf.key", openssl);
312 "%1 req -new -config leaf.cnf -days %2 -subj \"%3\" -key leaf.key -outform PEM -out leaf.csr",
313 quoted_openssl, validity_in_days - 2, leaf_subject
320 "%1 x509 -req -sha256 -days %2 -CA intermediate.signed.pem -CAkey intermediate.key"
321 " -set_serial 7 -in leaf.csr -extfile leaf.cnf -extensions v3_ca -out leaf.signed.pem",
322 quoted_openssl, validity_in_days - 2
326 /* Use boost:: rather than dcp:: here so we don't force UNC into the current path if it
327 * wasn't there before.
329 boost::filesystem::current_path(cwd);
331 _certificates.push_back (dcp::Certificate(dcp::file_to_string(directory / "ca.self-signed.pem")));
332 _certificates.push_back (dcp::Certificate(dcp::file_to_string(directory / "intermediate.signed.pem")));
333 _certificates.push_back (dcp::Certificate(dcp::file_to_string(directory / "leaf.signed.pem")));
335 _key = dcp::file_to_string (directory / "leaf.key");
337 filesystem::remove_all(directory);
341 CertificateChain::CertificateChain (string s)
346 s = c.read_string (s);
347 _certificates.push_back (c);
348 } catch (MiscError& e) {
349 /* Failed to read a certificate, just stop */
354 /* This will throw an exception if the chain cannot be ordered */
360 CertificateChain::root () const
362 DCP_ASSERT (!_certificates.empty());
363 return root_to_leaf().front();
368 CertificateChain::leaf () const
370 DCP_ASSERT (!_certificates.empty());
371 return root_to_leaf().back();
375 CertificateChain::List
376 CertificateChain::leaf_to_root () const
378 auto l = root_to_leaf ();
379 std::reverse (l.begin(), l.end());
384 CertificateChain::List
385 CertificateChain::unordered () const
387 return _certificates;
392 CertificateChain::add (Certificate c)
394 _certificates.push_back (c);
399 CertificateChain::remove (Certificate c)
401 auto i = std::find(_certificates.begin(), _certificates.end(), c);
402 if (i != _certificates.end()) {
403 _certificates.erase (i);
409 CertificateChain::remove (int i)
411 auto j = _certificates.begin ();
412 while (j != _certificates.end () && i > 0) {
417 if (j != _certificates.end ()) {
418 _certificates.erase (j);
424 CertificateChain::chain_valid () const
426 return chain_valid (_certificates);
430 /** @param error if non-null, filled with an error if a certificate in the list has a
432 * @return true if all the given certificates verify OK, and are in the correct order in the list
433 * (root to leaf). false if any certificate has a problem, or the order is wrong.
436 CertificateChain::chain_valid(List const & chain, string* error) const
438 /* Here I am taking a chain of certificates A/B/C/D and checking validity of B wrt A,
439 C wrt B and D wrt C. It also appears necessary to check the issuer of B/C/D matches
440 the subject of A/B/C; I don't understand why. I'm sure there's a better way of doing
441 this with OpenSSL but the documentation does not appear not likely to reveal it
445 auto store = X509_STORE_new ();
447 throw MiscError ("could not create X509 store");
450 /* Put all the certificates into the store */
451 for (auto const& i: chain) {
452 if (!X509_STORE_add_cert(store, i.x509())) {
453 X509_STORE_free(store);
458 /* Verify each one */
459 for (auto i = chain.begin(); i != chain.end(); ++i) {
463 if (j == chain.end ()) {
467 auto ctx = X509_STORE_CTX_new ();
469 X509_STORE_free (store);
470 throw MiscError ("could not create X509 store context");
473 X509_STORE_set_flags (store, 0);
474 if (!X509_STORE_CTX_init (ctx, store, j->x509(), 0)) {
475 X509_STORE_CTX_free (ctx);
476 X509_STORE_free (store);
477 throw MiscError ("could not initialise X509 store context");
480 int const v = X509_verify_cert (ctx);
483 X509_STORE_free (store);
485 *error = X509_verify_cert_error_string(X509_STORE_CTX_get_error(ctx));
487 X509_STORE_CTX_free(ctx);
491 X509_STORE_CTX_free(ctx);
493 /* I don't know why OpenSSL doesn't check this stuff
494 in verify_cert, but without these checks the
495 certificates_validation8 test fails.
497 if (j->issuer() != i->subject() || j->subject() == i->subject()) {
498 X509_STORE_free (store);
504 X509_STORE_free (store);
511 CertificateChain::private_key_valid () const
513 if (_certificates.empty ()) {
521 auto bio = BIO_new_mem_buf (const_cast<char *> (_key->c_str ()), -1);
523 throw MiscError ("could not create memory BIO");
526 auto private_key = PEM_read_bio_RSAPrivateKey (bio, 0, 0, 0);
531 auto public_key = leaf().public_key ();
533 #if OPENSSL_VERSION_NUMBER > 0x10100000L
534 BIGNUM const * private_key_n;
535 RSA_get0_key(private_key, &private_key_n, 0, 0);
536 BIGNUM const * public_key_n;
537 RSA_get0_key(public_key, &public_key_n, 0, 0);
538 if (!private_key_n || !public_key_n) {
541 bool const valid = !BN_cmp (private_key_n, public_key_n);
543 bool const valid = !BN_cmp (private_key->n, public_key->n);
552 CertificateChain::valid (string* reason) const
556 } catch (CertificateChainError& e) {
558 *reason = "certificates do not form a chain";
563 if (!private_key_valid ()) {
565 *reason = "private key does not exist, or does not match leaf certificate";
574 CertificateChain::List
575 CertificateChain::root_to_leaf () const
577 auto rtl = _certificates;
578 std::sort (rtl.begin(), rtl.end());
581 if (chain_valid(rtl, &error)) {
584 } while (std::next_permutation (rtl.begin(), rtl.end()));
586 throw CertificateChainError(error.empty() ? string{"certificate chain is not consistent"} : error);
591 CertificateChain::sign (xmlpp::Element* parent, Standard standard) const
595 parent->add_child_text(" ");
596 auto signer = parent->add_child("Signer");
597 signer->set_namespace_declaration ("http://www.w3.org/2000/09/xmldsig#", "dsig");
598 auto data = signer->add_child("X509Data", "dsig");
599 auto serial_element = data->add_child("X509IssuerSerial", "dsig");
600 serial_element->add_child("X509IssuerName", "dsig")->add_child_text (leaf().issuer());
601 serial_element->add_child("X509SerialNumber", "dsig")->add_child_text (leaf().serial());
602 data->add_child("X509SubjectName", "dsig")->add_child_text (leaf().subject());
608 parent->add_child_text("\n ");
609 auto signature = parent->add_child("Signature");
610 signature->set_namespace_declaration ("http://www.w3.org/2000/09/xmldsig#", "dsig");
611 signature->set_namespace ("dsig");
612 parent->add_child_text("\n");
614 auto signed_info = signature->add_child ("SignedInfo", "dsig");
615 signed_info->add_child("CanonicalizationMethod", "dsig")->set_attribute ("Algorithm", "http://www.w3.org/TR/2001/REC-xml-c14n-20010315");
617 if (standard == Standard::INTEROP) {
618 signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#rsa-sha1");
620 signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
623 auto reference = signed_info->add_child("Reference", "dsig");
624 reference->set_attribute ("URI", "");
626 auto transforms = reference->add_child("Transforms", "dsig");
627 transforms->add_child("Transform", "dsig")->set_attribute (
628 "Algorithm", "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
631 reference->add_child("DigestMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#sha1");
632 /* This will be filled in by the signing later */
633 reference->add_child("DigestValue", "dsig");
635 signature->add_child("SignatureValue", "dsig");
636 signature->add_child("KeyInfo", "dsig");
637 add_signature_value (signature, "dsig", true);
642 CertificateChain::add_signature_value (xmlpp::Element* parent, string ns, bool add_indentation) const
644 cxml::Node cp (parent);
645 auto key_info = cp.node_child("KeyInfo")->node();
647 /* Add the certificate chain to the KeyInfo child node of parent */
648 for (auto const& i: leaf_to_root()) {
649 auto data = key_info->add_child("X509Data", ns);
652 auto serial = data->add_child("X509IssuerSerial", ns);
653 serial->add_child("X509IssuerName", ns)->add_child_text (i.issuer ());
654 serial->add_child("X509SerialNumber", ns)->add_child_text (i.serial ());
657 data->add_child("X509Certificate", ns)->add_child_text (i.certificate());
660 auto signature_context = xmlSecDSigCtxCreate (0);
661 if (signature_context == 0) {
662 throw MiscError ("could not create signature context");
665 signature_context->signKey = xmlSecCryptoAppKeyLoadMemory (
666 reinterpret_cast<const unsigned char *> (_key->c_str()), _key->size(), xmlSecKeyDataFormatPem, 0, 0, 0
669 if (signature_context->signKey == 0) {
670 throw runtime_error ("could not read private key");
673 if (add_indentation) {
676 int const r = xmlSecDSigCtxSign (signature_context, parent->cobj ());
678 throw MiscError (String::compose ("could not sign (%1)", r));
681 xmlSecDSigCtxDestroy (signature_context);
686 CertificateChain::chain () const
689 for (auto const& i: root_to_leaf()) {
690 o += i.certificate(true);