2 Copyright (C) 2013-2015 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;
64 using std::stringstream;
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);
112 s << "error " << code << " in " << cmd << " within " << boost::filesystem::current_path();
113 throw dcp::MiscError (s.str());
117 /** Extract a public key from a private key and create a SHA1 digest of it.
118 * @param private_key Private key
119 * @param openssl openssl binary name (or full path if openssl is not on the system path).
120 * @return SHA1 digest of corresponding public key, with escaped / characters.
123 public_key_digest (boost::filesystem::path private_key, boost::filesystem::path openssl)
125 boost::filesystem::path public_name = private_key.string() + ".public";
127 /* Create the public key from the private key */
129 s << "\"" << openssl.string() << "\" rsa -outform PEM -pubout -in " << private_key.string() << " -out " << public_name.string ();
130 command (s.str().c_str ());
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, "/", "\\\\/");
184 CertificateChain::CertificateChain (
185 boost::filesystem::path openssl,
187 string organisational_unit,
188 string root_common_name,
189 string intermediate_common_name,
190 string leaf_common_name
193 boost::filesystem::path directory = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path ();
194 boost::filesystem::create_directories (directory);
196 boost::filesystem::path const cwd = boost::filesystem::current_path ();
197 boost::filesystem::current_path (directory);
199 string quoted_openssl = "\"" + openssl.string() + "\"";
201 command (quoted_openssl + " genrsa -out ca.key 2048");
204 ofstream f ("ca.cnf");
206 << "distinguished_name = req_distinguished_name\n"
207 << "x509_extensions = v3_ca\n"
209 << "basicConstraints = critical,CA:true,pathlen:3\n"
210 << "keyUsage = keyCertSign,cRLSign\n"
211 << "subjectKeyIdentifier = hash\n"
212 << "authorityKeyIdentifier = keyid:always,issuer:always\n"
213 << "[ req_distinguished_name ]\n"
214 << "O = Unique organization name\n"
215 << "OU = Organization unit\n"
216 << "CN = Entity and dnQualifier\n";
219 string const ca_subject = "/O=" + organisation +
220 "/OU=" + organisational_unit +
221 "/CN=" + root_common_name +
222 "/dnQualifier=" + public_key_digest ("ca.key", openssl);
227 << " req -new -x509 -sha256 -config ca.cnf -days 3650 -set_serial 5"
228 << " -subj \"" << ca_subject << "\" -key ca.key -outform PEM -out ca.self-signed.pem";
229 command (c.str().c_str());
232 command (quoted_openssl + " genrsa -out intermediate.key 2048");
235 ofstream f ("intermediate.cnf");
237 << "distinguished_name = req_distinguished_name\n"
238 << "x509_extensions = v3_ca\n"
240 << "basicConstraints = critical,CA:true,pathlen:2\n"
241 << "keyUsage = keyCertSign,cRLSign\n"
242 << "subjectKeyIdentifier = hash\n"
243 << "authorityKeyIdentifier = keyid:always,issuer:always\n"
244 << "[ req_distinguished_name ]\n"
245 << "O = Unique organization name\n"
246 << "OU = Organization unit\n"
247 << "CN = Entity and dnQualifier\n";
250 string const inter_subject = "/O=" + organisation +
251 "/OU=" + organisational_unit +
252 "/CN=" + intermediate_common_name +
253 "/dnQualifier=" + public_key_digest ("intermediate.key", openssl);
258 << " req -new -config intermediate.cnf -days 3649 -subj \"" << inter_subject << "\" -key intermediate.key -out intermediate.csr";
259 command (s.str().c_str());
265 " x509 -req -sha256 -days 3649 -CA ca.self-signed.pem -CAkey ca.key -set_serial 6"
266 " -in intermediate.csr -extfile intermediate.cnf -extensions v3_ca -out intermediate.signed.pem"
269 command (quoted_openssl + " genrsa -out leaf.key 2048");
272 ofstream f ("leaf.cnf");
274 << "distinguished_name = req_distinguished_name\n"
275 << "x509_extensions = v3_ca\n"
277 << "basicConstraints = critical,CA:false\n"
278 << "keyUsage = digitalSignature,keyEncipherment\n"
279 << "subjectKeyIdentifier = hash\n"
280 << "authorityKeyIdentifier = keyid,issuer:always\n"
281 << "[ req_distinguished_name ]\n"
282 << "O = Unique organization name\n"
283 << "OU = Organization unit\n"
284 << "CN = Entity and dnQualifier\n";
287 string const leaf_subject = "/O=" + organisation +
288 "/OU=" + organisational_unit +
289 "/CN=" + leaf_common_name +
290 "/dnQualifier=" + public_key_digest ("leaf.key", openssl);
294 s << quoted_openssl << " req -new -config leaf.cnf -days 3648 -subj \"" << leaf_subject << "\" -key leaf.key -outform PEM -out leaf.csr";
295 command (s.str().c_str());
300 " x509 -req -sha256 -days 3648 -CA intermediate.signed.pem -CAkey intermediate.key"
301 " -set_serial 7 -in leaf.csr -extfile leaf.cnf -extensions v3_ca -out leaf.signed.pem"
304 boost::filesystem::current_path (cwd);
306 _certificates.push_back (dcp::Certificate (dcp::file_to_string (directory / "ca.self-signed.pem")));
307 _certificates.push_back (dcp::Certificate (dcp::file_to_string (directory / "intermediate.signed.pem")));
308 _certificates.push_back (dcp::Certificate (dcp::file_to_string (directory / "leaf.signed.pem")));
310 _key = dcp::file_to_string (directory / "leaf.key");
312 boost::filesystem::remove_all (directory);
315 /** @return Root certificate */
317 CertificateChain::root () const
319 DCP_ASSERT (!_certificates.empty());
320 return _certificates.front ();
323 /** @return Leaf certificate */
325 CertificateChain::leaf () const
327 DCP_ASSERT (_certificates.size() >= 2);
328 return _certificates.back ();
331 /** @return Certificates in order from root to leaf */
332 CertificateChain::List
333 CertificateChain::root_to_leaf () const
335 return _certificates;
338 /** @return Certificates in order from leaf to root */
339 CertificateChain::List
340 CertificateChain::leaf_to_root () const
342 List c = _certificates;
347 /** Add a certificate to the end of the chain.
348 * @param c Certificate to add.
351 CertificateChain::add (Certificate c)
353 _certificates.push_back (c);
356 /** Remove a certificate from the chain.
357 * @param c Certificate to remove.
360 CertificateChain::remove (Certificate c)
362 _certificates.remove (c);
365 /** Remove the i'th certificate in the list, as listed
369 CertificateChain::remove (int i)
371 List::iterator j = _certificates.begin ();
372 while (j != _certificates.end () && i > 0) {
377 if (j != _certificates.end ()) {
378 _certificates.erase (j);
382 /** Check to see if the chain is valid (i.e. root signs the intermediate, intermediate
383 * signs the leaf and so on) and that the private key (if there is one) matches the
385 * @return true if it's ok, false if not.
388 CertificateChain::valid () const
390 /* Check the certificate chain */
392 X509_STORE* store = X509_STORE_new ();
397 for (List::const_iterator i = _certificates.begin(); i != _certificates.end(); ++i) {
399 List::const_iterator j = i;
401 if (j == _certificates.end ()) {
405 if (!X509_STORE_add_cert (store, i->x509 ())) {
406 X509_STORE_free (store);
410 X509_STORE_CTX* ctx = X509_STORE_CTX_new ();
412 X509_STORE_free (store);
416 X509_STORE_set_flags (store, 0);
417 if (!X509_STORE_CTX_init (ctx, store, j->x509 (), 0)) {
418 X509_STORE_CTX_free (ctx);
419 X509_STORE_free (store);
423 int v = X509_verify_cert (ctx);
424 X509_STORE_CTX_free (ctx);
427 X509_STORE_free (store);
432 X509_STORE_free (store);
434 /* Check that the leaf certificate matches the private key, if there is one */
440 BIO* bio = BIO_new_mem_buf (const_cast<char *> (_key->c_str ()), -1);
442 throw MiscError ("could not create memory BIO");
445 RSA* private_key = PEM_read_bio_RSAPrivateKey (bio, 0, 0, 0);
446 RSA* public_key = leaf().public_key ();
447 bool const valid = !BN_cmp (private_key->n, public_key->n);
453 /** @return true if the chain is now in order from root to leaf,
454 * false if no correct order was found.
457 CertificateChain::attempt_reorder ()
459 List original = _certificates;
460 _certificates.sort ();
465 } while (std::next_permutation (_certificates.begin(), _certificates.end ()));
467 _certificates = original;
471 /** Add a <Signer> and <ds:Signature> nodes to an XML node.
472 * @param parent XML node to add to.
473 * @param standard INTEROP or SMPTE.
476 CertificateChain::sign (xmlpp::Element* parent, Standard standard) const
480 xmlpp::Element* signer = parent->add_child("Signer");
481 xmlpp::Element* data = signer->add_child("X509Data", "dsig");
482 xmlpp::Element* serial_element = data->add_child("X509IssuerSerial", "dsig");
483 serial_element->add_child("X509IssuerName", "dsig")->add_child_text (leaf().issuer());
484 serial_element->add_child("X509SerialNumber", "dsig")->add_child_text (leaf().serial());
485 data->add_child("X509SubjectName", "dsig")->add_child_text (leaf().subject());
489 xmlpp::Element* signature = parent->add_child("Signature", "dsig");
491 xmlpp::Element* signed_info = signature->add_child ("SignedInfo", "dsig");
492 signed_info->add_child("CanonicalizationMethod", "dsig")->set_attribute ("Algorithm", "http://www.w3.org/TR/2001/REC-xml-c14n-20010315");
494 if (standard == INTEROP) {
495 signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#rsa-sha1");
497 signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
500 xmlpp::Element* reference = signed_info->add_child("Reference", "dsig");
501 reference->set_attribute ("URI", "");
503 xmlpp::Element* transforms = reference->add_child("Transforms", "dsig");
504 transforms->add_child("Transform", "dsig")->set_attribute (
505 "Algorithm", "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
508 reference->add_child("DigestMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#sha1");
509 /* This will be filled in by the signing later */
510 reference->add_child("DigestValue", "dsig");
512 signature->add_child("SignatureValue", "dsig");
513 signature->add_child("KeyInfo", "dsig");
514 add_signature_value (signature, "dsig");
518 /** Sign an XML node.
520 * @param parent Node to sign.
521 * @param ns Namespace to use for the signature XML nodes.
524 CertificateChain::add_signature_value (xmlpp::Node* parent, string ns) const
526 cxml::Node cp (parent);
527 xmlpp::Node* key_info = cp.node_child("KeyInfo")->node ();
529 /* Add the certificate chain to the KeyInfo child node of parent */
530 BOOST_FOREACH (Certificate const & i, leaf_to_root ()) {
531 xmlpp::Element* data = key_info->add_child("X509Data", ns);
534 xmlpp::Element* serial = data->add_child("X509IssuerSerial", ns);
535 serial->add_child("X509IssuerName", ns)->add_child_text (i.issuer ());
536 serial->add_child("X509SerialNumber", ns)->add_child_text (i.serial ());
539 data->add_child("X509Certificate", ns)->add_child_text (i.certificate());
542 xmlSecDSigCtxPtr signature_context = xmlSecDSigCtxCreate (0);
543 if (signature_context == 0) {
544 throw MiscError ("could not create signature context");
547 signature_context->signKey = xmlSecCryptoAppKeyLoadMemory (
548 reinterpret_cast<const unsigned char *> (_key->c_str()), _key->size(), xmlSecKeyDataFormatPem, 0, 0, 0
551 if (signature_context->signKey == 0) {
552 throw runtime_error ("could not read private key");
555 /* XXX: set key name to the PEM string: this can't be right! */
556 if (xmlSecKeySetName (signature_context->signKey, reinterpret_cast<const xmlChar *> (_key->c_str())) < 0) {
557 throw MiscError ("could not set key name");
560 int const r = xmlSecDSigCtxSign (signature_context, parent->cobj ());
562 throw MiscError (String::compose ("could not sign (%1)", r));
565 xmlSecDSigCtxDestroy (signature_context);