Add method to return a base-64 version of a whole CertificateChain.
[libdcp.git] / src / certificate_chain.cc
1 /*
2     Copyright (C) 2013-2016 Carl Hetherington <cth@carlh.net>
3
4     This file is part of libdcp.
5
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.
10
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.
15
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/>.
18
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
23     including the two.
24
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.
32 */
33
34 /** @file  src/signer_chain.cc
35  *  @brief Functions to make signer chains.
36  */
37
38 #include "certificate_chain.h"
39 #include "exceptions.h"
40 #include "util.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>
57 #include <fstream>
58
59 using std::string;
60 using std::ofstream;
61 using std::ifstream;
62 using std::runtime_error;
63 using namespace dcp;
64
65 /** Run a shell command.
66  *  @param cmd Command to run (UTF8-encoded).
67  */
68 static void
69 command (string cmd)
70 {
71 #ifdef LIBDCP_WINDOWS
72         /* We need to use CreateProcessW on Windows so that the UTF-8/16 mess
73            is handled correctly.
74         */
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) {
78                 delete[] buffer;
79                 return;
80         }
81
82         int code = 1;
83
84         STARTUPINFOW startup_info;
85         memset (&startup_info, 0, sizeof (startup_info));
86         startup_info.cb = sizeof (startup_info);
87         PROCESS_INFORMATION process_info;
88
89         /* XXX: this doesn't actually seem to work; failing commands end up with
90            a return code of 0
91         */
92         if (CreateProcessW (0, buffer, 0, 0, FALSE, CREATE_NO_WINDOW, 0, 0, &startup_info, &process_info)) {
93                 WaitForSingleObject (process_info.hProcess, INFINITE);
94                 DWORD c;
95                 if (GetExitCodeProcess (process_info.hProcess, &c)) {
96                         code = c;
97                 }
98                 CloseHandle (process_info.hProcess);
99                 CloseHandle (process_info.hThread);
100         }
101
102         delete[] buffer;
103 #else
104         cmd += " 2> /dev/null";
105         int const r = system (cmd.c_str ());
106         int const code = WEXITSTATUS (r);
107 #endif
108         if (code) {
109                 locked_stringstream s;
110                 s << "error " << code << " in " << cmd << " within " << boost::filesystem::current_path();
111                 throw dcp::MiscError (s.str());
112         }
113 }
114
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.
119  */
120 static string
121 public_key_digest (boost::filesystem::path private_key, boost::filesystem::path openssl)
122 {
123         boost::filesystem::path public_name = private_key.string() + ".public";
124
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 ());
129
130         /* Read in the public key from the file */
131
132         string pub;
133         ifstream f (public_name.string().c_str ());
134         if (!f.good ()) {
135                 throw dcp::MiscError ("public key not found");
136         }
137
138         bool read = false;
139         while (f.good ()) {
140                 string line;
141                 getline (f, line);
142                 if (line.length() >= 10 && line.substr(0, 10) == "-----BEGIN") {
143                         read = true;
144                 } else if (line.length() >= 8 && line.substr(0, 8) == "-----END") {
145                         break;
146                 } else if (read) {
147                         pub += line;
148                 }
149         }
150
151         /* Decode the base64 of the public key */
152
153         unsigned char buffer[512];
154         int const N = dcp::base64_decode (pub, buffer, 1024);
155
156         /* Hash it with SHA1 (without the first 24 bytes, for reasons that are not entirely clear) */
157
158         SHA_CTX context;
159         if (!SHA1_Init (&context)) {
160                 throw dcp::MiscError ("could not init SHA1 context");
161         }
162
163         if (!SHA1_Update (&context, buffer + 24, N - 24)) {
164                 throw dcp::MiscError ("could not update SHA1 digest");
165         }
166
167         unsigned char digest[SHA_DIGEST_LENGTH];
168         if (!SHA1_Final (digest, &context)) {
169                 throw dcp::MiscError ("could not finish SHA1 digest");
170         }
171
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, "/", "\\/");
176 #else
177         boost::replace_all (dig, "/", "\\\\/");
178 #endif
179         return dig;
180 }
181
182 CertificateChain::CertificateChain (
183         boost::filesystem::path openssl,
184         string organisation,
185         string organisational_unit,
186         string root_common_name,
187         string intermediate_common_name,
188         string leaf_common_name
189         )
190 {
191         boost::filesystem::path directory = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path ();
192         boost::filesystem::create_directories (directory);
193
194         boost::filesystem::path const cwd = boost::filesystem::current_path ();
195         boost::filesystem::current_path (directory);
196
197         string quoted_openssl = "\"" + openssl.string() + "\"";
198
199         command (quoted_openssl + " genrsa -out ca.key 2048");
200
201         {
202                 ofstream f ("ca.cnf");
203                 f << "[ req ]\n"
204                   << "distinguished_name = req_distinguished_name\n"
205                   << "x509_extensions   = v3_ca\n"
206                   << "[ 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";
215         }
216
217         string const ca_subject = "/O=" + organisation +
218                 "/OU=" + organisational_unit +
219                 "/CN=" + root_common_name +
220                 "/dnQualifier=" + public_key_digest ("ca.key", openssl);
221
222         {
223                 locked_stringstream c;
224                 c << quoted_openssl
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());
228         }
229
230         command (quoted_openssl + " genrsa -out intermediate.key 2048");
231
232         {
233                 ofstream f ("intermediate.cnf");
234                 f << "[ default ]\n"
235                   << "distinguished_name = req_distinguished_name\n"
236                   << "x509_extensions = v3_ca\n"
237                   << "[ 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";
246         }
247
248         string const inter_subject = "/O=" + organisation +
249                 "/OU=" + organisational_unit +
250                 "/CN=" + intermediate_common_name +
251                 "/dnQualifier=" + public_key_digest ("intermediate.key", openssl);
252
253         {
254                 locked_stringstream s;
255                 s << quoted_openssl
256                   << " req -new -config intermediate.cnf -days 3649 -subj \"" << inter_subject << "\" -key intermediate.key -out intermediate.csr";
257                 command (s.str().c_str());
258         }
259
260
261         command (
262                 quoted_openssl +
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"
265                 );
266
267         command (quoted_openssl + " genrsa -out leaf.key 2048");
268
269         {
270                 ofstream f ("leaf.cnf");
271                 f << "[ default ]\n"
272                   << "distinguished_name = req_distinguished_name\n"
273                   << "x509_extensions   = v3_ca\n"
274                   << "[ 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";
283         }
284
285         string const leaf_subject = "/O=" + organisation +
286                 "/OU=" + organisational_unit +
287                 "/CN=" + leaf_common_name +
288                 "/dnQualifier=" + public_key_digest ("leaf.key", openssl);
289
290         {
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());
294         }
295
296         command (
297                 quoted_openssl +
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"
300                 );
301
302         boost::filesystem::current_path (cwd);
303
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")));
307
308         _key = dcp::file_to_string (directory / "leaf.key");
309
310         boost::filesystem::remove_all (directory);
311 }
312
313 /** @return Root certificate */
314 Certificate
315 CertificateChain::root () const
316 {
317         DCP_ASSERT (!_certificates.empty());
318         return _certificates.front ();
319 }
320
321 /** @return Leaf certificate */
322 Certificate
323 CertificateChain::leaf () const
324 {
325         DCP_ASSERT (_certificates.size() >= 2);
326         return _certificates.back ();
327 }
328
329 /** @return Certificates in order from root to leaf */
330 CertificateChain::List
331 CertificateChain::root_to_leaf () const
332 {
333         return _certificates;
334 }
335
336 /** @return Certificates in order from leaf to root */
337 CertificateChain::List
338 CertificateChain::leaf_to_root () const
339 {
340         List c = _certificates;
341         c.reverse ();
342         return c;
343 }
344
345 /** Add a certificate to the end of the chain.
346  *  @param c Certificate to add.
347  */
348 void
349 CertificateChain::add (Certificate c)
350 {
351         _certificates.push_back (c);
352 }
353
354 /** Remove a certificate from the chain.
355  *  @param c Certificate to remove.
356  */
357 void
358 CertificateChain::remove (Certificate c)
359 {
360         _certificates.remove (c);
361 }
362
363 /** Remove the i'th certificate in the list, as listed
364  *  from root to leaf.
365  */
366 void
367 CertificateChain::remove (int i)
368 {
369         List::iterator j = _certificates.begin ();
370         while (j != _certificates.end () && i > 0) {
371                 --i;
372                 ++j;
373         }
374
375         if (j != _certificates.end ()) {
376                 _certificates.erase (j);
377         }
378 }
379
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
382  *  leaf certificate.
383  *  @return true if it's ok, false if not.
384  */
385 bool
386 CertificateChain::valid () const
387 {
388         /* Check the certificate chain */
389
390         X509_STORE* store = X509_STORE_new ();
391         if (!store) {
392                 return false;
393         }
394
395         for (List::const_iterator i = _certificates.begin(); i != _certificates.end(); ++i) {
396
397                 List::const_iterator j = i;
398                 ++j;
399                 if (j ==  _certificates.end ()) {
400                         break;
401                 }
402
403                 if (!X509_STORE_add_cert (store, i->x509 ())) {
404                         X509_STORE_free (store);
405                         return false;
406                 }
407
408                 X509_STORE_CTX* ctx = X509_STORE_CTX_new ();
409                 if (!ctx) {
410                         X509_STORE_free (store);
411                         return false;
412                 }
413
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);
418                         return false;
419                 }
420
421                 int v = X509_verify_cert (ctx);
422                 X509_STORE_CTX_free (ctx);
423
424                 if (v == 0) {
425                         X509_STORE_free (store);
426                         return false;
427                 }
428         }
429
430         X509_STORE_free (store);
431
432         /* Check that the leaf certificate matches the private key, if there is one */
433
434         if (!_key) {
435                 return true;
436         }
437
438         BIO* bio = BIO_new_mem_buf (const_cast<char *> (_key->c_str ()), -1);
439         if (!bio) {
440                 throw MiscError ("could not create memory BIO");
441         }
442
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);
446         BIO_free (bio);
447
448         return valid;
449 }
450
451 /** @return true if the chain is now in order from root to leaf,
452  *  false if no correct order was found.
453  */
454 bool
455 CertificateChain::attempt_reorder ()
456 {
457         List original = _certificates;
458         _certificates.sort ();
459         do {
460                 if (valid ()) {
461                         return true;
462                 }
463         } while (std::next_permutation (_certificates.begin(), _certificates.end ()));
464
465         _certificates = original;
466         return false;
467 }
468
469 /** Add a &lt;Signer&gt; and &lt;ds:Signature&gt; nodes to an XML node.
470  *  @param parent XML node to add to.
471  *  @param standard INTEROP or SMPTE.
472  */
473 void
474 CertificateChain::sign (xmlpp::Element* parent, Standard standard) const
475 {
476         /* <Signer> */
477
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());
484
485         /* <Signature> */
486
487         xmlpp::Element* signature = parent->add_child("Signature", "dsig");
488
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");
491
492         if (standard == INTEROP) {
493                 signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#rsa-sha1");
494         } else {
495                 signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
496         }
497
498         xmlpp::Element* reference = signed_info->add_child("Reference", "dsig");
499         reference->set_attribute ("URI", "");
500
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"
504                 );
505
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");
509
510         signature->add_child("SignatureValue", "dsig");
511         signature->add_child("KeyInfo", "dsig");
512         add_signature_value (signature, "dsig");
513 }
514
515
516 /** Sign an XML node.
517  *
518  *  @param parent Node to sign.
519  *  @param ns Namespace to use for the signature XML nodes.
520  */
521 void
522 CertificateChain::add_signature_value (xmlpp::Node* parent, string ns) const
523 {
524         cxml::Node cp (parent);
525         xmlpp::Node* key_info = cp.node_child("KeyInfo")->node ();
526
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);
530
531                 {
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 ());
535                 }
536
537                 data->add_child("X509Certificate", ns)->add_child_text (i.certificate());
538         }
539
540         xmlSecDSigCtxPtr signature_context = xmlSecDSigCtxCreate (0);
541         if (signature_context == 0) {
542                 throw MiscError ("could not create signature context");
543         }
544
545         signature_context->signKey = xmlSecCryptoAppKeyLoadMemory (
546                 reinterpret_cast<const unsigned char *> (_key->c_str()), _key->size(), xmlSecKeyDataFormatPem, 0, 0, 0
547                 );
548
549         if (signature_context->signKey == 0) {
550                 throw runtime_error ("could not read private key");
551         }
552
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");
556         }
557
558         int const r = xmlSecDSigCtxSign (signature_context, parent->cobj ());
559         if (r < 0) {
560                 throw MiscError (String::compose ("could not sign (%1)", r));
561         }
562
563         xmlSecDSigCtxDestroy (signature_context);
564 }
565
566 string
567 CertificateChain::chain () const
568 {
569         string o;
570         BOOST_FOREACH (Certificate const &i, root_to_leaf ()) {
571                 o += i.certificate(true);
572         }
573
574         return o;
575 }