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