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,
191 string organisational_unit,
192 string root_common_name,
193 string intermediate_common_name,
194 string leaf_common_name
197 /* Valid for 40 years */
198 int const days = 365 * 40;
200 auto directory = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path ();
201 boost::filesystem::create_directories (directory);
203 auto const cwd = boost::filesystem::current_path ();
204 boost::filesystem::current_path (directory);
206 string quoted_openssl = "\"" + openssl.string() + "\"";
208 command (quoted_openssl + " genrsa -out ca.key 2048");
211 ofstream f ("ca.cnf");
213 << "distinguished_name = req_distinguished_name\n"
214 << "x509_extensions = v3_ca\n"
215 << "string_mask = nombstr\n"
217 << "basicConstraints = critical,CA:true,pathlen:3\n"
218 << "keyUsage = keyCertSign,cRLSign\n"
219 << "subjectKeyIdentifier = hash\n"
220 << "authorityKeyIdentifier = keyid:always,issuer:always\n"
221 << "[ req_distinguished_name ]\n"
222 << "O = Unique organization name\n"
223 << "OU = Organization unit\n"
224 << "CN = Entity and dnQualifier\n";
227 string const ca_subject = "/O=" + organisation +
228 "/OU=" + organisational_unit +
229 "/CN=" + root_common_name +
230 "/dnQualifier=" + public_key_digest ("ca.key", openssl);
235 "%1 req -new -x509 -sha256 -config ca.cnf -days %2 -set_serial 5"
236 " -subj \"%3\" -key ca.key -outform PEM -out ca.self-signed.pem",
237 quoted_openssl, days, ca_subject
242 command (quoted_openssl + " genrsa -out intermediate.key 2048");
245 ofstream f ("intermediate.cnf");
247 << "distinguished_name = req_distinguished_name\n"
248 << "x509_extensions = v3_ca\n"
249 << "string_mask = nombstr\n"
251 << "basicConstraints = critical,CA:true,pathlen:2\n"
252 << "keyUsage = keyCertSign,cRLSign\n"
253 << "subjectKeyIdentifier = hash\n"
254 << "authorityKeyIdentifier = keyid:always,issuer:always\n"
255 << "[ req_distinguished_name ]\n"
256 << "O = Unique organization name\n"
257 << "OU = Organization unit\n"
258 << "CN = Entity and dnQualifier\n";
261 string const inter_subject = "/O=" + organisation +
262 "/OU=" + organisational_unit +
263 "/CN=" + intermediate_common_name +
264 "/dnQualifier=" + public_key_digest ("intermediate.key", openssl);
269 "%1 req -new -config intermediate.cnf -days %2 -subj \"%3\" -key intermediate.key -out intermediate.csr",
270 quoted_openssl, days - 1, inter_subject
277 "%1 x509 -req -sha256 -days %2 -CA ca.self-signed.pem -CAkey ca.key -set_serial 6"
278 " -in intermediate.csr -extfile intermediate.cnf -extensions v3_ca -out intermediate.signed.pem",
279 quoted_openssl, days - 1
283 command (quoted_openssl + " genrsa -out leaf.key 2048");
286 ofstream f ("leaf.cnf");
288 << "distinguished_name = req_distinguished_name\n"
289 << "x509_extensions = v3_ca\n"
290 << "string_mask = nombstr\n"
292 << "basicConstraints = critical,CA:false\n"
293 << "keyUsage = digitalSignature,keyEncipherment\n"
294 << "subjectKeyIdentifier = hash\n"
295 << "authorityKeyIdentifier = keyid,issuer:always\n"
296 << "[ req_distinguished_name ]\n"
297 << "O = Unique organization name\n"
298 << "OU = Organization unit\n"
299 << "CN = Entity and dnQualifier\n";
302 string const leaf_subject = "/O=" + organisation +
303 "/OU=" + organisational_unit +
304 "/CN=" + leaf_common_name +
305 "/dnQualifier=" + public_key_digest ("leaf.key", openssl);
310 "%1 req -new -config leaf.cnf -days %2 -subj \"%3\" -key leaf.key -outform PEM -out leaf.csr",
311 quoted_openssl, days - 2, leaf_subject
318 "%1 x509 -req -sha256 -days %2 -CA intermediate.signed.pem -CAkey intermediate.key"
319 " -set_serial 7 -in leaf.csr -extfile leaf.cnf -extensions v3_ca -out leaf.signed.pem",
320 quoted_openssl, days - 2
324 boost::filesystem::current_path (cwd);
326 _certificates.push_back (dcp::Certificate(dcp::file_to_string(directory / "ca.self-signed.pem")));
327 _certificates.push_back (dcp::Certificate(dcp::file_to_string(directory / "intermediate.signed.pem")));
328 _certificates.push_back (dcp::Certificate(dcp::file_to_string(directory / "leaf.signed.pem")));
330 _key = dcp::file_to_string (directory / "leaf.key");
332 boost::filesystem::remove_all (directory);
336 CertificateChain::CertificateChain (string s)
341 s = c.read_string (s);
342 _certificates.push_back (c);
343 } catch (MiscError& e) {
344 /* Failed to read a certificate, just stop */
349 /* This will throw an exception if the chain cannot be ordered */
355 CertificateChain::root () const
357 DCP_ASSERT (!_certificates.empty());
358 return root_to_leaf().front();
363 CertificateChain::leaf () const
365 DCP_ASSERT (!_certificates.empty());
366 return root_to_leaf().back();
370 CertificateChain::List
371 CertificateChain::leaf_to_root () const
373 auto l = root_to_leaf ();
374 std::reverse (l.begin(), l.end());
379 CertificateChain::List
380 CertificateChain::unordered () const
382 return _certificates;
387 CertificateChain::add (Certificate c)
389 _certificates.push_back (c);
394 CertificateChain::remove (Certificate c)
396 auto i = std::find(_certificates.begin(), _certificates.end(), c);
397 if (i != _certificates.end()) {
398 _certificates.erase (i);
404 CertificateChain::remove (int i)
406 auto j = _certificates.begin ();
407 while (j != _certificates.end () && i > 0) {
412 if (j != _certificates.end ()) {
413 _certificates.erase (j);
419 CertificateChain::chain_valid () const
421 return chain_valid (_certificates);
426 CertificateChain::chain_valid (List const & chain) const
428 /* Here I am taking a chain of certificates A/B/C/D and checking validity of B wrt A,
429 C wrt B and D wrt C. It also appears necessary to check the issuer of B/C/D matches
430 the subject of A/B/C; I don't understand why. I'm sure there's a better way of doing
431 this with OpenSSL but the documentation does not appear not likely to reveal it
435 auto store = X509_STORE_new ();
437 throw MiscError ("could not create X509 store");
440 /* Put all the certificates into the store */
441 for (auto const& i: chain) {
442 if (!X509_STORE_add_cert(store, i.x509())) {
443 X509_STORE_free(store);
448 /* Verify each one */
449 for (auto i = chain.begin(); i != chain.end(); ++i) {
453 if (j == chain.end ()) {
457 auto ctx = X509_STORE_CTX_new ();
459 X509_STORE_free (store);
460 throw MiscError ("could not create X509 store context");
463 X509_STORE_set_flags (store, 0);
464 if (!X509_STORE_CTX_init (ctx, store, j->x509(), 0)) {
465 X509_STORE_CTX_free (ctx);
466 X509_STORE_free (store);
467 throw MiscError ("could not initialise X509 store context");
470 int const v = X509_verify_cert (ctx);
471 X509_STORE_CTX_free (ctx);
474 X509_STORE_free (store);
478 /* I don't know why OpenSSL doesn't check this stuff
479 in verify_cert, but without these checks the
480 certificates_validation8 test fails.
482 if (j->issuer() != i->subject() || j->subject() == i->subject()) {
483 X509_STORE_free (store);
489 X509_STORE_free (store);
496 CertificateChain::private_key_valid () const
498 if (_certificates.empty ()) {
506 auto bio = BIO_new_mem_buf (const_cast<char *> (_key->c_str ()), -1);
508 throw MiscError ("could not create memory BIO");
511 auto private_key = PEM_read_bio_RSAPrivateKey (bio, 0, 0, 0);
516 auto public_key = leaf().public_key ();
518 #if OPENSSL_VERSION_NUMBER > 0x10100000L
519 BIGNUM const * private_key_n;
520 RSA_get0_key(private_key, &private_key_n, 0, 0);
521 BIGNUM const * public_key_n;
522 RSA_get0_key(public_key, &public_key_n, 0, 0);
523 if (!private_key_n || !public_key_n) {
526 bool const valid = !BN_cmp (private_key_n, public_key_n);
528 bool const valid = !BN_cmp (private_key->n, public_key->n);
537 CertificateChain::valid (string* reason) const
541 } catch (CertificateChainError& e) {
543 *reason = "certificates do not form a chain";
548 if (!private_key_valid ()) {
550 *reason = "private key does not exist, or does not match leaf certificate";
559 CertificateChain::List
560 CertificateChain::root_to_leaf () const
562 auto rtl = _certificates;
563 std::sort (rtl.begin(), rtl.end());
565 if (chain_valid (rtl)) {
568 } while (std::next_permutation (rtl.begin(), rtl.end()));
570 throw CertificateChainError ("certificate chain is not consistent");
575 CertificateChain::sign (xmlpp::Element* parent, Standard standard) const
579 parent->add_child_text(" ");
580 auto signer = parent->add_child("Signer");
581 signer->set_namespace_declaration ("http://www.w3.org/2000/09/xmldsig#", "dsig");
582 auto data = signer->add_child("X509Data", "dsig");
583 auto serial_element = data->add_child("X509IssuerSerial", "dsig");
584 serial_element->add_child("X509IssuerName", "dsig")->add_child_text (leaf().issuer());
585 serial_element->add_child("X509SerialNumber", "dsig")->add_child_text (leaf().serial());
586 data->add_child("X509SubjectName", "dsig")->add_child_text (leaf().subject());
592 parent->add_child_text("\n ");
593 auto signature = parent->add_child("Signature");
594 signature->set_namespace_declaration ("http://www.w3.org/2000/09/xmldsig#", "dsig");
595 signature->set_namespace ("dsig");
596 parent->add_child_text("\n");
598 auto signed_info = signature->add_child ("SignedInfo", "dsig");
599 signed_info->add_child("CanonicalizationMethod", "dsig")->set_attribute ("Algorithm", "http://www.w3.org/TR/2001/REC-xml-c14n-20010315");
601 if (standard == Standard::INTEROP) {
602 signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#rsa-sha1");
604 signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
607 auto reference = signed_info->add_child("Reference", "dsig");
608 reference->set_attribute ("URI", "");
610 auto transforms = reference->add_child("Transforms", "dsig");
611 transforms->add_child("Transform", "dsig")->set_attribute (
612 "Algorithm", "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
615 reference->add_child("DigestMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#sha1");
616 /* This will be filled in by the signing later */
617 reference->add_child("DigestValue", "dsig");
619 signature->add_child("SignatureValue", "dsig");
620 signature->add_child("KeyInfo", "dsig");
621 add_signature_value (signature, "dsig", true);
626 CertificateChain::add_signature_value (xmlpp::Element* parent, string ns, bool add_indentation) const
628 cxml::Node cp (parent);
629 auto key_info = cp.node_child("KeyInfo")->node();
631 /* Add the certificate chain to the KeyInfo child node of parent */
632 for (auto const& i: leaf_to_root()) {
633 auto data = key_info->add_child("X509Data", ns);
636 auto serial = data->add_child("X509IssuerSerial", ns);
637 serial->add_child("X509IssuerName", ns)->add_child_text (i.issuer ());
638 serial->add_child("X509SerialNumber", ns)->add_child_text (i.serial ());
641 data->add_child("X509Certificate", ns)->add_child_text (i.certificate());
644 auto signature_context = xmlSecDSigCtxCreate (0);
645 if (signature_context == 0) {
646 throw MiscError ("could not create signature context");
649 signature_context->signKey = xmlSecCryptoAppKeyLoadMemory (
650 reinterpret_cast<const unsigned char *> (_key->c_str()), _key->size(), xmlSecKeyDataFormatPem, 0, 0, 0
653 if (signature_context->signKey == 0) {
654 throw runtime_error ("could not read private key");
657 if (add_indentation) {
660 int const r = xmlSecDSigCtxSign (signature_context, parent->cobj ());
662 throw MiscError (String::compose ("could not sign (%1)", r));
665 xmlSecDSigCtxDestroy (signature_context);
670 CertificateChain::chain () const
673 for (auto const& i: root_to_leaf()) {
674 o += i.certificate(true);