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>
62 using std::runtime_error;
65 /** Run a shell command.
66 * @param cmd Command to run (UTF8-encoded).
72 /* We need to use CreateProcessW on Windows so that the UTF-8/16 mess
75 int const wn = MultiByteToWideChar (CP_UTF8, 0, cmd.c_str(), -1, 0, 0);
76 wchar_t* buffer = new wchar_t[wn];
77 if (MultiByteToWideChar (CP_UTF8, 0, cmd.c_str(), -1, buffer, wn) == 0) {
84 STARTUPINFOW startup_info;
85 memset (&startup_info, 0, sizeof (startup_info));
86 startup_info.cb = sizeof (startup_info);
87 PROCESS_INFORMATION process_info;
89 /* XXX: this doesn't actually seem to work; failing commands end up with
92 if (CreateProcessW (0, buffer, 0, 0, FALSE, CREATE_NO_WINDOW, 0, 0, &startup_info, &process_info)) {
93 WaitForSingleObject (process_info.hProcess, INFINITE);
95 if (GetExitCodeProcess (process_info.hProcess, &c)) {
98 CloseHandle (process_info.hProcess);
99 CloseHandle (process_info.hThread);
104 cmd += " 2> /dev/null";
105 int const r = system (cmd.c_str ());
106 int const code = WEXITSTATUS (r);
109 locked_stringstream s;
110 s << "error " << code << " in " << cmd << " within " << boost::filesystem::current_path();
111 throw dcp::MiscError (s.str());
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 locked_stringstream s;
127 s << "\"" << openssl.string() << "\" rsa -outform PEM -pubout -in " << private_key.string() << " -out " << public_name.string ();
128 command (s.str().c_str ());
130 /* Read in the public key from the file */
133 ifstream f (public_name.string().c_str ());
135 throw dcp::MiscError ("public key not found");
142 if (line.length() >= 10 && line.substr(0, 10) == "-----BEGIN") {
144 } else if (line.length() >= 8 && line.substr(0, 8) == "-----END") {
151 /* Decode the base64 of the public key */
153 unsigned char buffer[512];
154 int const N = dcp::base64_decode (pub, buffer, 1024);
156 /* Hash it with SHA1 (without the first 24 bytes, for reasons that are not entirely clear) */
159 if (!SHA1_Init (&context)) {
160 throw dcp::MiscError ("could not init SHA1 context");
163 if (!SHA1_Update (&context, buffer + 24, N - 24)) {
164 throw dcp::MiscError ("could not update SHA1 digest");
167 unsigned char digest[SHA_DIGEST_LENGTH];
168 if (!SHA1_Final (digest, &context)) {
169 throw dcp::MiscError ("could not finish SHA1 digest");
172 char digest_base64[64];
173 string dig = Kumu::base64encode (digest, SHA_DIGEST_LENGTH, digest_base64, 64);
174 #ifdef LIBDCP_WINDOWS
175 boost::replace_all (dig, "/", "\\/");
177 boost::replace_all (dig, "/", "\\\\/");
182 CertificateChain::CertificateChain (
183 boost::filesystem::path openssl,
185 string organisational_unit,
186 string root_common_name,
187 string intermediate_common_name,
188 string leaf_common_name
191 boost::filesystem::path directory = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path ();
192 boost::filesystem::create_directories (directory);
194 boost::filesystem::path const cwd = boost::filesystem::current_path ();
195 boost::filesystem::current_path (directory);
197 string quoted_openssl = "\"" + openssl.string() + "\"";
199 command (quoted_openssl + " genrsa -out ca.key 2048");
202 ofstream f ("ca.cnf");
204 << "distinguished_name = req_distinguished_name\n"
205 << "x509_extensions = v3_ca\n"
207 << "basicConstraints = critical,CA:true,pathlen:3\n"
208 << "keyUsage = keyCertSign,cRLSign\n"
209 << "subjectKeyIdentifier = hash\n"
210 << "authorityKeyIdentifier = keyid:always,issuer:always\n"
211 << "[ req_distinguished_name ]\n"
212 << "O = Unique organization name\n"
213 << "OU = Organization unit\n"
214 << "CN = Entity and dnQualifier\n";
217 string const ca_subject = "/O=" + organisation +
218 "/OU=" + organisational_unit +
219 "/CN=" + root_common_name +
220 "/dnQualifier=" + public_key_digest ("ca.key", openssl);
223 locked_stringstream c;
225 << " req -new -x509 -sha256 -config ca.cnf -days 3650 -set_serial 5"
226 << " -subj \"" << ca_subject << "\" -key ca.key -outform PEM -out ca.self-signed.pem";
227 command (c.str().c_str());
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);
254 locked_stringstream s;
256 << " req -new -config intermediate.cnf -days 3649 -subj \"" << inter_subject << "\" -key intermediate.key -out intermediate.csr";
257 command (s.str().c_str());
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);
291 locked_stringstream s;
292 s << quoted_openssl << " req -new -config leaf.cnf -days 3648 -subj \"" << leaf_subject << "\" -key leaf.key -outform PEM -out leaf.csr";
293 command (s.str().c_str());
298 " x509 -req -sha256 -days 3648 -CA intermediate.signed.pem -CAkey intermediate.key"
299 " -set_serial 7 -in leaf.csr -extfile leaf.cnf -extensions v3_ca -out leaf.signed.pem"
302 boost::filesystem::current_path (cwd);
304 _certificates.push_back (dcp::Certificate (dcp::file_to_string (directory / "ca.self-signed.pem")));
305 _certificates.push_back (dcp::Certificate (dcp::file_to_string (directory / "intermediate.signed.pem")));
306 _certificates.push_back (dcp::Certificate (dcp::file_to_string (directory / "leaf.signed.pem")));
308 _key = dcp::file_to_string (directory / "leaf.key");
310 boost::filesystem::remove_all (directory);
313 /** @return Root certificate */
315 CertificateChain::root () const
317 DCP_ASSERT (!_certificates.empty());
318 return _certificates.front ();
321 /** @return Leaf certificate */
323 CertificateChain::leaf () const
325 DCP_ASSERT (_certificates.size() >= 2);
326 return _certificates.back ();
329 /** @return Certificates in order from root to leaf */
330 CertificateChain::List
331 CertificateChain::root_to_leaf () const
333 return _certificates;
336 /** @return Certificates in order from leaf to root */
337 CertificateChain::List
338 CertificateChain::leaf_to_root () const
340 List c = _certificates;
345 /** Add a certificate to the end of the chain.
346 * @param c Certificate to add.
349 CertificateChain::add (Certificate c)
351 _certificates.push_back (c);
354 /** Remove a certificate from the chain.
355 * @param c Certificate to remove.
358 CertificateChain::remove (Certificate c)
360 _certificates.remove (c);
363 /** Remove the i'th certificate in the list, as listed
367 CertificateChain::remove (int i)
369 List::iterator j = _certificates.begin ();
370 while (j != _certificates.end () && i > 0) {
375 if (j != _certificates.end ()) {
376 _certificates.erase (j);
380 /** Check to see if the chain is valid (i.e. root signs the intermediate, intermediate
381 * signs the leaf and so on) and that the private key (if there is one) matches the
383 * @return true if it's ok, false if not.
386 CertificateChain::valid () const
388 /* Check the certificate chain */
390 X509_STORE* store = X509_STORE_new ();
395 for (List::const_iterator i = _certificates.begin(); i != _certificates.end(); ++i) {
397 List::const_iterator j = i;
399 if (j == _certificates.end ()) {
403 if (!X509_STORE_add_cert (store, i->x509 ())) {
404 X509_STORE_free (store);
408 X509_STORE_CTX* ctx = X509_STORE_CTX_new ();
410 X509_STORE_free (store);
414 X509_STORE_set_flags (store, 0);
415 if (!X509_STORE_CTX_init (ctx, store, j->x509 (), 0)) {
416 X509_STORE_CTX_free (ctx);
417 X509_STORE_free (store);
421 int v = X509_verify_cert (ctx);
422 X509_STORE_CTX_free (ctx);
425 X509_STORE_free (store);
430 X509_STORE_free (store);
432 /* Check that the leaf certificate matches the private key, if there is one */
438 BIO* bio = BIO_new_mem_buf (const_cast<char *> (_key->c_str ()), -1);
440 throw MiscError ("could not create memory BIO");
443 RSA* private_key = PEM_read_bio_RSAPrivateKey (bio, 0, 0, 0);
444 RSA* public_key = leaf().public_key ();
445 bool const valid = !BN_cmp (private_key->n, public_key->n);
451 /** @return true if the chain is now in order from root to leaf,
452 * false if no correct order was found.
455 CertificateChain::attempt_reorder ()
457 List original = _certificates;
458 _certificates.sort ();
463 } while (std::next_permutation (_certificates.begin(), _certificates.end ()));
465 _certificates = original;
469 /** Add a <Signer> and <ds:Signature> nodes to an XML node.
470 * @param parent XML node to add to.
471 * @param standard INTEROP or SMPTE.
474 CertificateChain::sign (xmlpp::Element* parent, Standard standard) const
478 xmlpp::Element* signer = parent->add_child("Signer");
479 xmlpp::Element* data = signer->add_child("X509Data", "dsig");
480 xmlpp::Element* serial_element = data->add_child("X509IssuerSerial", "dsig");
481 serial_element->add_child("X509IssuerName", "dsig")->add_child_text (leaf().issuer());
482 serial_element->add_child("X509SerialNumber", "dsig")->add_child_text (leaf().serial());
483 data->add_child("X509SubjectName", "dsig")->add_child_text (leaf().subject());
487 xmlpp::Element* signature = parent->add_child("Signature", "dsig");
489 xmlpp::Element* signed_info = signature->add_child ("SignedInfo", "dsig");
490 signed_info->add_child("CanonicalizationMethod", "dsig")->set_attribute ("Algorithm", "http://www.w3.org/TR/2001/REC-xml-c14n-20010315");
492 if (standard == INTEROP) {
493 signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#rsa-sha1");
495 signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
498 xmlpp::Element* reference = signed_info->add_child("Reference", "dsig");
499 reference->set_attribute ("URI", "");
501 xmlpp::Element* transforms = reference->add_child("Transforms", "dsig");
502 transforms->add_child("Transform", "dsig")->set_attribute (
503 "Algorithm", "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
506 reference->add_child("DigestMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#sha1");
507 /* This will be filled in by the signing later */
508 reference->add_child("DigestValue", "dsig");
510 signature->add_child("SignatureValue", "dsig");
511 signature->add_child("KeyInfo", "dsig");
512 add_signature_value (signature, "dsig");
516 /** Sign an XML node.
518 * @param parent Node to sign.
519 * @param ns Namespace to use for the signature XML nodes.
522 CertificateChain::add_signature_value (xmlpp::Node* parent, string ns) const
524 cxml::Node cp (parent);
525 xmlpp::Node* key_info = cp.node_child("KeyInfo")->node ();
527 /* Add the certificate chain to the KeyInfo child node of parent */
528 BOOST_FOREACH (Certificate const & i, leaf_to_root ()) {
529 xmlpp::Element* data = key_info->add_child("X509Data", ns);
532 xmlpp::Element* serial = data->add_child("X509IssuerSerial", ns);
533 serial->add_child("X509IssuerName", ns)->add_child_text (i.issuer ());
534 serial->add_child("X509SerialNumber", ns)->add_child_text (i.serial ());
537 data->add_child("X509Certificate", ns)->add_child_text (i.certificate());
540 xmlSecDSigCtxPtr signature_context = xmlSecDSigCtxCreate (0);
541 if (signature_context == 0) {
542 throw MiscError ("could not create signature context");
545 signature_context->signKey = xmlSecCryptoAppKeyLoadMemory (
546 reinterpret_cast<const unsigned char *> (_key->c_str()), _key->size(), xmlSecKeyDataFormatPem, 0, 0, 0
549 if (signature_context->signKey == 0) {
550 throw runtime_error ("could not read private key");
553 /* XXX: set key name to the PEM string: this can't be right! */
554 if (xmlSecKeySetName (signature_context->signKey, reinterpret_cast<const xmlChar *> (_key->c_str())) < 0) {
555 throw MiscError ("could not set key name");
558 int const r = xmlSecDSigCtxSign (signature_context, parent->cobj ());
560 throw MiscError (String::compose ("could not sign (%1)", r));
563 xmlSecDSigCtxDestroy (signature_context);
567 CertificateChain::chain () const
570 BOOST_FOREACH (Certificate const &i, root_to_leaf ()) {
571 o += i.certificate(true);