X-Git-Url: https://main.carlh.net/gitweb/?a=blobdiff_plain;f=src%2Fcertificate_chain.cc;h=4f86ba0db6275a3db0e0b38b03c06dc0d0dde9fa;hb=4bd57fbbac67ac04ec47a9765b9f278aa1691851;hp=0046fe757e07eaf453be3320b2790ee524474f8a;hpb=89d5fe15b399eae5afad0b856f2d7b267a1c86c0;p=libdcp.git diff --git a/src/certificate_chain.cc b/src/certificate_chain.cc index 0046fe75..4f86ba0d 100644 --- a/src/certificate_chain.cc +++ b/src/certificate_chain.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2015 Carl Hetherington + Copyright (C) 2013-2021 Carl Hetherington This file is part of libdcp. @@ -31,10 +31,12 @@ files in the program, then also delete it here. */ -/** @file src/signer_chain.cc - * @brief Functions to make signer chains. + +/** @file src/certificate_chain.cc + * @brief CertificateChain class */ + #include "certificate_chain.h" #include "exceptions.h" #include "util.h" @@ -51,19 +53,20 @@ #include #include #include +#include #include #include -#include #include -#include +#include + using std::string; using std::ofstream; using std::ifstream; using std::runtime_error; -using std::stringstream; using namespace dcp; + /** Run a shell command. * @param cmd Command to run (UTF8-encoded). */ @@ -75,7 +78,7 @@ command (string cmd) is handled correctly. */ int const wn = MultiByteToWideChar (CP_UTF8, 0, cmd.c_str(), -1, 0, 0); - wchar_t* buffer = new wchar_t[wn]; + auto buffer = new wchar_t[wn]; if (MultiByteToWideChar (CP_UTF8, 0, cmd.c_str(), -1, buffer, wn) == 0) { delete[] buffer; return; @@ -108,12 +111,11 @@ command (string cmd) int const code = WEXITSTATUS (r); #endif if (code) { - stringstream s; - s << "error " << code << " in " << cmd << " within " << boost::filesystem::current_path(); - throw dcp::MiscError (s.str()); + throw dcp::MiscError (String::compose ("error %1 in %2 within %3", code, cmd, boost::filesystem::current_path().string())); } } + /** Extract a public key from a private key and create a SHA1 digest of it. * @param private_key Private key * @param openssl openssl binary name (or full path if openssl is not on the system path). @@ -125,14 +127,12 @@ public_key_digest (boost::filesystem::path private_key, boost::filesystem::path boost::filesystem::path public_name = private_key.string() + ".public"; /* Create the public key from the private key */ - stringstream s; - s << "\"" << openssl.string() << "\" rsa -outform PEM -pubout -in " << private_key.string() << " -out " << public_name.string (); - command (s.str().c_str ()); + command (String::compose("\"%1\" rsa -outform PEM -pubout -in %2 -out %3", openssl.string(), private_key.string(), public_name.string())); /* Read in the public key from the file */ string pub; - ifstream f (public_name.string().c_str ()); + ifstream f (public_name.string().c_str()); if (!f.good ()) { throw dcp::MiscError ("public key not found"); } @@ -181,6 +181,7 @@ public_key_digest (boost::filesystem::path private_key, boost::filesystem::path return dig; } + CertificateChain::CertificateChain ( boost::filesystem::path openssl, string organisation, @@ -190,10 +191,13 @@ CertificateChain::CertificateChain ( string leaf_common_name ) { - boost::filesystem::path directory = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path (); + /* Valid for 40 years */ + int const days = 365 * 40; + + auto directory = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path (); boost::filesystem::create_directories (directory); - boost::filesystem::path const cwd = boost::filesystem::current_path (); + auto const cwd = boost::filesystem::current_path (); boost::filesystem::current_path (directory); string quoted_openssl = "\"" + openssl.string() + "\""; @@ -205,6 +209,7 @@ CertificateChain::CertificateChain ( f << "[ req ]\n" << "distinguished_name = req_distinguished_name\n" << "x509_extensions = v3_ca\n" + << "string_mask = nombstr\n" << "[ v3_ca ]\n" << "basicConstraints = critical,CA:true,pathlen:3\n" << "keyUsage = keyCertSign,cRLSign\n" @@ -222,11 +227,13 @@ CertificateChain::CertificateChain ( "/dnQualifier=" + public_key_digest ("ca.key", openssl); { - stringstream c; - c << quoted_openssl - << " req -new -x509 -sha256 -config ca.cnf -days 3650 -set_serial 5" - << " -subj \"" << ca_subject << "\" -key ca.key -outform PEM -out ca.self-signed.pem"; - command (c.str().c_str()); + command ( + String::compose ( + "%1 req -new -x509 -sha256 -config ca.cnf -days %2 -set_serial 5" + " -subj \"%3\" -key ca.key -outform PEM -out ca.self-signed.pem", + quoted_openssl, days, ca_subject + ) + ); } command (quoted_openssl + " genrsa -out intermediate.key 2048"); @@ -236,6 +243,7 @@ CertificateChain::CertificateChain ( f << "[ default ]\n" << "distinguished_name = req_distinguished_name\n" << "x509_extensions = v3_ca\n" + << "string_mask = nombstr\n" << "[ v3_ca ]\n" << "basicConstraints = critical,CA:true,pathlen:2\n" << "keyUsage = keyCertSign,cRLSign\n" @@ -253,17 +261,20 @@ CertificateChain::CertificateChain ( "/dnQualifier=" + public_key_digest ("intermediate.key", openssl); { - stringstream s; - s << quoted_openssl - << " req -new -config intermediate.cnf -days 3649 -subj \"" << inter_subject << "\" -key intermediate.key -out intermediate.csr"; - command (s.str().c_str()); + command ( + String::compose ( + "%1 req -new -config intermediate.cnf -days %2 -subj \"%3\" -key intermediate.key -out intermediate.csr", + quoted_openssl, days - 1, inter_subject + ) + ); } - command ( - quoted_openssl + - " x509 -req -sha256 -days 3649 -CA ca.self-signed.pem -CAkey ca.key -set_serial 6" - " -in intermediate.csr -extfile intermediate.cnf -extensions v3_ca -out intermediate.signed.pem" + String::compose ( + "%1 x509 -req -sha256 -days %2 -CA ca.self-signed.pem -CAkey ca.key -set_serial 6" + " -in intermediate.csr -extfile intermediate.cnf -extensions v3_ca -out intermediate.signed.pem", + quoted_openssl, days - 1 + ) ); command (quoted_openssl + " genrsa -out leaf.key 2048"); @@ -273,6 +284,7 @@ CertificateChain::CertificateChain ( f << "[ default ]\n" << "distinguished_name = req_distinguished_name\n" << "x509_extensions = v3_ca\n" + << "string_mask = nombstr\n" << "[ v3_ca ]\n" << "basicConstraints = critical,CA:false\n" << "keyUsage = digitalSignature,keyEncipherment\n" @@ -290,85 +302,105 @@ CertificateChain::CertificateChain ( "/dnQualifier=" + public_key_digest ("leaf.key", openssl); { - stringstream s; - s << quoted_openssl << " req -new -config leaf.cnf -days 3648 -subj \"" << leaf_subject << "\" -key leaf.key -outform PEM -out leaf.csr"; - command (s.str().c_str()); + command ( + String::compose ( + "%1 req -new -config leaf.cnf -days %2 -subj \"%3\" -key leaf.key -outform PEM -out leaf.csr", + quoted_openssl, days - 2, leaf_subject + ) + ); } command ( - quoted_openssl + - " x509 -req -sha256 -days 3648 -CA intermediate.signed.pem -CAkey intermediate.key" - " -set_serial 7 -in leaf.csr -extfile leaf.cnf -extensions v3_ca -out leaf.signed.pem" + String::compose ( + "%1 x509 -req -sha256 -days %2 -CA intermediate.signed.pem -CAkey intermediate.key" + " -set_serial 7 -in leaf.csr -extfile leaf.cnf -extensions v3_ca -out leaf.signed.pem", + quoted_openssl, days - 2 + ) ); boost::filesystem::current_path (cwd); - _certificates.push_back (dcp::Certificate (dcp::file_to_string (directory / "ca.self-signed.pem"))); - _certificates.push_back (dcp::Certificate (dcp::file_to_string (directory / "intermediate.signed.pem"))); - _certificates.push_back (dcp::Certificate (dcp::file_to_string (directory / "leaf.signed.pem"))); + _certificates.push_back (dcp::Certificate(dcp::file_to_string(directory / "ca.self-signed.pem"))); + _certificates.push_back (dcp::Certificate(dcp::file_to_string(directory / "intermediate.signed.pem"))); + _certificates.push_back (dcp::Certificate(dcp::file_to_string(directory / "leaf.signed.pem"))); _key = dcp::file_to_string (directory / "leaf.key"); boost::filesystem::remove_all (directory); } -/** @return Root certificate */ + +CertificateChain::CertificateChain (string s) +{ + while (true) { + try { + Certificate c; + s = c.read_string (s); + _certificates.push_back (c); + } catch (MiscError& e) { + /* Failed to read a certificate, just stop */ + break; + } + } + + /* This will throw an exception if the chain cannot be ordered */ + leaf_to_root (); +} + + Certificate CertificateChain::root () const { DCP_ASSERT (!_certificates.empty()); - return _certificates.front (); + return root_to_leaf().front(); } -/** @return Leaf certificate */ + Certificate CertificateChain::leaf () const { - DCP_ASSERT (_certificates.size() >= 2); - return _certificates.back (); + DCP_ASSERT (!_certificates.empty()); + return root_to_leaf().back(); } -/** @return Certificates in order from root to leaf */ + CertificateChain::List -CertificateChain::root_to_leaf () const +CertificateChain::leaf_to_root () const { - return _certificates; + auto l = root_to_leaf (); + std::reverse (l.begin(), l.end()); + return l; } -/** @return Certificates in order from leaf to root */ + CertificateChain::List -CertificateChain::leaf_to_root () const +CertificateChain::unordered () const { - List c = _certificates; - c.reverse (); - return c; + return _certificates; } -/** Add a certificate to the end of the chain. - * @param c Certificate to add. - */ + void CertificateChain::add (Certificate c) { _certificates.push_back (c); } -/** Remove a certificate from the chain. - * @param c Certificate to remove. - */ + void CertificateChain::remove (Certificate c) { - _certificates.remove (c); + auto i = std::find(_certificates.begin(), _certificates.end(), c); + if (i != _certificates.end()) { + _certificates.erase (i); + } } -/** Remove the i'th certificate in the list, as listed - * from root to leaf. - */ + void CertificateChain::remove (int i) { - List::iterator j = _certificates.begin (); + auto j = _certificates.begin (); while (j != _certificates.end () && i > 0) { --i; ++j; @@ -379,128 +411,200 @@ CertificateChain::remove (int i) } } -/** Check to see if the chain is valid (i.e. root signs the intermediate, intermediate - * signs the leaf and so on) and that the private key (if there is one) matches the - * leaf certificate. - * @return true if it's ok, false if not. - */ + bool -CertificateChain::valid () const +CertificateChain::chain_valid () const { - /* Check the certificate chain */ + return chain_valid (_certificates); +} - X509_STORE* store = X509_STORE_new (); + +bool +CertificateChain::chain_valid (List const & chain) const +{ + /* Here I am taking a chain of certificates A/B/C/D and checking validity of B wrt A, + C wrt B and D wrt C. It also appears necessary to check the issuer of B/C/D matches + the subject of A/B/C; I don't understand why. I'm sure there's a better way of doing + this with OpenSSL but the documentation does not appear not likely to reveal it + any time soon. + */ + + auto store = X509_STORE_new (); if (!store) { - return false; + throw MiscError ("could not create X509 store"); + } + + /* Put all the certificates into the store */ + for (auto const& i: chain) { + if (!X509_STORE_add_cert(store, i.x509())) { + X509_STORE_free(store); + return false; + } } - for (List::const_iterator i = _certificates.begin(); i != _certificates.end(); ++i) { + /* Verify each one */ + for (auto i = chain.begin(); i != chain.end(); ++i) { - List::const_iterator j = i; + auto j = i; ++j; - if (j == _certificates.end ()) { + if (j == chain.end ()) { break; } - if (!X509_STORE_add_cert (store, i->x509 ())) { - X509_STORE_free (store); - return false; - } - - X509_STORE_CTX* ctx = X509_STORE_CTX_new (); + auto ctx = X509_STORE_CTX_new (); if (!ctx) { X509_STORE_free (store); - return false; + throw MiscError ("could not create X509 store context"); } X509_STORE_set_flags (store, 0); - if (!X509_STORE_CTX_init (ctx, store, j->x509 (), 0)) { + if (!X509_STORE_CTX_init (ctx, store, j->x509(), 0)) { X509_STORE_CTX_free (ctx); X509_STORE_free (store); - return false; + throw MiscError ("could not initialise X509 store context"); } - int v = X509_verify_cert (ctx); + int const v = X509_verify_cert (ctx); X509_STORE_CTX_free (ctx); - if (v == 0) { + if (v != 1) { X509_STORE_free (store); return false; } + + /* I don't know why OpenSSL doesn't check this stuff + in verify_cert, but without these checks the + certificates_validation8 test fails. + */ + if (j->issuer() != i->subject() || j->subject() == i->subject()) { + X509_STORE_free (store); + return false; + } + } X509_STORE_free (store); - /* Check that the leaf certificate matches the private key, if there is one */ + return true; +} - if (!_key) { + +bool +CertificateChain::private_key_valid () const +{ + if (_certificates.empty ()) { return true; } - BIO* bio = BIO_new_mem_buf (const_cast (_key->c_str ()), -1); + if (!_key) { + return false; + } + + auto bio = BIO_new_mem_buf (const_cast (_key->c_str ()), -1); if (!bio) { throw MiscError ("could not create memory BIO"); } - RSA* private_key = PEM_read_bio_RSAPrivateKey (bio, 0, 0, 0); - RSA* public_key = leaf().public_key (); + auto private_key = PEM_read_bio_RSAPrivateKey (bio, 0, 0, 0); + if (!private_key) { + return false; + } + + auto public_key = leaf().public_key (); + +#if OPENSSL_VERSION_NUMBER > 0x10100000L + BIGNUM const * private_key_n; + RSA_get0_key(private_key, &private_key_n, 0, 0); + BIGNUM const * public_key_n; + RSA_get0_key(public_key, &public_key_n, 0, 0); + if (!private_key_n || !public_key_n) { + return false; + } + bool const valid = !BN_cmp (private_key_n, public_key_n); +#else bool const valid = !BN_cmp (private_key->n, public_key->n); +#endif BIO_free (bio); return valid; } -/** @return true if the chain is now in order from root to leaf, - * false if no correct order was found. - */ + bool -CertificateChain::attempt_reorder () +CertificateChain::valid (string* reason) const { - List original = _certificates; - _certificates.sort (); + try { + root_to_leaf (); + } catch (CertificateChainError& e) { + if (reason) { + *reason = "certificates do not form a chain"; + } + return false; + } + + if (!private_key_valid ()) { + if (reason) { + *reason = "private key does not exist, or does not match leaf certificate"; + } + return false; + } + + return true; +} + + +CertificateChain::List +CertificateChain::root_to_leaf () const +{ + auto rtl = _certificates; + std::sort (rtl.begin(), rtl.end()); do { - if (valid ()) { - return true; + if (chain_valid (rtl)) { + return rtl; } - } while (std::next_permutation (_certificates.begin(), _certificates.end ())); + } while (std::next_permutation (rtl.begin(), rtl.end())); - _certificates = original; - return false; + throw CertificateChainError ("certificate chain is not consistent"); } -/** Add a <Signer> and <ds:Signature> nodes to an XML node. - * @param parent XML node to add to. - * @param standard INTEROP or SMPTE. - */ + void CertificateChain::sign (xmlpp::Element* parent, Standard standard) const { /* */ - xmlpp::Element* signer = parent->add_child("Signer"); - xmlpp::Element* data = signer->add_child("X509Data", "dsig"); - xmlpp::Element* serial_element = data->add_child("X509IssuerSerial", "dsig"); + parent->add_child_text(" "); + auto signer = parent->add_child("Signer"); + signer->set_namespace_declaration ("http://www.w3.org/2000/09/xmldsig#", "dsig"); + auto data = signer->add_child("X509Data", "dsig"); + auto serial_element = data->add_child("X509IssuerSerial", "dsig"); serial_element->add_child("X509IssuerName", "dsig")->add_child_text (leaf().issuer()); serial_element->add_child("X509SerialNumber", "dsig")->add_child_text (leaf().serial()); data->add_child("X509SubjectName", "dsig")->add_child_text (leaf().subject()); + indent (signer, 2); + /* */ - xmlpp::Element* signature = parent->add_child("Signature", "dsig"); + parent->add_child_text("\n "); + auto signature = parent->add_child("Signature"); + signature->set_namespace_declaration ("http://www.w3.org/2000/09/xmldsig#", "dsig"); + signature->set_namespace ("dsig"); + parent->add_child_text("\n"); - xmlpp::Element* signed_info = signature->add_child ("SignedInfo", "dsig"); + auto signed_info = signature->add_child ("SignedInfo", "dsig"); signed_info->add_child("CanonicalizationMethod", "dsig")->set_attribute ("Algorithm", "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"); - if (standard == INTEROP) { + if (standard == Standard::INTEROP) { signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#rsa-sha1"); } else { signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"); } - xmlpp::Element* reference = signed_info->add_child("Reference", "dsig"); + auto reference = signed_info->add_child("Reference", "dsig"); reference->set_attribute ("URI", ""); - xmlpp::Element* transforms = reference->add_child("Transforms", "dsig"); + auto transforms = reference->add_child("Transforms", "dsig"); transforms->add_child("Transform", "dsig")->set_attribute ( "Algorithm", "http://www.w3.org/2000/09/xmldsig#enveloped-signature" ); @@ -511,27 +615,22 @@ CertificateChain::sign (xmlpp::Element* parent, Standard standard) const signature->add_child("SignatureValue", "dsig"); signature->add_child("KeyInfo", "dsig"); - add_signature_value (signature, "dsig"); + add_signature_value (signature, "dsig", true); } -/** Sign an XML node. - * - * @param parent Node to sign. - * @param ns Namespace to use for the signature XML nodes. - */ void -CertificateChain::add_signature_value (xmlpp::Node* parent, string ns) const +CertificateChain::add_signature_value (xmlpp::Element* parent, string ns, bool add_indentation) const { cxml::Node cp (parent); - xmlpp::Node* key_info = cp.node_child("KeyInfo")->node (); + auto key_info = cp.node_child("KeyInfo")->node(); /* Add the certificate chain to the KeyInfo child node of parent */ - BOOST_FOREACH (Certificate const & i, leaf_to_root ()) { - xmlpp::Element* data = key_info->add_child("X509Data", ns); + for (auto const& i: leaf_to_root()) { + auto data = key_info->add_child("X509Data", ns); { - xmlpp::Element* serial = data->add_child("X509IssuerSerial", ns); + auto serial = data->add_child("X509IssuerSerial", ns); serial->add_child("X509IssuerName", ns)->add_child_text (i.issuer ()); serial->add_child("X509SerialNumber", ns)->add_child_text (i.serial ()); } @@ -539,7 +638,7 @@ CertificateChain::add_signature_value (xmlpp::Node* parent, string ns) const data->add_child("X509Certificate", ns)->add_child_text (i.certificate()); } - xmlSecDSigCtxPtr signature_context = xmlSecDSigCtxCreate (0); + auto signature_context = xmlSecDSigCtxCreate (0); if (signature_context == 0) { throw MiscError ("could not create signature context"); } @@ -552,11 +651,9 @@ CertificateChain::add_signature_value (xmlpp::Node* parent, string ns) const throw runtime_error ("could not read private key"); } - /* XXX: set key name to the PEM string: this can't be right! */ - if (xmlSecKeySetName (signature_context->signKey, reinterpret_cast (_key->c_str())) < 0) { - throw MiscError ("could not set key name"); + if (add_indentation) { + indent (parent, 2); } - int const r = xmlSecDSigCtxSign (signature_context, parent->cobj ()); if (r < 0) { throw MiscError (String::compose ("could not sign (%1)", r)); @@ -564,3 +661,15 @@ CertificateChain::add_signature_value (xmlpp::Node* parent, string ns) const xmlSecDSigCtxDestroy (signature_context); } + + +string +CertificateChain::chain () const +{ + string o; + for (auto const& i: root_to_leaf()) { + o += i.certificate(true); + } + + return o; +}