2 Copyright (C) 2013-2016 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.
34 /** @file src/signer_chain.cc
35 * @brief Functions to make signer chains.
38 #include "certificate_chain.h"
39 #include "exceptions.h"
41 #include "dcp_assert.h"
42 #include "compose.hpp"
43 #include <asdcp/KM_util.h>
44 #include <libcxml/cxml.h>
45 #include <libxml++/libxml++.h>
46 #include <xmlsec/xmldsig.h>
47 #include <xmlsec/dl.h>
48 #include <xmlsec/app.h>
49 #include <xmlsec/crypto.h>
50 #include <openssl/sha.h>
51 #include <openssl/bio.h>
52 #include <openssl/evp.h>
53 #include <openssl/pem.h>
54 #include <openssl/rsa.h>
55 #include <boost/filesystem.hpp>
56 #include <boost/algorithm/string.hpp>
57 #include <boost/foreach.hpp>
64 using std::runtime_error;
67 /** Run a shell command.
68 * @param cmd Command to run (UTF8-encoded).
74 /* We need to use CreateProcessW on Windows so that the UTF-8/16 mess
77 int const wn = MultiByteToWideChar (CP_UTF8, 0, cmd.c_str(), -1, 0, 0);
78 wchar_t* buffer = new wchar_t[wn];
79 if (MultiByteToWideChar (CP_UTF8, 0, cmd.c_str(), -1, buffer, wn) == 0) {
86 STARTUPINFOW startup_info;
87 memset (&startup_info, 0, sizeof (startup_info));
88 startup_info.cb = sizeof (startup_info);
89 PROCESS_INFORMATION process_info;
91 /* XXX: this doesn't actually seem to work; failing commands end up with
94 if (CreateProcessW (0, buffer, 0, 0, FALSE, CREATE_NO_WINDOW, 0, 0, &startup_info, &process_info)) {
95 WaitForSingleObject (process_info.hProcess, INFINITE);
97 if (GetExitCodeProcess (process_info.hProcess, &c)) {
100 CloseHandle (process_info.hProcess);
101 CloseHandle (process_info.hThread);
106 cmd += " 2> /dev/null";
107 int const r = system (cmd.c_str ());
108 int const code = WEXITSTATUS (r);
111 throw dcp::MiscError (String::compose ("error %1 in %2 within %3", code, cmd, boost::filesystem::current_path().string()));
115 /** Extract a public key from a private key and create a SHA1 digest of it.
116 * @param private_key Private key
117 * @param openssl openssl binary name (or full path if openssl is not on the system path).
118 * @return SHA1 digest of corresponding public key, with escaped / characters.
121 public_key_digest (boost::filesystem::path private_key, boost::filesystem::path openssl)
123 boost::filesystem::path public_name = private_key.string() + ".public";
125 /* Create the public key from the private key */
126 command (String::compose("\"%1\" rsa -outform PEM -pubout -in %2 -out %3", openssl.string(), private_key.string(), public_name.string ()));
128 /* Read in the public key from the file */
131 ifstream f (public_name.string().c_str ());
133 throw dcp::MiscError ("public key not found");
140 if (line.length() >= 10 && line.substr(0, 10) == "-----BEGIN") {
142 } else if (line.length() >= 8 && line.substr(0, 8) == "-----END") {
149 /* Decode the base64 of the public key */
151 unsigned char buffer[512];
152 int const N = dcp::base64_decode (pub, buffer, 1024);
154 /* Hash it with SHA1 (without the first 24 bytes, for reasons that are not entirely clear) */
157 if (!SHA1_Init (&context)) {
158 throw dcp::MiscError ("could not init SHA1 context");
161 if (!SHA1_Update (&context, buffer + 24, N - 24)) {
162 throw dcp::MiscError ("could not update SHA1 digest");
165 unsigned char digest[SHA_DIGEST_LENGTH];
166 if (!SHA1_Final (digest, &context)) {
167 throw dcp::MiscError ("could not finish SHA1 digest");
170 char digest_base64[64];
171 string dig = Kumu::base64encode (digest, SHA_DIGEST_LENGTH, digest_base64, 64);
172 #ifdef LIBDCP_WINDOWS
173 boost::replace_all (dig, "/", "\\/");
175 boost::replace_all (dig, "/", "\\\\/");
180 CertificateChain::CertificateChain (
181 boost::filesystem::path openssl,
183 string organisational_unit,
184 string root_common_name,
185 string intermediate_common_name,
186 string leaf_common_name
189 boost::filesystem::path directory = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path ();
190 boost::filesystem::create_directories (directory);
192 boost::filesystem::path const cwd = boost::filesystem::current_path ();
193 boost::filesystem::current_path (directory);
195 string quoted_openssl = "\"" + openssl.string() + "\"";
197 command (quoted_openssl + " genrsa -out ca.key 2048");
200 ofstream f ("ca.cnf");
202 << "distinguished_name = req_distinguished_name\n"
203 << "x509_extensions = v3_ca\n"
205 << "basicConstraints = critical,CA:true,pathlen:3\n"
206 << "keyUsage = keyCertSign,cRLSign\n"
207 << "subjectKeyIdentifier = hash\n"
208 << "authorityKeyIdentifier = keyid:always,issuer:always\n"
209 << "[ req_distinguished_name ]\n"
210 << "O = Unique organization name\n"
211 << "OU = Organization unit\n"
212 << "CN = Entity and dnQualifier\n";
215 string const ca_subject = "/O=" + organisation +
216 "/OU=" + organisational_unit +
217 "/CN=" + root_common_name +
218 "/dnQualifier=" + public_key_digest ("ca.key", openssl);
223 "%1 req -new -x509 -sha256 -config ca.cnf -days 3650 -set_serial 5"
224 " -subj \"%2\" -key ca.key -outform PEM -out ca.self-signed.pem",
225 quoted_openssl, ca_subject
230 command (quoted_openssl + " genrsa -out intermediate.key 2048");
233 ofstream f ("intermediate.cnf");
235 << "distinguished_name = req_distinguished_name\n"
236 << "x509_extensions = v3_ca\n"
238 << "basicConstraints = critical,CA:true,pathlen:2\n"
239 << "keyUsage = keyCertSign,cRLSign\n"
240 << "subjectKeyIdentifier = hash\n"
241 << "authorityKeyIdentifier = keyid:always,issuer:always\n"
242 << "[ req_distinguished_name ]\n"
243 << "O = Unique organization name\n"
244 << "OU = Organization unit\n"
245 << "CN = Entity and dnQualifier\n";
248 string const inter_subject = "/O=" + organisation +
249 "/OU=" + organisational_unit +
250 "/CN=" + intermediate_common_name +
251 "/dnQualifier=" + public_key_digest ("intermediate.key", openssl);
256 "%1 req -new -config intermediate.cnf -days 3649 -subj \"%2\" -key intermediate.key -out intermediate.csr",
257 quoted_openssl, inter_subject
264 " x509 -req -sha256 -days 3649 -CA ca.self-signed.pem -CAkey ca.key -set_serial 6"
265 " -in intermediate.csr -extfile intermediate.cnf -extensions v3_ca -out intermediate.signed.pem"
268 command (quoted_openssl + " genrsa -out leaf.key 2048");
271 ofstream f ("leaf.cnf");
273 << "distinguished_name = req_distinguished_name\n"
274 << "x509_extensions = v3_ca\n"
276 << "basicConstraints = critical,CA:false\n"
277 << "keyUsage = digitalSignature,keyEncipherment\n"
278 << "subjectKeyIdentifier = hash\n"
279 << "authorityKeyIdentifier = keyid,issuer:always\n"
280 << "[ req_distinguished_name ]\n"
281 << "O = Unique organization name\n"
282 << "OU = Organization unit\n"
283 << "CN = Entity and dnQualifier\n";
286 string const leaf_subject = "/O=" + organisation +
287 "/OU=" + organisational_unit +
288 "/CN=" + leaf_common_name +
289 "/dnQualifier=" + public_key_digest ("leaf.key", openssl);
294 "%1 req -new -config leaf.cnf -days 3648 -subj \"%2\" -key leaf.key -outform PEM -out leaf.csr",
295 quoted_openssl, leaf_subject
302 " x509 -req -sha256 -days 3648 -CA intermediate.signed.pem -CAkey intermediate.key"
303 " -set_serial 7 -in leaf.csr -extfile leaf.cnf -extensions v3_ca -out leaf.signed.pem"
306 boost::filesystem::current_path (cwd);
308 _certificates.push_back (dcp::Certificate (dcp::file_to_string (directory / "ca.self-signed.pem")));
309 _certificates.push_back (dcp::Certificate (dcp::file_to_string (directory / "intermediate.signed.pem")));
310 _certificates.push_back (dcp::Certificate (dcp::file_to_string (directory / "leaf.signed.pem")));
312 _key = dcp::file_to_string (directory / "leaf.key");
314 boost::filesystem::remove_all (directory);
317 CertificateChain::CertificateChain (string s)
322 s = c.read_string (s);
323 _certificates.push_back (c);
324 } catch (MiscError& e) {
325 /* Failed to read a certificate, just stop */
330 if (!attempt_reorder ()) {
331 throw MiscError ("could not find certificate chain order");
335 /** @return Root certificate */
337 CertificateChain::root () const
339 DCP_ASSERT (!_certificates.empty());
340 return _certificates.front ();
343 /** @return Leaf certificate */
345 CertificateChain::leaf () const
347 DCP_ASSERT (!_certificates.empty());
348 return _certificates.back ();
351 /** @return Certificates in order from root to leaf */
352 CertificateChain::List
353 CertificateChain::root_to_leaf () const
355 return _certificates;
358 /** @return Certificates in order from leaf to root */
359 CertificateChain::List
360 CertificateChain::leaf_to_root () const
362 List c = _certificates;
367 /** Add a certificate to the end of the chain.
368 * @param c Certificate to add.
371 CertificateChain::add (Certificate c)
373 _certificates.push_back (c);
376 /** Remove a certificate from the chain.
377 * @param c Certificate to remove.
380 CertificateChain::remove (Certificate c)
382 _certificates.remove (c);
385 /** Remove the i'th certificate in the list, as listed
389 CertificateChain::remove (int i)
391 List::iterator j = _certificates.begin ();
392 while (j != _certificates.end () && i > 0) {
397 if (j != _certificates.end ()) {
398 _certificates.erase (j);
402 /** Check to see if the chain is valid (i.e. root signs the intermediate, intermediate
403 * signs the leaf and so on) and that the private key (if there is one) matches the
405 * @param valid if non-0 and the CertificateChain is not valid, this is filled in with
407 * @return true if it's ok, false if not.
410 CertificateChain::valid (string* reason) const
412 /* Check the certificate chain */
414 X509_STORE* store = X509_STORE_new ();
416 throw MiscError ("could not create X509 store");
420 for (List::const_iterator i = _certificates.begin(); i != _certificates.end(); ++i) {
422 List::const_iterator j = i;
424 if (j == _certificates.end ()) {
428 if (!X509_STORE_add_cert (store, i->x509 ())) {
429 X509_STORE_free (store);
431 *reason = "X509_STORE_add_cert failed";
436 X509_STORE_CTX* ctx = X509_STORE_CTX_new ();
438 X509_STORE_free (store);
439 throw MiscError ("could not create X509 store context");
442 X509_STORE_set_flags (store, 0);
443 if (!X509_STORE_CTX_init (ctx, store, j->x509(), 0)) {
444 X509_STORE_CTX_free (ctx);
445 X509_STORE_free (store);
447 *reason = "X509_STORE_CTX_init failed";
452 int v = X509_verify_cert (ctx);
453 X509_STORE_CTX_free (ctx);
456 X509_STORE_free (store);
458 *reason = String::compose ("X509_verify_cert failed for certificate number %1", n);
466 X509_STORE_free (store);
468 /* Check that the leaf certificate matches the private key, if there is one */
474 BIO* bio = BIO_new_mem_buf (const_cast<char *> (_key->c_str ()), -1);
476 throw MiscError ("could not create memory BIO");
479 RSA* private_key = PEM_read_bio_RSAPrivateKey (bio, 0, 0, 0);
480 RSA* public_key = leaf().public_key ();
482 #if OPENSSL_VERSION_NUMBER > 0x10100000L
483 #warning "Using new OpenSSL API"
484 BIGNUM const * private_key_n;
485 RSA_get0_key(private_key, &private_key_n, 0, 0);
486 BIGNUM const * public_key_n;
487 RSA_get0_key(public_key, &public_key_n, 0, 0);
488 bool const valid = !BN_cmp (private_key_n, public_key_n);
490 bool const valid = !BN_cmp (private_key->n, public_key->n);
494 if (!valid && reason) {
495 *reason = "leaf certificate does not match private key";
501 /** @return true if the chain is now in order from root to leaf,
502 * false if no correct order was found.
505 CertificateChain::attempt_reorder ()
507 List original = _certificates;
508 _certificates.sort ();
513 } while (std::next_permutation (_certificates.begin(), _certificates.end ()));
515 _certificates = original;
519 /** Add a <Signer> and <ds:Signature> nodes to an XML node.
520 * @param parent XML node to add to.
521 * @param standard INTEROP or SMPTE.
524 CertificateChain::sign (xmlpp::Element* parent, Standard standard) const
528 xmlpp::Element* signer = parent->add_child("Signer");
529 xmlpp::Element* data = signer->add_child("X509Data", "dsig");
530 xmlpp::Element* serial_element = data->add_child("X509IssuerSerial", "dsig");
531 serial_element->add_child("X509IssuerName", "dsig")->add_child_text (leaf().issuer());
532 serial_element->add_child("X509SerialNumber", "dsig")->add_child_text (leaf().serial());
533 data->add_child("X509SubjectName", "dsig")->add_child_text (leaf().subject());
537 xmlpp::Element* signature = parent->add_child("Signature", "dsig");
539 xmlpp::Element* signed_info = signature->add_child ("SignedInfo", "dsig");
540 signed_info->add_child("CanonicalizationMethod", "dsig")->set_attribute ("Algorithm", "http://www.w3.org/TR/2001/REC-xml-c14n-20010315");
542 if (standard == INTEROP) {
543 signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#rsa-sha1");
545 signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
548 xmlpp::Element* reference = signed_info->add_child("Reference", "dsig");
549 reference->set_attribute ("URI", "");
551 xmlpp::Element* transforms = reference->add_child("Transforms", "dsig");
552 transforms->add_child("Transform", "dsig")->set_attribute (
553 "Algorithm", "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
556 reference->add_child("DigestMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#sha1");
557 /* This will be filled in by the signing later */
558 reference->add_child("DigestValue", "dsig");
560 signature->add_child("SignatureValue", "dsig");
561 signature->add_child("KeyInfo", "dsig");
562 add_signature_value (signature, "dsig");
566 /** Sign an XML node.
568 * @param parent Node to sign.
569 * @param ns Namespace to use for the signature XML nodes.
572 CertificateChain::add_signature_value (xmlpp::Node* parent, string ns) const
574 cxml::Node cp (parent);
575 xmlpp::Node* key_info = cp.node_child("KeyInfo")->node ();
577 /* Add the certificate chain to the KeyInfo child node of parent */
578 BOOST_FOREACH (Certificate const & i, leaf_to_root ()) {
579 xmlpp::Element* data = key_info->add_child("X509Data", ns);
582 xmlpp::Element* serial = data->add_child("X509IssuerSerial", ns);
583 serial->add_child("X509IssuerName", ns)->add_child_text (i.issuer ());
584 serial->add_child("X509SerialNumber", ns)->add_child_text (i.serial ());
587 data->add_child("X509Certificate", ns)->add_child_text (i.certificate());
590 xmlSecDSigCtxPtr signature_context = xmlSecDSigCtxCreate (0);
591 if (signature_context == 0) {
592 throw MiscError ("could not create signature context");
595 signature_context->signKey = xmlSecCryptoAppKeyLoadMemory (
596 reinterpret_cast<const unsigned char *> (_key->c_str()), _key->size(), xmlSecKeyDataFormatPem, 0, 0, 0
599 if (signature_context->signKey == 0) {
600 throw runtime_error ("could not read private key");
603 /* XXX: set key name to the PEM string: this can't be right! */
604 if (xmlSecKeySetName (signature_context->signKey, reinterpret_cast<const xmlChar *> (_key->c_str())) < 0) {
605 throw MiscError ("could not set key name");
608 int const r = xmlSecDSigCtxSign (signature_context, parent->cobj ());
610 throw MiscError (String::compose ("could not sign (%1)", r));
613 xmlSecDSigCtxDestroy (signature_context);
617 CertificateChain::chain () const
620 BOOST_FOREACH (Certificate const &i, root_to_leaf ()) {
621 o += i.certificate(true);