Add Reader classes to permit much more efficient DCP reading.
[libdcp.git] / src / certificate_chain.cc
1 /*
2     Copyright (C) 2013-2015 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 */
20
21 /** @file  src/signer_chain.cc
22  *  @brief Functions to make signer chains.
23  */
24
25 #include "certificate_chain.h"
26 #include "exceptions.h"
27 #include "util.h"
28 #include "dcp_assert.h"
29 #include "KM_util.h"
30 #include "compose.hpp"
31 #include <libcxml/cxml.h>
32 #include <libxml++/libxml++.h>
33 #include <xmlsec/xmldsig.h>
34 #include <xmlsec/dl.h>
35 #include <xmlsec/app.h>
36 #include <xmlsec/crypto.h>
37 #include <openssl/sha.h>
38 #include <openssl/bio.h>
39 #include <openssl/evp.h>
40 #include <openssl/pem.h>
41 #include <boost/filesystem.hpp>
42 #include <boost/algorithm/string.hpp>
43 #include <boost/foreach.hpp>
44 #include <fstream>
45 #include <sstream>
46
47 using std::string;
48 using std::ofstream;
49 using std::ifstream;
50 using std::runtime_error;
51 using std::stringstream;
52 using namespace dcp;
53
54 /** Run a shell command.
55  *  @param cmd Command to run (UTF8-encoded).
56  */
57 static void
58 command (string cmd)
59 {
60 #ifdef LIBDCP_WINDOWS
61         /* We need to use CreateProcessW on Windows so that the UTF-8/16 mess
62            is handled correctly.
63         */
64         int const wn = MultiByteToWideChar (CP_UTF8, 0, cmd.c_str(), -1, 0, 0);
65         wchar_t* buffer = new wchar_t[wn];
66         if (MultiByteToWideChar (CP_UTF8, 0, cmd.c_str(), -1, buffer, wn) == 0) {
67                 delete[] buffer;
68                 return;
69         }
70
71         int code = 1;
72
73         STARTUPINFOW startup_info;
74         memset (&startup_info, 0, sizeof (startup_info));
75         startup_info.cb = sizeof (startup_info);
76         PROCESS_INFORMATION process_info;
77
78         /* XXX: this doesn't actually seem to work; failing commands end up with
79            a return code of 0
80         */
81         if (CreateProcessW (0, buffer, 0, 0, FALSE, CREATE_NO_WINDOW, 0, 0, &startup_info, &process_info)) {
82                 WaitForSingleObject (process_info.hProcess, INFINITE);
83                 DWORD c;
84                 if (GetExitCodeProcess (process_info.hProcess, &c)) {
85                         code = c;
86                 }
87                 CloseHandle (process_info.hProcess);
88                 CloseHandle (process_info.hThread);
89         }
90
91         delete[] buffer;
92 #else
93         cmd += " 2> /dev/null";
94         int const r = system (cmd.c_str ());
95         int const code = WEXITSTATUS (r);
96 #endif
97         if (code) {
98                 stringstream s;
99                 s << "error " << code << " in " << cmd << " within " << boost::filesystem::current_path();
100                 throw dcp::MiscError (s.str());
101         }
102 }
103
104 /** Extract a public key from a private key and create a SHA1 digest of it.
105  *  @param private_key Private key
106  *  @param openssl openssl binary name (or full path if openssl is not on the system path).
107  *  @return SHA1 digest of corresponding public key, with escaped / characters.
108  */
109 static string
110 public_key_digest (boost::filesystem::path private_key, boost::filesystem::path openssl)
111 {
112         boost::filesystem::path public_name = private_key.string() + ".public";
113
114         /* Create the public key from the private key */
115         stringstream s;
116         s << "\"" << openssl.string() << "\" rsa -outform PEM -pubout -in " << private_key.string() << " -out " << public_name.string ();
117         command (s.str().c_str ());
118
119         /* Read in the public key from the file */
120
121         string pub;
122         ifstream f (public_name.string().c_str ());
123         if (!f.good ()) {
124                 throw dcp::MiscError ("public key not found");
125         }
126
127         bool read = false;
128         while (f.good ()) {
129                 string line;
130                 getline (f, line);
131                 if (line.length() >= 10 && line.substr(0, 10) == "-----BEGIN") {
132                         read = true;
133                 } else if (line.length() >= 8 && line.substr(0, 8) == "-----END") {
134                         break;
135                 } else if (read) {
136                         pub += line;
137                 }
138         }
139
140         /* Decode the base64 of the public key */
141
142         unsigned char buffer[512];
143         int const N = dcp::base64_decode (pub, buffer, 1024);
144
145         /* Hash it with SHA1 (without the first 24 bytes, for reasons that are not entirely clear) */
146
147         SHA_CTX context;
148         if (!SHA1_Init (&context)) {
149                 throw dcp::MiscError ("could not init SHA1 context");
150         }
151
152         if (!SHA1_Update (&context, buffer + 24, N - 24)) {
153                 throw dcp::MiscError ("could not update SHA1 digest");
154         }
155
156         unsigned char digest[SHA_DIGEST_LENGTH];
157         if (!SHA1_Final (digest, &context)) {
158                 throw dcp::MiscError ("could not finish SHA1 digest");
159         }
160
161         char digest_base64[64];
162         string dig = Kumu::base64encode (digest, SHA_DIGEST_LENGTH, digest_base64, 64);
163 #ifdef LIBDCP_WINDOWS
164         boost::replace_all (dig, "/", "\\/");
165 #else
166         boost::replace_all (dig, "/", "\\\\/");
167 #endif
168         return dig;
169 }
170
171 CertificateChain::CertificateChain (
172         boost::filesystem::path openssl,
173         string organisation,
174         string organisational_unit,
175         string root_common_name,
176         string intermediate_common_name,
177         string leaf_common_name
178         )
179 {
180         boost::filesystem::path directory = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path ();
181         boost::filesystem::create_directories (directory);
182
183         boost::filesystem::path const cwd = boost::filesystem::current_path ();
184         boost::filesystem::current_path (directory);
185
186         string quoted_openssl = "\"" + openssl.string() + "\"";
187
188         command (quoted_openssl + " genrsa -out ca.key 2048");
189
190         {
191                 ofstream f ("ca.cnf");
192                 f << "[ req ]\n"
193                   << "distinguished_name = req_distinguished_name\n"
194                   << "x509_extensions   = v3_ca\n"
195                   << "[ v3_ca ]\n"
196                   << "basicConstraints = critical,CA:true,pathlen:3\n"
197                   << "keyUsage = keyCertSign,cRLSign\n"
198                   << "subjectKeyIdentifier = hash\n"
199                   << "authorityKeyIdentifier = keyid:always,issuer:always\n"
200                   << "[ req_distinguished_name ]\n"
201                   << "O = Unique organization name\n"
202                   << "OU = Organization unit\n"
203                   << "CN = Entity and dnQualifier\n";
204         }
205
206         string const ca_subject = "/O=" + organisation +
207                 "/OU=" + organisational_unit +
208                 "/CN=" + root_common_name +
209                 "/dnQualifier=" + public_key_digest ("ca.key", openssl);
210
211         {
212                 stringstream c;
213                 c << quoted_openssl
214                   << " req -new -x509 -sha256 -config ca.cnf -days 3650 -set_serial 5"
215                   << " -subj \"" << ca_subject << "\" -key ca.key -outform PEM -out ca.self-signed.pem";
216                 command (c.str().c_str());
217         }
218
219         command (quoted_openssl + " genrsa -out intermediate.key 2048");
220
221         {
222                 ofstream f ("intermediate.cnf");
223                 f << "[ default ]\n"
224                   << "distinguished_name = req_distinguished_name\n"
225                   << "x509_extensions = v3_ca\n"
226                   << "[ v3_ca ]\n"
227                   << "basicConstraints = critical,CA:true,pathlen:2\n"
228                   << "keyUsage = keyCertSign,cRLSign\n"
229                   << "subjectKeyIdentifier = hash\n"
230                   << "authorityKeyIdentifier = keyid:always,issuer:always\n"
231                   << "[ req_distinguished_name ]\n"
232                   << "O = Unique organization name\n"
233                   << "OU = Organization unit\n"
234                   << "CN = Entity and dnQualifier\n";
235         }
236
237         string const inter_subject = "/O=" + organisation +
238                 "/OU=" + organisational_unit +
239                 "/CN=" + intermediate_common_name +
240                 "/dnQualifier=" + public_key_digest ("intermediate.key", openssl);
241
242         {
243                 stringstream s;
244                 s << quoted_openssl
245                   << " req -new -config intermediate.cnf -days 3649 -subj \"" << inter_subject << "\" -key intermediate.key -out intermediate.csr";
246                 command (s.str().c_str());
247         }
248
249
250         command (
251                 quoted_openssl +
252                 " x509 -req -sha256 -days 3649 -CA ca.self-signed.pem -CAkey ca.key -set_serial 6"
253                 " -in intermediate.csr -extfile intermediate.cnf -extensions v3_ca -out intermediate.signed.pem"
254                 );
255
256         command (quoted_openssl + " genrsa -out leaf.key 2048");
257
258         {
259                 ofstream f ("leaf.cnf");
260                 f << "[ default ]\n"
261                   << "distinguished_name = req_distinguished_name\n"
262                   << "x509_extensions   = v3_ca\n"
263                   << "[ v3_ca ]\n"
264                   << "basicConstraints = critical,CA:false\n"
265                   << "keyUsage = digitalSignature,keyEncipherment\n"
266                   << "subjectKeyIdentifier = hash\n"
267                   << "authorityKeyIdentifier = keyid,issuer:always\n"
268                   << "[ req_distinguished_name ]\n"
269                   << "O = Unique organization name\n"
270                   << "OU = Organization unit\n"
271                   << "CN = Entity and dnQualifier\n";
272         }
273
274         string const leaf_subject = "/O=" + organisation +
275                 "/OU=" + organisational_unit +
276                 "/CN=" + leaf_common_name +
277                 "/dnQualifier=" + public_key_digest ("leaf.key", openssl);
278
279         {
280                 stringstream s;
281                 s << quoted_openssl << " req -new -config leaf.cnf -days 3648 -subj \"" << leaf_subject << "\" -key leaf.key -outform PEM -out leaf.csr";
282                 command (s.str().c_str());
283         }
284
285         command (
286                 quoted_openssl +
287                 " x509 -req -sha256 -days 3648 -CA intermediate.signed.pem -CAkey intermediate.key"
288                 " -set_serial 7 -in leaf.csr -extfile leaf.cnf -extensions v3_ca -out leaf.signed.pem"
289                 );
290
291         boost::filesystem::current_path (cwd);
292
293         _certificates.push_back (dcp::Certificate (dcp::file_to_string (directory / "ca.self-signed.pem")));
294         _certificates.push_back (dcp::Certificate (dcp::file_to_string (directory / "intermediate.signed.pem")));
295         _certificates.push_back (dcp::Certificate (dcp::file_to_string (directory / "leaf.signed.pem")));
296
297         _key = dcp::file_to_string (directory / "leaf.key");
298
299         boost::filesystem::remove_all (directory);
300 }
301
302 /** @return Root certificate */
303 Certificate
304 CertificateChain::root () const
305 {
306         DCP_ASSERT (!_certificates.empty());
307         return _certificates.front ();
308 }
309
310 /** @return Leaf certificate */
311 Certificate
312 CertificateChain::leaf () const
313 {
314         DCP_ASSERT (_certificates.size() >= 2);
315         return _certificates.back ();
316 }
317
318 /** @return Certificates in order from root to leaf */
319 CertificateChain::List
320 CertificateChain::root_to_leaf () const
321 {
322         return _certificates;
323 }
324
325 /** @return Certificates in order from leaf to root */
326 CertificateChain::List
327 CertificateChain::leaf_to_root () const
328 {
329         List c = _certificates;
330         c.reverse ();
331         return c;
332 }
333
334 /** Add a certificate to the end of the chain.
335  *  @param c Certificate to add.
336  */
337 void
338 CertificateChain::add (Certificate c)
339 {
340         _certificates.push_back (c);
341 }
342
343 /** Remove a certificate from the chain.
344  *  @param c Certificate to remove.
345  */
346 void
347 CertificateChain::remove (Certificate c)
348 {
349         _certificates.remove (c);
350 }
351
352 /** Remove the i'th certificate in the list, as listed
353  *  from root to leaf.
354  */
355 void
356 CertificateChain::remove (int i)
357 {
358         List::iterator j = _certificates.begin ();
359         while (j != _certificates.end () && i > 0) {
360                 --i;
361                 ++j;
362         }
363
364         if (j != _certificates.end ()) {
365                 _certificates.erase (j);
366         }
367 }
368
369 /** Check to see if the chain is valid (i.e. root signs the intermediate, intermediate
370  *  signs the leaf and so on) and that the private key (if there is one) matches the
371  *  leaf certificate.
372  *  @return true if it's ok, false if not.
373  */
374 bool
375 CertificateChain::valid () const
376 {
377         /* Check the certificate chain */
378
379         X509_STORE* store = X509_STORE_new ();
380         if (!store) {
381                 return false;
382         }
383
384         for (List::const_iterator i = _certificates.begin(); i != _certificates.end(); ++i) {
385
386                 List::const_iterator j = i;
387                 ++j;
388                 if (j ==  _certificates.end ()) {
389                         break;
390                 }
391
392                 if (!X509_STORE_add_cert (store, i->x509 ())) {
393                         X509_STORE_free (store);
394                         return false;
395                 }
396
397                 X509_STORE_CTX* ctx = X509_STORE_CTX_new ();
398                 if (!ctx) {
399                         X509_STORE_free (store);
400                         return false;
401                 }
402
403                 X509_STORE_set_flags (store, 0);
404                 if (!X509_STORE_CTX_init (ctx, store, j->x509 (), 0)) {
405                         X509_STORE_CTX_free (ctx);
406                         X509_STORE_free (store);
407                         return false;
408                 }
409
410                 int v = X509_verify_cert (ctx);
411                 X509_STORE_CTX_free (ctx);
412
413                 if (v == 0) {
414                         X509_STORE_free (store);
415                         return false;
416                 }
417         }
418
419         X509_STORE_free (store);
420
421         /* Check that the leaf certificate matches the private key, if there is one */
422
423         if (!_key) {
424                 return true;
425         }
426
427         BIO* bio = BIO_new_mem_buf (const_cast<char *> (_key->c_str ()), -1);
428         if (!bio) {
429                 throw MiscError ("could not create memory BIO");
430         }
431
432         RSA* private_key = PEM_read_bio_RSAPrivateKey (bio, 0, 0, 0);
433         RSA* public_key = leaf().public_key ();
434         bool const valid = !BN_cmp (private_key->n, public_key->n);
435         BIO_free (bio);
436
437         return valid;
438 }
439
440 /** @return true if the chain is now in order from root to leaf,
441  *  false if no correct order was found.
442  */
443 bool
444 CertificateChain::attempt_reorder ()
445 {
446         List original = _certificates;
447         _certificates.sort ();
448         do {
449                 if (valid ()) {
450                         return true;
451                 }
452         } while (std::next_permutation (_certificates.begin(), _certificates.end ()));
453
454         _certificates = original;
455         return false;
456 }
457
458 /** Add a &lt;Signer&gt; and &lt;ds:Signature&gt; nodes to an XML node.
459  *  @param parent XML node to add to.
460  *  @param standard INTEROP or SMPTE.
461  */
462 void
463 CertificateChain::sign (xmlpp::Element* parent, Standard standard) const
464 {
465         /* <Signer> */
466
467         xmlpp::Element* signer = parent->add_child("Signer");
468         xmlpp::Element* data = signer->add_child("X509Data", "dsig");
469         xmlpp::Element* serial_element = data->add_child("X509IssuerSerial", "dsig");
470         serial_element->add_child("X509IssuerName", "dsig")->add_child_text (leaf().issuer());
471         serial_element->add_child("X509SerialNumber", "dsig")->add_child_text (leaf().serial());
472         data->add_child("X509SubjectName", "dsig")->add_child_text (leaf().subject());
473
474         /* <Signature> */
475
476         xmlpp::Element* signature = parent->add_child("Signature", "dsig");
477
478         xmlpp::Element* signed_info = signature->add_child ("SignedInfo", "dsig");
479         signed_info->add_child("CanonicalizationMethod", "dsig")->set_attribute ("Algorithm", "http://www.w3.org/TR/2001/REC-xml-c14n-20010315");
480
481         if (standard == INTEROP) {
482                 signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#rsa-sha1");
483         } else {
484                 signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
485         }
486
487         xmlpp::Element* reference = signed_info->add_child("Reference", "dsig");
488         reference->set_attribute ("URI", "");
489
490         xmlpp::Element* transforms = reference->add_child("Transforms", "dsig");
491         transforms->add_child("Transform", "dsig")->set_attribute (
492                 "Algorithm", "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
493                 );
494
495         reference->add_child("DigestMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#sha1");
496         /* This will be filled in by the signing later */
497         reference->add_child("DigestValue", "dsig");
498
499         signature->add_child("SignatureValue", "dsig");
500         signature->add_child("KeyInfo", "dsig");
501         add_signature_value (signature, "dsig");
502 }
503
504
505 /** Sign an XML node.
506  *
507  *  @param parent Node to sign.
508  *  @param ns Namespace to use for the signature XML nodes.
509  */
510 void
511 CertificateChain::add_signature_value (xmlpp::Node* parent, string ns) const
512 {
513         cxml::Node cp (parent);
514         xmlpp::Node* key_info = cp.node_child("KeyInfo")->node ();
515
516         /* Add the certificate chain to the KeyInfo child node of parent */
517         CertificateChain::List c = leaf_to_root ();
518         BOOST_FOREACH (Certificate const & i, leaf_to_root ()) {
519                 xmlpp::Element* data = key_info->add_child("X509Data", ns);
520
521                 {
522                         xmlpp::Element* serial = data->add_child("X509IssuerSerial", ns);
523                         serial->add_child("X509IssuerName", ns)->add_child_text (i.issuer ());
524                         serial->add_child("X509SerialNumber", ns)->add_child_text (i.serial ());
525                 }
526
527                 data->add_child("X509Certificate", ns)->add_child_text (i.certificate());
528         }
529
530         xmlSecDSigCtxPtr signature_context = xmlSecDSigCtxCreate (0);
531         if (signature_context == 0) {
532                 throw MiscError ("could not create signature context");
533         }
534
535         signature_context->signKey = xmlSecCryptoAppKeyLoadMemory (
536                 reinterpret_cast<const unsigned char *> (_key->c_str()), _key->size(), xmlSecKeyDataFormatPem, 0, 0, 0
537                 );
538
539         if (signature_context->signKey == 0) {
540                 throw runtime_error ("could not read private key");
541         }
542
543         /* XXX: set key name to the PEM string: this can't be right! */
544         if (xmlSecKeySetName (signature_context->signKey, reinterpret_cast<const xmlChar *> (_key->c_str())) < 0) {
545                 throw MiscError ("could not set key name");
546         }
547
548         int const r = xmlSecDSigCtxSign (signature_context, parent->cobj ());
549         if (r < 0) {
550                 throw MiscError (String::compose ("could not sign (%1)", r));
551         }
552
553         xmlSecDSigCtxDestroy (signature_context);
554 }