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 <boost/filesystem.hpp>
55 #include <boost/algorithm/string.hpp>
56 #include <boost/foreach.hpp>
63 using std::runtime_error;
66 /** Run a shell command.
67 * @param cmd Command to run (UTF8-encoded).
73 /* We need to use CreateProcessW on Windows so that the UTF-8/16 mess
76 int const wn = MultiByteToWideChar (CP_UTF8, 0, cmd.c_str(), -1, 0, 0);
77 wchar_t* buffer = new wchar_t[wn];
78 if (MultiByteToWideChar (CP_UTF8, 0, cmd.c_str(), -1, buffer, wn) == 0) {
85 STARTUPINFOW startup_info;
86 memset (&startup_info, 0, sizeof (startup_info));
87 startup_info.cb = sizeof (startup_info);
88 PROCESS_INFORMATION process_info;
90 /* XXX: this doesn't actually seem to work; failing commands end up with
93 if (CreateProcessW (0, buffer, 0, 0, FALSE, CREATE_NO_WINDOW, 0, 0, &startup_info, &process_info)) {
94 WaitForSingleObject (process_info.hProcess, INFINITE);
96 if (GetExitCodeProcess (process_info.hProcess, &c)) {
99 CloseHandle (process_info.hProcess);
100 CloseHandle (process_info.hThread);
105 cmd += " 2> /dev/null";
106 int const r = system (cmd.c_str ());
107 int const code = WEXITSTATUS (r);
110 throw dcp::MiscError (String::compose ("error %1 in %2 within %3", code, cmd, boost::filesystem::current_path().string()));
114 /** Extract a public key from a private key and create a SHA1 digest of it.
115 * @param private_key Private key
116 * @param openssl openssl binary name (or full path if openssl is not on the system path).
117 * @return SHA1 digest of corresponding public key, with escaped / characters.
120 public_key_digest (boost::filesystem::path private_key, boost::filesystem::path openssl)
122 boost::filesystem::path public_name = private_key.string() + ".public";
124 /* Create the public key from the private key */
125 command (String::compose("\"%1\" rsa -outform PEM -pubout -in %2 -out %3", openssl.string(), private_key.string(), public_name.string ()));
127 /* Read in the public key from the file */
130 ifstream f (public_name.string().c_str ());
132 throw dcp::MiscError ("public key not found");
139 if (line.length() >= 10 && line.substr(0, 10) == "-----BEGIN") {
141 } else if (line.length() >= 8 && line.substr(0, 8) == "-----END") {
148 /* Decode the base64 of the public key */
150 unsigned char buffer[512];
151 int const N = dcp::base64_decode (pub, buffer, 1024);
153 /* Hash it with SHA1 (without the first 24 bytes, for reasons that are not entirely clear) */
156 if (!SHA1_Init (&context)) {
157 throw dcp::MiscError ("could not init SHA1 context");
160 if (!SHA1_Update (&context, buffer + 24, N - 24)) {
161 throw dcp::MiscError ("could not update SHA1 digest");
164 unsigned char digest[SHA_DIGEST_LENGTH];
165 if (!SHA1_Final (digest, &context)) {
166 throw dcp::MiscError ("could not finish SHA1 digest");
169 char digest_base64[64];
170 string dig = Kumu::base64encode (digest, SHA_DIGEST_LENGTH, digest_base64, 64);
171 #ifdef LIBDCP_WINDOWS
172 boost::replace_all (dig, "/", "\\/");
174 boost::replace_all (dig, "/", "\\\\/");
179 CertificateChain::CertificateChain (
180 boost::filesystem::path openssl,
182 string organisational_unit,
183 string root_common_name,
184 string intermediate_common_name,
185 string leaf_common_name
188 boost::filesystem::path directory = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path ();
189 boost::filesystem::create_directories (directory);
191 boost::filesystem::path const cwd = boost::filesystem::current_path ();
192 boost::filesystem::current_path (directory);
194 string quoted_openssl = "\"" + openssl.string() + "\"";
196 command (quoted_openssl + " genrsa -out ca.key 2048");
199 ofstream f ("ca.cnf");
201 << "distinguished_name = req_distinguished_name\n"
202 << "x509_extensions = v3_ca\n"
204 << "basicConstraints = critical,CA:true,pathlen:3\n"
205 << "keyUsage = keyCertSign,cRLSign\n"
206 << "subjectKeyIdentifier = hash\n"
207 << "authorityKeyIdentifier = keyid:always,issuer:always\n"
208 << "[ req_distinguished_name ]\n"
209 << "O = Unique organization name\n"
210 << "OU = Organization unit\n"
211 << "CN = Entity and dnQualifier\n";
214 string const ca_subject = "/O=" + organisation +
215 "/OU=" + organisational_unit +
216 "/CN=" + root_common_name +
217 "/dnQualifier=" + public_key_digest ("ca.key", openssl);
222 "%1 req -new -x509 -sha256 -config ca.cnf -days 3650 -set_serial 5"
223 " -subj \"%2\" -key ca.key -outform PEM -out ca.self-signed.pem",
224 quoted_openssl, ca_subject
229 command (quoted_openssl + " genrsa -out intermediate.key 2048");
232 ofstream f ("intermediate.cnf");
234 << "distinguished_name = req_distinguished_name\n"
235 << "x509_extensions = v3_ca\n"
237 << "basicConstraints = critical,CA:true,pathlen:2\n"
238 << "keyUsage = keyCertSign,cRLSign\n"
239 << "subjectKeyIdentifier = hash\n"
240 << "authorityKeyIdentifier = keyid:always,issuer:always\n"
241 << "[ req_distinguished_name ]\n"
242 << "O = Unique organization name\n"
243 << "OU = Organization unit\n"
244 << "CN = Entity and dnQualifier\n";
247 string const inter_subject = "/O=" + organisation +
248 "/OU=" + organisational_unit +
249 "/CN=" + intermediate_common_name +
250 "/dnQualifier=" + public_key_digest ("intermediate.key", openssl);
255 "%1 req -new -config intermediate.cnf -days 3649 -subj \"%2\" -key intermediate.key -out intermediate.csr",
256 quoted_openssl, inter_subject
263 " x509 -req -sha256 -days 3649 -CA ca.self-signed.pem -CAkey ca.key -set_serial 6"
264 " -in intermediate.csr -extfile intermediate.cnf -extensions v3_ca -out intermediate.signed.pem"
267 command (quoted_openssl + " genrsa -out leaf.key 2048");
270 ofstream f ("leaf.cnf");
272 << "distinguished_name = req_distinguished_name\n"
273 << "x509_extensions = v3_ca\n"
275 << "basicConstraints = critical,CA:false\n"
276 << "keyUsage = digitalSignature,keyEncipherment\n"
277 << "subjectKeyIdentifier = hash\n"
278 << "authorityKeyIdentifier = keyid,issuer:always\n"
279 << "[ req_distinguished_name ]\n"
280 << "O = Unique organization name\n"
281 << "OU = Organization unit\n"
282 << "CN = Entity and dnQualifier\n";
285 string const leaf_subject = "/O=" + organisation +
286 "/OU=" + organisational_unit +
287 "/CN=" + leaf_common_name +
288 "/dnQualifier=" + public_key_digest ("leaf.key", openssl);
293 "%1 req -new -config leaf.cnf -days 3648 -subj \"%2\" -key leaf.key -outform PEM -out leaf.csr",
294 quoted_openssl, leaf_subject
301 " x509 -req -sha256 -days 3648 -CA intermediate.signed.pem -CAkey intermediate.key"
302 " -set_serial 7 -in leaf.csr -extfile leaf.cnf -extensions v3_ca -out leaf.signed.pem"
305 boost::filesystem::current_path (cwd);
307 _certificates.push_back (dcp::Certificate (dcp::file_to_string (directory / "ca.self-signed.pem")));
308 _certificates.push_back (dcp::Certificate (dcp::file_to_string (directory / "intermediate.signed.pem")));
309 _certificates.push_back (dcp::Certificate (dcp::file_to_string (directory / "leaf.signed.pem")));
311 _key = dcp::file_to_string (directory / "leaf.key");
313 boost::filesystem::remove_all (directory);
316 CertificateChain::CertificateChain (string s)
321 s = c.read_string (s);
322 _certificates.push_back (c);
323 } catch (MiscError& e) {
324 /* Failed to read a certificate, just stop */
329 if (!attempt_reorder ()) {
330 throw MiscError ("could not find certificate chain order");
334 /** @return Root certificate */
336 CertificateChain::root () const
338 DCP_ASSERT (!_certificates.empty());
339 return _certificates.front ();
342 /** @return Leaf certificate */
344 CertificateChain::leaf () const
346 DCP_ASSERT (!_certificates.empty());
347 return _certificates.back ();
350 /** @return Certificates in order from root to leaf */
351 CertificateChain::List
352 CertificateChain::root_to_leaf () const
354 return _certificates;
357 /** @return Certificates in order from leaf to root */
358 CertificateChain::List
359 CertificateChain::leaf_to_root () const
361 List c = _certificates;
366 /** Add a certificate to the end of the chain.
367 * @param c Certificate to add.
370 CertificateChain::add (Certificate c)
372 _certificates.push_back (c);
375 /** Remove a certificate from the chain.
376 * @param c Certificate to remove.
379 CertificateChain::remove (Certificate c)
381 _certificates.remove (c);
384 /** Remove the i'th certificate in the list, as listed
388 CertificateChain::remove (int i)
390 List::iterator j = _certificates.begin ();
391 while (j != _certificates.end () && i > 0) {
396 if (j != _certificates.end ()) {
397 _certificates.erase (j);
401 /** Check to see if the chain is valid (i.e. root signs the intermediate, intermediate
402 * signs the leaf and so on) and that the private key (if there is one) matches the
404 * @return true if it's ok, false if not.
407 CertificateChain::valid () const
409 /* Check the certificate chain */
411 X509_STORE* store = X509_STORE_new ();
416 for (List::const_iterator i = _certificates.begin(); i != _certificates.end(); ++i) {
418 List::const_iterator j = i;
420 if (j == _certificates.end ()) {
424 if (!X509_STORE_add_cert (store, i->x509 ())) {
425 X509_STORE_free (store);
429 X509_STORE_CTX* ctx = X509_STORE_CTX_new ();
431 X509_STORE_free (store);
435 X509_STORE_set_flags (store, 0);
436 if (!X509_STORE_CTX_init (ctx, store, j->x509 (), 0)) {
437 X509_STORE_CTX_free (ctx);
438 X509_STORE_free (store);
442 int v = X509_verify_cert (ctx);
443 X509_STORE_CTX_free (ctx);
446 X509_STORE_free (store);
451 X509_STORE_free (store);
453 /* Check that the leaf certificate matches the private key, if there is one */
459 BIO* bio = BIO_new_mem_buf (const_cast<char *> (_key->c_str ()), -1);
461 throw MiscError ("could not create memory BIO");
464 RSA* private_key = PEM_read_bio_RSAPrivateKey (bio, 0, 0, 0);
465 RSA* public_key = leaf().public_key ();
466 bool const valid = !BN_cmp (private_key->n, public_key->n);
472 /** @return true if the chain is now in order from root to leaf,
473 * false if no correct order was found.
476 CertificateChain::attempt_reorder ()
478 List original = _certificates;
479 _certificates.sort ();
484 } while (std::next_permutation (_certificates.begin(), _certificates.end ()));
486 _certificates = original;
490 /** Add a <Signer> and <ds:Signature> nodes to an XML node.
491 * @param parent XML node to add to.
492 * @param standard INTEROP or SMPTE.
495 CertificateChain::sign (xmlpp::Element* parent, Standard standard) const
499 xmlpp::Element* signer = parent->add_child("Signer");
500 xmlpp::Element* data = signer->add_child("X509Data", "dsig");
501 xmlpp::Element* serial_element = data->add_child("X509IssuerSerial", "dsig");
502 serial_element->add_child("X509IssuerName", "dsig")->add_child_text (leaf().issuer());
503 serial_element->add_child("X509SerialNumber", "dsig")->add_child_text (leaf().serial());
504 data->add_child("X509SubjectName", "dsig")->add_child_text (leaf().subject());
508 xmlpp::Element* signature = parent->add_child("Signature", "dsig");
510 xmlpp::Element* signed_info = signature->add_child ("SignedInfo", "dsig");
511 signed_info->add_child("CanonicalizationMethod", "dsig")->set_attribute ("Algorithm", "http://www.w3.org/TR/2001/REC-xml-c14n-20010315");
513 if (standard == INTEROP) {
514 signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#rsa-sha1");
516 signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
519 xmlpp::Element* reference = signed_info->add_child("Reference", "dsig");
520 reference->set_attribute ("URI", "");
522 xmlpp::Element* transforms = reference->add_child("Transforms", "dsig");
523 transforms->add_child("Transform", "dsig")->set_attribute (
524 "Algorithm", "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
527 reference->add_child("DigestMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#sha1");
528 /* This will be filled in by the signing later */
529 reference->add_child("DigestValue", "dsig");
531 signature->add_child("SignatureValue", "dsig");
532 signature->add_child("KeyInfo", "dsig");
533 add_signature_value (signature, "dsig");
537 /** Sign an XML node.
539 * @param parent Node to sign.
540 * @param ns Namespace to use for the signature XML nodes.
543 CertificateChain::add_signature_value (xmlpp::Node* parent, string ns) const
545 cxml::Node cp (parent);
546 xmlpp::Node* key_info = cp.node_child("KeyInfo")->node ();
548 /* Add the certificate chain to the KeyInfo child node of parent */
549 BOOST_FOREACH (Certificate const & i, leaf_to_root ()) {
550 xmlpp::Element* data = key_info->add_child("X509Data", ns);
553 xmlpp::Element* serial = data->add_child("X509IssuerSerial", ns);
554 serial->add_child("X509IssuerName", ns)->add_child_text (i.issuer ());
555 serial->add_child("X509SerialNumber", ns)->add_child_text (i.serial ());
558 data->add_child("X509Certificate", ns)->add_child_text (i.certificate());
561 xmlSecDSigCtxPtr signature_context = xmlSecDSigCtxCreate (0);
562 if (signature_context == 0) {
563 throw MiscError ("could not create signature context");
566 signature_context->signKey = xmlSecCryptoAppKeyLoadMemory (
567 reinterpret_cast<const unsigned char *> (_key->c_str()), _key->size(), xmlSecKeyDataFormatPem, 0, 0, 0
570 if (signature_context->signKey == 0) {
571 throw runtime_error ("could not read private key");
574 /* XXX: set key name to the PEM string: this can't be right! */
575 if (xmlSecKeySetName (signature_context->signKey, reinterpret_cast<const xmlChar *> (_key->c_str())) < 0) {
576 throw MiscError ("could not set key name");
579 int const r = xmlSecDSigCtxSign (signature_context, parent->cobj ());
581 throw MiscError (String::compose ("could not sign (%1)", r));
584 xmlSecDSigCtxDestroy (signature_context);
588 CertificateChain::chain () const
591 BOOST_FOREACH (Certificate const &i, root_to_leaf ()) {
592 o += i.certificate(true);