libdcp
certificate_chain.cc
Go to the documentation of this file.
1 /*
2  Copyright (C) 2013-2021 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 
40 #include "certificate_chain.h"
41 #include "compose.hpp"
42 #include "dcp_assert.h"
43 #include "exceptions.h"
44 #include "filesystem.h"
45 #include "scope_guard.h"
46 #include "util.h"
47 #include "warnings.h"
48 #include <asdcp/KM_util.h>
49 #include <libcxml/cxml.h>
50 LIBDCP_DISABLE_WARNINGS
51 #include <libxml++/libxml++.h>
52 LIBDCP_ENABLE_WARNINGS
53 #include <xmlsec/xmldsig.h>
54 #include <xmlsec/dl.h>
55 #include <xmlsec/app.h>
56 #include <xmlsec/crypto.h>
57 #include <openssl/sha.h>
58 #include <openssl/bio.h>
59 #include <openssl/evp.h>
60 #include <openssl/pem.h>
61 #include <openssl/rsa.h>
62 #include <openssl/x509.h>
63 #include <boost/algorithm/string.hpp>
64 #include <fstream>
65 #include <iostream>
66 
67 
68 using std::string;
69 using std::ofstream;
70 using std::ifstream;
71 using std::runtime_error;
72 using namespace dcp;
73 
74 
78 static void
79 command (string cmd)
80 {
81 #ifdef LIBDCP_WINDOWS
82  /* We need to use CreateProcessW on Windows so that the UTF-8/16 mess
83  is handled correctly.
84  */
85  int const wn = MultiByteToWideChar (CP_UTF8, 0, cmd.c_str(), -1, 0, 0);
86  auto buffer = new wchar_t[wn];
87  if (MultiByteToWideChar (CP_UTF8, 0, cmd.c_str(), -1, buffer, wn) == 0) {
88  delete[] buffer;
89  return;
90  }
91 
92  int code = 1;
93 
94  STARTUPINFOW startup_info;
95  memset (&startup_info, 0, sizeof (startup_info));
96  startup_info.cb = sizeof (startup_info);
97  PROCESS_INFORMATION process_info;
98 
99  /* XXX: this doesn't actually seem to work; failing commands end up with
100  a return code of 0
101  */
102  if (CreateProcessW (0, buffer, 0, 0, FALSE, CREATE_NO_WINDOW, 0, 0, &startup_info, &process_info)) {
103  WaitForSingleObject (process_info.hProcess, INFINITE);
104  DWORD c;
105  if (GetExitCodeProcess (process_info.hProcess, &c)) {
106  code = c;
107  }
108  CloseHandle (process_info.hProcess);
109  CloseHandle (process_info.hThread);
110  }
111 
112  delete[] buffer;
113 #else
114  cmd += " 2> /dev/null";
115  int const r = system (cmd.c_str ());
116  int const code = WEXITSTATUS (r);
117 #endif
118  if (code) {
119  throw dcp::MiscError(String::compose("error %1 in %2 within %3", code, cmd, filesystem::current_path().string()));
120  }
121 }
122 
123 
124 string
125 dcp::public_key_digest(RSA* public_key)
126 {
127  /* Convert public key to DER (binary) format */
128  unsigned char buffer[512];
129  unsigned char* buffer_ptr = buffer;
130  auto length = i2d_RSA_PUBKEY(public_key, &buffer_ptr);
131  if (length < 0) {
132  throw MiscError("Could not convert public key to DER");
133  }
134 
135  /* Hash it with SHA1 (without the first 24 bytes, for reasons that are not entirely clear) */
136 
137  SHA_CTX context;
138  if (!SHA1_Init (&context)) {
139  throw dcp::MiscError ("could not init SHA1 context");
140  }
141 
142  if (!SHA1_Update(&context, buffer + 24, length - 24)) {
143  throw dcp::MiscError ("could not update SHA1 digest");
144  }
145 
146  unsigned char digest[SHA_DIGEST_LENGTH];
147  if (!SHA1_Final (digest, &context)) {
148  throw dcp::MiscError ("could not finish SHA1 digest");
149  }
150 
151  char digest_base64[64];
152  string dig = Kumu::base64encode (digest, SHA_DIGEST_LENGTH, digest_base64, 64);
153  return escape_digest(dig);
154 }
155 
156 
157 string
158 dcp::escape_digest(string digest)
159 {
160  boost::replace_all(digest, "/", "\\/");
161  boost::replace_all(digest, "+", "\\+");
162  return digest;
163 }
164 
165 
171 string
172 dcp::public_key_digest(boost::filesystem::path private_key_file)
173 {
174  auto private_key_string = dcp::file_to_string(private_key_file);
175 
176  /* Read private key into memory */
177  auto private_key_bio = BIO_new_mem_buf(const_cast<char*>(private_key_string.c_str()), -1);
178  if (!private_key_bio) {
179  throw MiscError("Could not create memory BIO");
180  }
181  dcp::ScopeGuard sg_private_key_bio([private_key_bio]() { BIO_free(private_key_bio); });
182 
183  /* Extract private key */
184  auto private_key = PEM_read_bio_PrivateKey(private_key_bio, nullptr, nullptr, nullptr);
185  if (!private_key) {
186  throw MiscError("Could not read private key");
187  }
188  dcp::ScopeGuard sg_private_key([private_key]() { EVP_PKEY_free(private_key); });
189 
190  /* Get public key from private key */
191  auto public_key = EVP_PKEY_get1_RSA(private_key);
192  if (!public_key) {
193  throw MiscError("Could not obtain public key");
194  }
195  dcp::ScopeGuard sg_public_key([public_key]() { RSA_free(public_key); });
196 
197  return public_key_digest(public_key);
198 }
199 
200 
201 CertificateChain::CertificateChain (
202  boost::filesystem::path openssl,
203  int validity_in_days,
204  string organisation,
205  string organisational_unit,
206  string root_common_name,
207  string intermediate_common_name,
208  string leaf_common_name
209  )
210 {
211  auto directory = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path ();
212  filesystem::create_directories(directory);
213 
214  auto const cwd = boost::filesystem::current_path();
215  /* On Windows we will use cmd.exe here, and that doesn't work with UNC paths, so make sure
216  * we don't use our own filesystem::current_path() as it will make the current working
217  * directory a UNC path.
218  */
219  boost::filesystem::current_path(directory);
220 
221  string quoted_openssl = "\"" + openssl.string() + "\"";
222 
223  command (quoted_openssl + " genrsa -out ca.key 2048");
224 
225  {
226  ofstream f ("ca.cnf");
227  f << "[ req ]\n"
228  << "distinguished_name = req_distinguished_name\n"
229  << "x509_extensions = v3_ca\n"
230  << "string_mask = nombstr\n"
231  << "[ v3_ca ]\n"
232  << "basicConstraints = critical,CA:true,pathlen:3\n"
233  << "keyUsage = keyCertSign,cRLSign\n"
234  << "subjectKeyIdentifier = hash\n"
235  << "authorityKeyIdentifier = keyid:always,issuer:always\n"
236  << "[ req_distinguished_name ]\n"
237  << "O = Unique organization name\n"
238  << "OU = Organization unit\n"
239  << "CN = Entity and dnQualifier\n";
240  }
241 
242  string const ca_subject = "/O=" + organisation +
243  "/OU=" + organisational_unit +
244  "/CN=" + root_common_name +
245  "/dnQualifier=" + public_key_digest ("ca.key");
246 
247  {
248  command (
249  String::compose (
250  "%1 req -new -x509 -sha256 -config ca.cnf -days %2 -set_serial 5"
251  " -subj \"%3\" -key ca.key -outform PEM -out ca.self-signed.pem",
252  quoted_openssl, validity_in_days, ca_subject
253  )
254  );
255  }
256 
257  command (quoted_openssl + " genrsa -out intermediate.key 2048");
258 
259  {
260  ofstream f ("intermediate.cnf");
261  f << "[ default ]\n"
262  << "distinguished_name = req_distinguished_name\n"
263  << "x509_extensions = v3_ca\n"
264  << "string_mask = nombstr\n"
265  << "[ v3_ca ]\n"
266  << "basicConstraints = critical,CA:true,pathlen:2\n"
267  << "keyUsage = keyCertSign,cRLSign\n"
268  << "subjectKeyIdentifier = hash\n"
269  << "authorityKeyIdentifier = keyid:always,issuer:always\n"
270  << "[ req_distinguished_name ]\n"
271  << "O = Unique organization name\n"
272  << "OU = Organization unit\n"
273  << "CN = Entity and dnQualifier\n";
274  }
275 
276  string const inter_subject = "/O=" + organisation +
277  "/OU=" + organisational_unit +
278  "/CN=" + intermediate_common_name +
279  "/dnQualifier=" + public_key_digest ("intermediate.key");
280 
281  {
282  command (
283  String::compose (
284  "%1 req -new -config intermediate.cnf -days %2 -subj \"%3\" -key intermediate.key -out intermediate.csr",
285  quoted_openssl, validity_in_days - 1, inter_subject
286  )
287  );
288  }
289 
290  command (
291  String::compose (
292  "%1 x509 -req -sha256 -days %2 -CA ca.self-signed.pem -CAkey ca.key -set_serial 6"
293  " -in intermediate.csr -extfile intermediate.cnf -extensions v3_ca -out intermediate.signed.pem",
294  quoted_openssl, validity_in_days - 1
295  )
296  );
297 
298  command (quoted_openssl + " genrsa -out leaf.key 2048");
299 
300  {
301  ofstream f ("leaf.cnf");
302  f << "[ default ]\n"
303  << "distinguished_name = req_distinguished_name\n"
304  << "x509_extensions = v3_ca\n"
305  << "string_mask = nombstr\n"
306  << "[ v3_ca ]\n"
307  << "basicConstraints = critical,CA:false\n"
308  << "keyUsage = digitalSignature,keyEncipherment\n"
309  << "subjectKeyIdentifier = hash\n"
310  << "authorityKeyIdentifier = keyid,issuer:always\n"
311  << "[ req_distinguished_name ]\n"
312  << "O = Unique organization name\n"
313  << "OU = Organization unit\n"
314  << "CN = Entity and dnQualifier\n";
315  }
316 
317  string const leaf_subject = "/O=" + organisation +
318  "/OU=" + organisational_unit +
319  "/CN=" + leaf_common_name +
320  "/dnQualifier=" + public_key_digest ("leaf.key");
321 
322  {
323  command (
324  String::compose (
325  "%1 req -new -config leaf.cnf -days %2 -subj \"%3\" -key leaf.key -outform PEM -out leaf.csr",
326  quoted_openssl, validity_in_days - 2, leaf_subject
327  )
328  );
329  }
330 
331  command (
332  String::compose (
333  "%1 x509 -req -sha256 -days %2 -CA intermediate.signed.pem -CAkey intermediate.key"
334  " -set_serial 7 -in leaf.csr -extfile leaf.cnf -extensions v3_ca -out leaf.signed.pem",
335  quoted_openssl, validity_in_days - 2
336  )
337  );
338 
339  /* Use boost:: rather than dcp:: here so we don't force UNC into the current path if it
340  * wasn't there before.
341  */
342  boost::filesystem::current_path(cwd);
343 
344  _certificates.push_back (dcp::Certificate(dcp::file_to_string(directory / "ca.self-signed.pem")));
345  _certificates.push_back (dcp::Certificate(dcp::file_to_string(directory / "intermediate.signed.pem")));
346  _certificates.push_back (dcp::Certificate(dcp::file_to_string(directory / "leaf.signed.pem")));
347 
348  _key = dcp::file_to_string (directory / "leaf.key");
349 
350  filesystem::remove_all(directory);
351 }
352 
353 
354 CertificateChain::CertificateChain (string s)
355 {
356  while (true) {
357  try {
358  Certificate c;
359  s = c.read_string (s);
360  _certificates.push_back (c);
361  } catch (MiscError& e) {
362  /* Failed to read a certificate, just stop */
363  break;
364  }
365  }
366 
367  /* This will throw an exception if the chain cannot be ordered */
368  leaf_to_root ();
369 }
370 
371 
374 {
375  DCP_ASSERT (!_certificates.empty());
376  return root_to_leaf().front();
377 }
378 
379 
382 {
383  DCP_ASSERT (!_certificates.empty());
384  return root_to_leaf().back();
385 }
386 
387 
388 CertificateChain::List
390 {
391  auto l = root_to_leaf ();
392  std::reverse (l.begin(), l.end());
393  return l;
394 }
395 
396 
397 CertificateChain::List
398 CertificateChain::unordered () const
399 {
400  return _certificates;
401 }
402 
403 
404 void
406 {
407  _certificates.push_back (c);
408 }
409 
410 
411 void
413 {
414  auto i = std::find(_certificates.begin(), _certificates.end(), c);
415  if (i != _certificates.end()) {
416  _certificates.erase (i);
417  }
418 }
419 
420 
421 void
423 {
424  auto j = _certificates.begin ();
425  while (j != _certificates.end () && i > 0) {
426  --i;
427  ++j;
428  }
429 
430  if (j != _certificates.end ()) {
431  _certificates.erase (j);
432  }
433 }
434 
435 
436 bool
438 {
439  return chain_valid (_certificates);
440 }
441 
442 
448 bool
449 CertificateChain::chain_valid(List const & chain, string* error) const
450 {
451  /* Here I am taking a chain of certificates A/B/C/D and checking validity of B wrt A,
452  C wrt B and D wrt C. It also appears necessary to check the issuer of B/C/D matches
453  the subject of A/B/C; I don't understand why. I'm sure there's a better way of doing
454  this with OpenSSL but the documentation does not appear not likely to reveal it
455  any time soon.
456  */
457 
458  auto store = X509_STORE_new ();
459  if (!store) {
460  throw MiscError ("could not create X509 store");
461  }
462 
463  /* Put all the certificates into the store */
464  for (auto const& i: chain) {
465  if (!X509_STORE_add_cert(store, i.x509())) {
466  X509_STORE_free(store);
467  return false;
468  }
469  }
470 
471  /* Verify each one */
472  for (auto i = chain.begin(); i != chain.end(); ++i) {
473 
474  auto j = i;
475  ++j;
476  if (j == chain.end ()) {
477  break;
478  }
479 
480  auto ctx = X509_STORE_CTX_new ();
481  if (!ctx) {
482  X509_STORE_free (store);
483  throw MiscError ("could not create X509 store context");
484  }
485 
486  X509_STORE_set_flags (store, 0);
487  if (!X509_STORE_CTX_init (ctx, store, j->x509(), 0)) {
488  X509_STORE_CTX_free (ctx);
489  X509_STORE_free (store);
490  throw MiscError ("could not initialise X509 store context");
491  }
492 
493  int const v = X509_verify_cert (ctx);
494 
495  if (v != 1) {
496  X509_STORE_free (store);
497  if (error) {
498  *error = X509_verify_cert_error_string(X509_STORE_CTX_get_error(ctx));
499  }
500  X509_STORE_CTX_free(ctx);
501  return false;
502  }
503 
504  X509_STORE_CTX_free(ctx);
505 
506  /* I don't know why OpenSSL doesn't check this stuff
507  in verify_cert, but without these checks the
508  certificates_validation8 test fails.
509  */
510  if (j->issuer() != i->subject() || j->subject() == i->subject()) {
511  X509_STORE_free (store);
512  return false;
513  }
514 
515  }
516 
517  X509_STORE_free (store);
518 
519  return true;
520 }
521 
522 
523 bool
525 {
526  if (_certificates.empty ()) {
527  return true;
528  }
529 
530  if (!_key) {
531  return false;
532  }
533 
534  auto bio = BIO_new_mem_buf (const_cast<char *> (_key->c_str ()), -1);
535  if (!bio) {
536  throw MiscError ("could not create memory BIO");
537  }
538 
539  auto private_key = PEM_read_bio_RSAPrivateKey (bio, 0, 0, 0);
540  if (!private_key) {
541  return false;
542  }
543 
544  auto public_key = leaf().public_key ();
545 
546 #if OPENSSL_VERSION_NUMBER > 0x10100000L
547  BIGNUM const * private_key_n;
548  RSA_get0_key(private_key, &private_key_n, 0, 0);
549  BIGNUM const * public_key_n;
550  RSA_get0_key(public_key, &public_key_n, 0, 0);
551  if (!private_key_n || !public_key_n) {
552  return false;
553  }
554  bool const valid = !BN_cmp (private_key_n, public_key_n);
555 #else
556  bool const valid = !BN_cmp (private_key->n, public_key->n);
557 #endif
558  BIO_free (bio);
559 
560  return valid;
561 }
562 
563 
564 bool
565 CertificateChain::valid (string* reason) const
566 {
567  try {
568  root_to_leaf ();
569  } catch (CertificateChainError& e) {
570  if (reason) {
571  *reason = "certificates do not form a chain";
572  }
573  return false;
574  }
575 
576  if (!private_key_valid ()) {
577  if (reason) {
578  *reason = "private key does not exist, or does not match leaf certificate";
579  }
580  return false;
581  }
582 
583  return true;
584 }
585 
586 
587 CertificateChain::List
589 {
590  auto rtl = _certificates;
591  std::sort (rtl.begin(), rtl.end());
592  string error;
593  do {
594  if (chain_valid(rtl, &error)) {
595  return rtl;
596  }
597  } while (std::next_permutation (rtl.begin(), rtl.end()));
598 
599  throw CertificateChainError(error.empty() ? string{"certificate chain is not consistent"} : error);
600 }
601 
602 
603 void
604 CertificateChain::sign (xmlpp::Element* parent, Standard standard) const
605 {
606  /* <Signer> */
607 
608  parent->add_child_text(" ");
609  auto signer = cxml::add_child(parent, "Signer");
610  signer->set_namespace_declaration ("http://www.w3.org/2000/09/xmldsig#", "dsig");
611  auto data = cxml::add_child(signer, "X509Data", string("dsig"));
612  auto serial_element = cxml::add_child(data, "X509IssuerSerial", string("dsig"));
613  cxml::add_child(serial_element, "X509IssuerName", string("dsig"))->add_child_text(leaf().issuer());
614  cxml::add_child(serial_element, "X509SerialNumber", string("dsig"))->add_child_text(leaf().serial());
615  cxml::add_child(data, "X509SubjectName", string("dsig"))->add_child_text(leaf().subject());
616 
617  indent (signer, 2);
618 
619  /* <Signature> */
620 
621  parent->add_child_text("\n ");
622  auto signature = cxml::add_child(parent, "Signature");
623  signature->set_namespace_declaration ("http://www.w3.org/2000/09/xmldsig#", "dsig");
624  signature->set_namespace ("dsig");
625  parent->add_child_text("\n");
626 
627  auto signed_info = cxml::add_child(signature, "SignedInfo", string("dsig"));
628  cxml::add_child(signed_info, "CanonicalizationMethod", string("dsig"))->set_attribute("Algorithm", "http://www.w3.org/TR/2001/REC-xml-c14n-20010315");
629 
630  if (standard == Standard::INTEROP) {
631  cxml::add_child(signed_info, "SignatureMethod", string("dsig"))->set_attribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#rsa-sha1");
632  } else {
633  cxml::add_child(signed_info, "SignatureMethod", string("dsig"))->set_attribute("Algorithm", "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
634  }
635 
636  auto reference = cxml::add_child(signed_info, "Reference", string("dsig"));
637  reference->set_attribute ("URI", "");
638 
639  auto transforms = cxml::add_child(reference, "Transforms", string("dsig"));
640  cxml::add_child(transforms, "Transform", string("dsig"))->set_attribute(
641  "Algorithm", "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
642  );
643 
644  cxml::add_child(reference, "DigestMethod", string("dsig"))->set_attribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#sha1");
645  /* This will be filled in by the signing later */
646  cxml::add_child(reference, "DigestValue", string("dsig"));
647 
648  cxml::add_child(signature, "SignatureValue", string("dsig"));
649  cxml::add_child(signature, "KeyInfo", string("dsig"));
650  add_signature_value (signature, "dsig", true);
651 }
652 
653 
654 void
655 CertificateChain::add_signature_value (xmlpp::Element* parent, string ns, bool add_indentation) const
656 {
657  cxml::Node cp (parent);
658  auto key_info = dynamic_cast<xmlpp::Element*>(cp.node_child("KeyInfo")->node());
659  DCP_ASSERT(key_info);
660 
661  /* Add the certificate chain to the KeyInfo child node of parent */
662  for (auto const& i: leaf_to_root()) {
663  auto data = cxml::add_child(key_info, "X509Data", ns);
664 
665  {
666  auto serial = cxml::add_child(data, "X509IssuerSerial", ns);
667  cxml::add_child(serial, "X509IssuerName", ns)->add_child_text(i.issuer());
668  cxml::add_child(serial, "X509SerialNumber", ns)->add_child_text(i.serial());
669  }
670 
671  cxml::add_child(data, "X509Certificate", ns)->add_child_text(i.certificate());
672  }
673 
674  auto signature_context = xmlSecDSigCtxCreate (0);
675  if (signature_context == 0) {
676  throw MiscError ("could not create signature context");
677  }
678 
679  signature_context->signKey = xmlSecCryptoAppKeyLoadMemory (
680  reinterpret_cast<const unsigned char *> (_key->c_str()), _key->size(), xmlSecKeyDataFormatPem, 0, 0, 0
681  );
682 
683  if (signature_context->signKey == 0) {
684  throw runtime_error ("could not read private key");
685  }
686 
687  if (add_indentation) {
688  indent (parent, 2);
689  }
690  int const r = xmlSecDSigCtxSign (signature_context, parent->cobj ());
691  if (r < 0) {
692  throw MiscError (String::compose ("could not sign (%1)", r));
693  }
694 
695  xmlSecDSigCtxDestroy (signature_context);
696 }
697 
698 
699 string
700 CertificateChain::chain () const
701 {
702  string o;
703  for (auto const& i: root_to_leaf()) {
704  o += i.certificate(true);
705  }
706 
707  return o;
708 }
static void command(string cmd)
CertificateChain class.
void add(Certificate c)
Certificate leaf() const
Certificate root() const
boost::optional< std::string > _key
bool valid(std::string *reason=nullptr) const
void add_signature_value(xmlpp::Element *parent, std::string ns, bool add_indentation) const
void sign(xmlpp::Element *parent, Standard standard) const
void remove(Certificate c)
A wrapper for an X509 certificate.
Definition: certificate.h:66
std::string read_string(std::string)
Definition: certificate.cc:93
RSA * public_key() const
Definition: certificate.cc:458
A miscellaneous exception.
Definition: exceptions.h:94
DCP_ASSERT macro.
Exceptions thrown by libdcp.
Namespace for everything in libdcp.
Definition: array_data.h:50
Utility methods and classes.