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 "exceptions.h"
43 #include "dcp_assert.h"
44 #include "compose.hpp"
45 #include <asdcp/KM_util.h>
46 #include <libcxml/cxml.h>
47 #include <libxml++/libxml++.h>
48 #include <xmlsec/xmldsig.h>
49 #include <xmlsec/dl.h>
50 #include <xmlsec/app.h>
51 #include <xmlsec/crypto.h>
52 #include <openssl/sha.h>
53 #include <openssl/bio.h>
54 #include <openssl/evp.h>
55 #include <openssl/pem.h>
56 #include <openssl/rsa.h>
57 #include <boost/filesystem.hpp>
58 #include <boost/algorithm/string.hpp>
66 using std::runtime_error;
70 /** Run a shell command.
71 * @param cmd Command to run (UTF8-encoded).
77 /* We need to use CreateProcessW on Windows so that the UTF-8/16 mess
80 int const wn = MultiByteToWideChar (CP_UTF8, 0, cmd.c_str(), -1, 0, 0);
81 char buffer = new wchar_t[wn];
82 if (MultiByteToWideChar (CP_UTF8, 0, cmd.c_str(), -1, buffer, wn) == 0) {
89 STARTUPINFOW startup_info;
90 memset (&startup_info, 0, sizeof (startup_info));
91 startup_info.cb = sizeof (startup_info);
92 PROCESS_INFORMATION process_info;
94 /* XXX: this doesn't actually seem to work; failing commands end up with
97 if (CreateProcessW (0, buffer, 0, 0, FALSE, CREATE_NO_WINDOW, 0, 0, &startup_info, &process_info)) {
98 WaitForSingleObject (process_info.hProcess, INFINITE);
100 if (GetExitCodeProcess (process_info.hProcess, &c)) {
103 CloseHandle (process_info.hProcess);
104 CloseHandle (process_info.hThread);
109 cmd += " 2> /dev/null";
110 int const r = system (cmd.c_str ());
111 int const code = WEXITSTATUS (r);
114 throw dcp::MiscError (String::compose ("error %1 in %2 within %3", code, cmd, boost::filesystem::current_path().string()));
119 /** Extract a public key from a private key and create a SHA1 digest of it.
120 * @param private_key Private key
121 * @param openssl openssl binary name (or full path if openssl is not on the system path).
122 * @return SHA1 digest of corresponding public key, with escaped / characters.
125 public_key_digest (boost::filesystem::path private_key, boost::filesystem::path openssl)
127 boost::filesystem::path public_name = private_key.string() + ".public";
129 /* Create the public key from the private key */
130 command (String::compose("\"%1\" rsa -outform PEM -pubout -in %2 -out %3", openssl.string(), private_key.string(), public_name.string()));
132 /* Read in the public key from the file */
135 ifstream f (public_name.string().c_str());
137 throw dcp::MiscError ("public key not found");
144 if (line.length() >= 10 && line.substr(0, 10) == "-----BEGIN") {
146 } else if (line.length() >= 8 && line.substr(0, 8) == "-----END") {
153 /* Decode the base64 of the public key */
155 unsigned char buffer[512];
156 int const N = dcp::base64_decode (pub, buffer, 1024);
158 /* Hash it with SHA1 (without the first 24 bytes, for reasons that are not entirely clear) */
161 if (!SHA1_Init (&context)) {
162 throw dcp::MiscError ("could not init SHA1 context");
165 if (!SHA1_Update (&context, buffer + 24, N - 24)) {
166 throw dcp::MiscError ("could not update SHA1 digest");
169 unsigned char digest[SHA_DIGEST_LENGTH];
170 if (!SHA1_Final (digest, &context)) {
171 throw dcp::MiscError ("could not finish SHA1 digest");
174 char digest_base64[64];
175 string dig = Kumu::base64encode (digest, SHA_DIGEST_LENGTH, digest_base64, 64);
176 #ifdef LIBDCP_WINDOWS
177 boost::replace_all (dig, "/", "\\/");
179 boost::replace_all (dig, "/", "\\\\/");
185 CertificateChain::CertificateChain (
186 boost::filesystem::path openssl,
188 string organisational_unit,
189 string root_common_name,
190 string intermediate_common_name,
191 string leaf_common_name
194 /* Valid for 40 years */
195 int const days = 365 * 40;
197 auto directory = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path ();
198 boost::filesystem::create_directories (directory);
200 auto const cwd = boost::filesystem::current_path ();
201 boost::filesystem::current_path (directory);
203 string quoted_openssl = "\"" + openssl.string() + "\"";
205 command (quoted_openssl + " genrsa -out ca.key 2048");
208 ofstream f ("ca.cnf");
210 << "distinguished_name = req_distinguished_name\n"
211 << "x509_extensions = v3_ca\n"
212 << "string_mask = nombstr\n"
214 << "basicConstraints = critical,CA:true,pathlen:3\n"
215 << "keyUsage = keyCertSign,cRLSign\n"
216 << "subjectKeyIdentifier = hash\n"
217 << "authorityKeyIdentifier = keyid:always,issuer:always\n"
218 << "[ req_distinguished_name ]\n"
219 << "O = Unique organization name\n"
220 << "OU = Organization unit\n"
221 << "CN = Entity and dnQualifier\n";
224 string const ca_subject = "/O=" + organisation +
225 "/OU=" + organisational_unit +
226 "/CN=" + root_common_name +
227 "/dnQualifier=" + public_key_digest ("ca.key", openssl);
232 "%1 req -new -x509 -sha256 -config ca.cnf -days %2 -set_serial 5"
233 " -subj \"%3\" -key ca.key -outform PEM -out ca.self-signed.pem",
234 quoted_openssl, days, ca_subject
239 command (quoted_openssl + " genrsa -out intermediate.key 2048");
242 ofstream f ("intermediate.cnf");
244 << "distinguished_name = req_distinguished_name\n"
245 << "x509_extensions = v3_ca\n"
246 << "string_mask = nombstr\n"
248 << "basicConstraints = critical,CA:true,pathlen:2\n"
249 << "keyUsage = keyCertSign,cRLSign\n"
250 << "subjectKeyIdentifier = hash\n"
251 << "authorityKeyIdentifier = keyid:always,issuer:always\n"
252 << "[ req_distinguished_name ]\n"
253 << "O = Unique organization name\n"
254 << "OU = Organization unit\n"
255 << "CN = Entity and dnQualifier\n";
258 string const inter_subject = "/O=" + organisation +
259 "/OU=" + organisational_unit +
260 "/CN=" + intermediate_common_name +
261 "/dnQualifier=" + public_key_digest ("intermediate.key", openssl);
266 "%1 req -new -config intermediate.cnf -days %2 -subj \"%3\" -key intermediate.key -out intermediate.csr",
267 quoted_openssl, days - 1, inter_subject
274 "%1 x509 -req -sha256 -days %2 -CA ca.self-signed.pem -CAkey ca.key -set_serial 6"
275 " -in intermediate.csr -extfile intermediate.cnf -extensions v3_ca -out intermediate.signed.pem",
276 quoted_openssl, days - 1
280 command (quoted_openssl + " genrsa -out leaf.key 2048");
283 ofstream f ("leaf.cnf");
285 << "distinguished_name = req_distinguished_name\n"
286 << "x509_extensions = v3_ca\n"
287 << "string_mask = nombstr\n"
289 << "basicConstraints = critical,CA:false\n"
290 << "keyUsage = digitalSignature,keyEncipherment\n"
291 << "subjectKeyIdentifier = hash\n"
292 << "authorityKeyIdentifier = keyid,issuer:always\n"
293 << "[ req_distinguished_name ]\n"
294 << "O = Unique organization name\n"
295 << "OU = Organization unit\n"
296 << "CN = Entity and dnQualifier\n";
299 string const leaf_subject = "/O=" + organisation +
300 "/OU=" + organisational_unit +
301 "/CN=" + leaf_common_name +
302 "/dnQualifier=" + public_key_digest ("leaf.key", openssl);
307 "%1 req -new -config leaf.cnf -days %2 -subj \"%3\" -key leaf.key -outform PEM -out leaf.csr",
308 quoted_openssl, days - 2, leaf_subject
315 "%1 x509 -req -sha256 -days %2 -CA intermediate.signed.pem -CAkey intermediate.key"
316 " -set_serial 7 -in leaf.csr -extfile leaf.cnf -extensions v3_ca -out leaf.signed.pem",
317 quoted_openssl, days - 2
321 boost::filesystem::current_path (cwd);
323 _certificates.push_back (dcp::Certificate(dcp::file_to_string(directory / "ca.self-signed.pem")));
324 _certificates.push_back (dcp::Certificate(dcp::file_to_string(directory / "intermediate.signed.pem")));
325 _certificates.push_back (dcp::Certificate(dcp::file_to_string(directory / "leaf.signed.pem")));
327 _key = dcp::file_to_string (directory / "leaf.key");
329 boost::filesystem::remove_all (directory);
333 CertificateChain::CertificateChain (string s)
338 s = c.read_string (s);
339 _certificates.push_back (c);
340 } catch (MiscError& e) {
341 /* Failed to read a certificate, just stop */
346 /* This will throw an exception if the chain cannot be ordered */
352 CertificateChain::root () const
354 DCP_ASSERT (!_certificates.empty());
355 return root_to_leaf().front();
360 CertificateChain::leaf () const
362 DCP_ASSERT (!_certificates.empty());
363 return root_to_leaf().back();
367 CertificateChain::List
368 CertificateChain::leaf_to_root () const
370 auto l = root_to_leaf ();
371 std::reverse (l.begin(), l.end());
376 CertificateChain::List
377 CertificateChain::unordered () const
379 return _certificates;
384 CertificateChain::add (Certificate c)
386 _certificates.push_back (c);
391 CertificateChain::remove (Certificate c)
393 auto i = std::find(_certificates.begin(), _certificates.end(), c);
394 if (i != _certificates.end()) {
395 _certificates.erase (i);
401 CertificateChain::remove (int i)
403 auto j = _certificates.begin ();
404 while (j != _certificates.end () && i > 0) {
409 if (j != _certificates.end ()) {
410 _certificates.erase (j);
416 CertificateChain::chain_valid () const
418 return chain_valid (_certificates);
423 CertificateChain::chain_valid (List const & chain) const
425 /* Here I am taking a chain of certificates A/B/C/D and checking validity of B wrt A,
426 C wrt B and D wrt C. It also appears necessary to check the issuer of B/C/D matches
427 the subject of A/B/C; I don't understand why. I'm sure there's a better way of doing
428 this with OpenSSL but the documentation does not appear not likely to reveal it
432 auto store = X509_STORE_new ();
434 throw MiscError ("could not create X509 store");
437 /* Put all the certificates into the store */
438 for (auto const& i: chain) {
439 if (!X509_STORE_add_cert(store, i.x509())) {
440 X509_STORE_free(store);
445 /* Verify each one */
446 for (auto i = chain.begin(); i != chain.end(); ++i) {
450 if (j == chain.end ()) {
454 auto ctx = X509_STORE_CTX_new ();
456 X509_STORE_free (store);
457 throw MiscError ("could not create X509 store context");
460 X509_STORE_set_flags (store, 0);
461 if (!X509_STORE_CTX_init (ctx, store, j->x509(), 0)) {
462 X509_STORE_CTX_free (ctx);
463 X509_STORE_free (store);
464 throw MiscError ("could not initialise X509 store context");
467 int const v = X509_verify_cert (ctx);
468 X509_STORE_CTX_free (ctx);
471 X509_STORE_free (store);
475 /* I don't know why OpenSSL doesn't check this stuff
476 in verify_cert, but without these checks the
477 certificates_validation8 test fails.
479 if (j->issuer() != i->subject() || j->subject() == i->subject()) {
480 X509_STORE_free (store);
486 X509_STORE_free (store);
493 CertificateChain::private_key_valid () const
495 if (_certificates.empty ()) {
503 auto bio = BIO_new_mem_buf (const_cast<char *> (_key->c_str ()), -1);
505 throw MiscError ("could not create memory BIO");
508 auto private_key = PEM_read_bio_RSAPrivateKey (bio, 0, 0, 0);
513 auto public_key = leaf().public_key ();
515 #if OPENSSL_VERSION_NUMBER > 0x10100000L
516 BIGNUM const * private_key_n;
517 RSA_get0_key(private_key, &private_key_n, 0, 0);
518 BIGNUM const * public_key_n;
519 RSA_get0_key(public_key, &public_key_n, 0, 0);
520 if (!private_key_n || !public_key_n) {
523 bool const valid = !BN_cmp (private_key_n, public_key_n);
525 bool const valid = !BN_cmp (private_key->n, public_key->n);
534 CertificateChain::valid (string* reason) const
538 } catch (CertificateChainError& e) {
540 *reason = "certificates do not form a chain";
545 if (!private_key_valid ()) {
547 *reason = "private key does not exist, or does not match leaf certificate";
556 CertificateChain::List
557 CertificateChain::root_to_leaf () const
559 auto rtl = _certificates;
560 std::sort (rtl.begin(), rtl.end());
562 if (chain_valid (rtl)) {
565 } while (std::next_permutation (rtl.begin(), rtl.end()));
567 throw CertificateChainError ("certificate chain is not consistent");
572 CertificateChain::sign (xmlpp::Element* parent, Standard standard) const
576 parent->add_child_text(" ");
577 auto signer = parent->add_child("Signer");
578 signer->set_namespace_declaration ("http://www.w3.org/2000/09/xmldsig#", "dsig");
579 auto data = signer->add_child("X509Data", "dsig");
580 auto serial_element = data->add_child("X509IssuerSerial", "dsig");
581 serial_element->add_child("X509IssuerName", "dsig")->add_child_text (leaf().issuer());
582 serial_element->add_child("X509SerialNumber", "dsig")->add_child_text (leaf().serial());
583 data->add_child("X509SubjectName", "dsig")->add_child_text (leaf().subject());
589 parent->add_child_text("\n ");
590 auto signature = parent->add_child("Signature");
591 signature->set_namespace_declaration ("http://www.w3.org/2000/09/xmldsig#", "dsig");
592 signature->set_namespace ("dsig");
593 parent->add_child_text("\n");
595 auto signed_info = signature->add_child ("SignedInfo", "dsig");
596 signed_info->add_child("CanonicalizationMethod", "dsig")->set_attribute ("Algorithm", "http://www.w3.org/TR/2001/REC-xml-c14n-20010315");
598 if (standard == Standard::INTEROP) {
599 signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#rsa-sha1");
601 signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
604 auto reference = signed_info->add_child("Reference", "dsig");
605 reference->set_attribute ("URI", "");
607 auto transforms = reference->add_child("Transforms", "dsig");
608 transforms->add_child("Transform", "dsig")->set_attribute (
609 "Algorithm", "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
612 reference->add_child("DigestMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#sha1");
613 /* This will be filled in by the signing later */
614 reference->add_child("DigestValue", "dsig");
616 signature->add_child("SignatureValue", "dsig");
617 signature->add_child("KeyInfo", "dsig");
618 add_signature_value (signature, "dsig", true);
623 CertificateChain::add_signature_value (xmlpp::Element* parent, string ns, bool add_indentation) const
625 cxml::Node cp (parent);
626 auto key_info = cp.node_child("KeyInfo")->node();
628 /* Add the certificate chain to the KeyInfo child node of parent */
629 for (auto const& i: leaf_to_root()) {
630 auto data = key_info->add_child("X509Data", ns);
633 auto serial = data->add_child("X509IssuerSerial", ns);
634 serial->add_child("X509IssuerName", ns)->add_child_text (i.issuer ());
635 serial->add_child("X509SerialNumber", ns)->add_child_text (i.serial ());
638 data->add_child("X509Certificate", ns)->add_child_text (i.certificate());
641 auto signature_context = xmlSecDSigCtxCreate (0);
642 if (signature_context == 0) {
643 throw MiscError ("could not create signature context");
646 signature_context->signKey = xmlSecCryptoAppKeyLoadMemory (
647 reinterpret_cast<const unsigned char *> (_key->c_str()), _key->size(), xmlSecKeyDataFormatPem, 0, 0, 0
650 if (signature_context->signKey == 0) {
651 throw runtime_error ("could not read private key");
654 if (add_indentation) {
657 int const r = xmlSecDSigCtxSign (signature_context, parent->cobj ());
659 throw MiscError (String::compose ("could not sign (%1)", r));
662 xmlSecDSigCtxDestroy (signature_context);
667 CertificateChain::chain () const
670 for (auto const& i: root_to_leaf()) {
671 o += i.certificate(true);