diff options
author | Tor Brede Vekterli <vekterli@oath.com> | 2018-10-12 14:45:40 +0000 |
---|---|---|
committer | Tor Brede Vekterli <vekterli@oath.com> | 2018-10-15 13:23:11 +0000 |
commit | 0cca964deebf35edf2d918dbcebff4bf33f36ccd (patch) | |
tree | 1a37fb5b007782932dba3a1fee2883bf2fb662f7 /vespalib/src/tests/net | |
parent | c8e17ae332cae0ddeaf50a60eaf1ba5a7dc589b3 (diff) |
Add support for custom certificate verification callbacks
Specified as part of `TransportSecurityOptions` and will default
to a callback accepting all pre-verified certificates if not given.
Callback is provided with certificate subject Common Name and
DNS Subject Alternate Name entries.
Diffstat (limited to 'vespalib/src/tests/net')
4 files changed, 642 insertions, 7 deletions
diff --git a/vespalib/src/tests/net/tls/openssl_impl/CMakeLists.txt b/vespalib/src/tests/net/tls/openssl_impl/CMakeLists.txt index 799e2291d7c..e8f77d36e16 100644 --- a/vespalib/src/tests/net/tls/openssl_impl/CMakeLists.txt +++ b/vespalib/src/tests/net/tls/openssl_impl/CMakeLists.txt @@ -1,6 +1,7 @@ # Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. vespa_add_executable(vespalib_net_tls_openssl_impl_test_app TEST SOURCES + crypto_utils.cpp openssl_impl_test.cpp DEPENDS vespalib diff --git a/vespalib/src/tests/net/tls/openssl_impl/crypto_utils.cpp b/vespalib/src/tests/net/tls/openssl_impl/crypto_utils.cpp new file mode 100644 index 00000000000..14755360b51 --- /dev/null +++ b/vespalib/src/tests/net/tls/openssl_impl/crypto_utils.cpp @@ -0,0 +1,341 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include "crypto_utils.h" +#include <vespa/vespalib/net/tls/crypto_exception.h> +#include <cassert> +#include <openssl/bn.h> +#include <openssl/rand.h> +#include <openssl/x509v3.h> + +namespace vespalib::net::tls::impl { + +namespace { + +struct EvpPkeyCtxDeleter { + void operator()(::EVP_PKEY_CTX* ctx) const noexcept { + ::EVP_PKEY_CTX_free(ctx); + } +}; + +using EvpPkeyCtxPtr = std::unique_ptr<::EVP_PKEY_CTX, EvpPkeyCtxDeleter>; + +struct BignumDeleter { + void operator()(::BIGNUM* bn) const noexcept { + ::BN_free(bn); + } +}; + +using BignumPtr = std::unique_ptr<::BIGNUM, BignumDeleter>; + +struct Asn1IntegerDeleter { + void operator()(::ASN1_INTEGER* i) const noexcept { + ::ASN1_INTEGER_free(i); + } +}; + +using Asn1IntegerPtr = std::unique_ptr<::ASN1_INTEGER, Asn1IntegerDeleter>; + +struct X509ExtensionDeleter { + void operator()(X509_EXTENSION* ext) const noexcept { + ::X509_EXTENSION_free(ext); + } +}; + +using X509ExtensionPtr = std::unique_ptr<::X509_EXTENSION, X509ExtensionDeleter>; + +vespalib::string bio_to_string(BIO& bio) { + int written = BIO_pending(&bio); + assert(written >= 0); + vespalib::string pem_str(written, '\0'); + if (::BIO_read(&bio, &pem_str[0], written) != written) { + throw CryptoException("BIO_read did not copy all PEM data"); + } + return pem_str; +} + +BioPtr new_memory_bio() { + BioPtr bio(::BIO_new(::BIO_s_mem())); + if (!bio) { + throw CryptoException("BIO_new(BIO_s_mem())"); + } + return bio; +} + +void assign_random_positive_serial_number(::X509& cert) { + /* + * From RFC3280, section 4.1.2.2: + * "The serial number MUST be a positive integer assigned by the CA to + * each certificate. It MUST be unique for each certificate issued by a + * given CA (i.e., the issuer name and serial number identify a unique + * certificate). CAs MUST force the serialNumber to be a non-negative + * integer. + * Given the uniqueness requirements above, serial numbers can be + * expected to contain long integers. Certificate users MUST be able to + * handle serialNumber values up to 20 octets. (...)" + */ + unsigned char rand_buf[20]; + // Could also have used BN_rand() for this, but RAND_bytes is just as simple + // for our purposes. + if (::RAND_bytes(rand_buf, sizeof(rand_buf)) != 1) { + throw CryptoException("RAND_bytes"); + } + // X509 serial numbers must be positive, so clear the MSB of the + // bignum-to-be. Binary buffer to BIGNUM is interpreted as big endian. + rand_buf[0] &= 0x7f; + BignumPtr bn(::BN_bin2bn(rand_buf, sizeof(rand_buf), nullptr)); + if (!bn) { + throw CryptoException("BN_bin2bn"); + } + Asn1IntegerPtr bn_as_asn1(::BN_to_ASN1_INTEGER(bn.get(), nullptr)); + if (!bn_as_asn1) { + throw CryptoException("BN_to_ASN1_INTEGER"); + } + if (!::X509_set_serialNumber(&cert, bn_as_asn1.get())) { // Makes internal copy of bignum + throw CryptoException("X509_set_serialNumber"); + } +} + +void set_certificate_expires_from_now(::X509& cert, std::chrono::seconds valid_for) { + if (::X509_gmtime_adj(X509_get_notBefore(&cert), 0) == nullptr) { + throw CryptoException("X509_gmtime_adj(X509_get_notBefore())"); + } + if (::X509_gmtime_adj(X509_get_notAfter(&cert), valid_for.count()) == nullptr) { + throw CryptoException("X509_gmtime_adj(X509_get_notAfter())"); + } +} + +void set_name_entry_if_non_empty(::X509_NAME& name, const char* field, vespalib::stringref entry) { + if (entry.empty()) { + return; + } + assert(entry.size() <= INT_MAX); + auto* entry_buf = reinterpret_cast<const unsigned char*>(entry.data()); + auto entry_len = static_cast<int>(entry.size()); + if (::X509_NAME_add_entry_by_txt(&name, field, MBSTRING_UTF8, entry_buf, entry_len, -1, 0) != 1) { + throw CryptoException("X509_NAME_add_entry_by_txt"); + } +} + +void assign_subject_distinguished_name(::X509_NAME& name, const X509Certificate::DistinguishedName& dn) { + set_name_entry_if_non_empty(name, "C", dn._country); + set_name_entry_if_non_empty(name, "ST", dn._state); + set_name_entry_if_non_empty(name, "L", dn._locality); + set_name_entry_if_non_empty(name, "O", dn._organization); + set_name_entry_if_non_empty(name, "OU", dn._organizational_unit); + for (auto& cn : dn._common_names) { + set_name_entry_if_non_empty(name, "CN", cn); + } +} + +// `value` string is taken by value since X509V3_EXT_conf_nid takes in a mutable char* +// and who knows what terrible things it might do to it (we must also ensure null +// termination of the string). +void add_v3_ext(::X509& subject, ::X509& issuer, int nid, vespalib::string value) { + // We are now reaching a point where the API we need to use is not properly documented. + // This functionality is inferred from https://opensource.apple.com/source/OpenSSL/OpenSSL-22/openssl/demos/x509/mkcert.c + ::X509V3_CTX ctx; + X509V3_set_ctx_nodb(&ctx); + // Need to set the certificate(s) so that e.g. subjectKeyIdentifier can find + // the correct public key to hash. + ::X509V3_set_ctx(&ctx, &issuer, &subject, /*CSR*/nullptr, /*CRL*/nullptr, /*flags*/0); + X509ExtensionPtr ext(::X509V3_EXT_conf_nid(nullptr, &ctx, nid, &value[0])); + if (!ext) { + throw CryptoException("X509V3_EXT_conf_nid"); + } + // Makes internal copy of ext. + // Return value semantics not documented; inferred from reading source... + if (::X509_add_ext(&subject, ext.get(), -1) != 1) { + throw CryptoException("X509_add_ext"); + } +} + +void add_any_subject_alternate_names(::X509& subject, ::X509& issuer, + const std::vector<vespalib::string>& sans) { + // There can only be 1 SAN entry in a valid cert, but it can have multiple + // logical entries separated by commas in a single string. + vespalib::string san_csv; + for (auto& san : sans) { + if (!san_csv.empty()) { + san_csv.append(','); + } + san_csv.append(san); + } + if (!san_csv.empty()) { + add_v3_ext(subject, issuer, NID_subject_alt_name, std::move(san_csv)); + } +} + +} // anon ns + +vespalib::string PrivateKey::private_to_pem() const { + BioPtr bio = new_memory_bio(); + // TODO this API is const-broken even on 1.1.1, revisit in the future... + auto* mutable_pkey = const_cast<::EVP_PKEY*>(_pkey.get()); + if (::PEM_write_bio_PrivateKey(bio.get(), mutable_pkey, nullptr, nullptr, + 0, nullptr, nullptr) != 1) { + throw CryptoException("PEM_write_bio_PrivateKey"); + } + return bio_to_string(*bio); +} + +std::shared_ptr<PrivateKey> PrivateKey::generate_p256_ec_key() { + // We first have to generate an EVP context for the keygen _parameters_... + EvpPkeyCtxPtr params_ctx(::EVP_PKEY_CTX_new_id(EVP_PKEY_EC, nullptr)); + if (!params_ctx) { + throw CryptoException("EVP_PKEY_CTX_new_id"); + } + if (::EVP_PKEY_paramgen_init(params_ctx.get()) != 1) { + throw CryptoException("EVP_PKEY_paramgen_init"); + } + // Set EC keygen parameters to use prime256v1, which is the same as P-256 + if (EVP_PKEY_CTX_set_ec_paramgen_curve_nid(params_ctx.get(), NID_X9_62_prime256v1) <= 0) { + throw CryptoException("EVP_PKEY_CTX_set_ec_paramgen_curve_nid"); + } +#if (OPENSSL_VERSION_NUMBER >= 0x10100000L) + // Must tag _explicitly_ as a named curve or many won't accept our pretty keys. + // If we don't do this, explicit curve parameters are included with the key, + // and this is not widely supported nor needed since we're generating a key on + // a standardized curve. + if (EVP_PKEY_CTX_set_ec_param_enc(params_ctx.get(), OPENSSL_EC_NAMED_CURVE) <= 0) { + throw CryptoException("EVP_PKEY_CTX_set_ec_param_enc"); + } +#endif + // Note: despite being an EVP_PKEY this is not an actual key, just key parameters! + ::EVP_PKEY* params_raw = nullptr; + if (::EVP_PKEY_paramgen(params_ctx.get(), ¶ms_raw) != 1) { + throw CryptoException("EVP_PKEY_paramgen"); + } + EvpPkeyPtr params(params_raw); + // Now we can create a context for the proper key generation + EvpPkeyCtxPtr key_ctx(::EVP_PKEY_CTX_new(params.get(), nullptr)); + if (!params_ctx) { + throw CryptoException("EVP_PKEY_CTX_new"); + } + if (::EVP_PKEY_keygen_init(key_ctx.get()) != 1) { + throw CryptoException("EVP_PKEY_keygen_init"); + } + // Finally, it's time to generate the key pair itself. + ::EVP_PKEY* pkey_raw = nullptr; + if (::EVP_PKEY_keygen(key_ctx.get(), &pkey_raw) != 1) { + throw CryptoException("EVP_PKEY_keygen"); + } + EvpPkeyPtr generated_key(pkey_raw); +#if (OPENSSL_VERSION_NUMBER < 0x10100000L) + // On OpenSSL versions prior to 1.1.0, we must set the named curve ASN1 flag + // directly on the EC_KEY, as the EVP_PKEY wrapper doesn't exist (this is a + // half truth, as it exists on 1.0.2 stable, but not necessarily on all 1.0.2 + // versions, and certainly not on 1.0.1). + EcKeyPtr ec_key(::EVP_PKEY_get1_EC_KEY(generated_key.get())); // Bumps ref count, needs free + if (!ec_key) { + throw CryptoException("EVP_PKEY_get1_EC_KEY"); + } + ::EC_KEY_set_asn1_flag(ec_key.get(), OPENSSL_EC_NAMED_CURVE); +#endif + return std::make_shared<PrivateKey>(std::move(generated_key), Type::EC); +} + +// Some references: +// https://stackoverflow.com/questions/256405/programmatically-create-x509-certificate-using-openssl +// https://opensource.apple.com/source/OpenSSL/OpenSSL-22/openssl/demos/x509/mkcert.c +std::shared_ptr<X509Certificate> X509Certificate::generate_from(Params params) { + X509Ptr cert(::X509_new()); + if (!cert) { + throw CryptoException("X509_new"); + } + ::X509_set_version(cert.get(), 2); // 2 actually means v3 :) + assign_random_positive_serial_number(*cert); + set_certificate_expires_from_now(*cert, params.valid_for); + // Internal key copy; does not take ownership + if (::X509_set_pubkey(cert.get(), params.subject_key->native_key()) != 1) { + throw CryptoException("X509_set_pubkey"); + } + // The "subject" is the target entity the certificate is intended to, well, certify. + ::X509_NAME* subj_name = ::X509_get_subject_name(cert.get()); // Internal pointer, never null, not owned by us + assign_subject_distinguished_name(*subj_name, params.subject_info.dn); + + // If our parameters indicate that there is no parent issuer of this certificate, the + // certificate to generate is by definition a self-signed (root) certificate authority. + // In this case, the Issuer name and Subject name are identical. + // If we _do_ have an issuer, we'll record its Subject name as our Issuer name. + // Note that it's legal to have a self-signed non-CA certificate, though it obviously + // cannot be used to sign any subordinate certificates. + ::X509_NAME* issuer_name = (params.issuer + ? ::X509_get_subject_name(params.issuer->native_cert()) + : subj_name); + if (::X509_set_issuer_name(cert.get(), issuer_name) != 1) { // Makes internal copy + throw CryptoException("X509_set_issuer_name"); + } + ::X509& issuer_cert = params.issuer ? *params.issuer->native_cert() : *cert; + + const char* basic_constraints = params.is_ca ? "critical,CA:TRUE" : "critical,CA:FALSE"; + const char* key_usage = params.is_ca ? "critical,keyCertSign,digitalSignature" + : "critical,digitalSignature"; + + add_v3_ext(*cert, issuer_cert, NID_basic_constraints, basic_constraints); + add_v3_ext(*cert, issuer_cert, NID_key_usage, key_usage); + add_v3_ext(*cert, issuer_cert, NID_subject_key_identifier, "hash"); + // For root CAs, authority key id == subject key id + add_v3_ext(*cert, issuer_cert, NID_authority_key_identifier, "keyid:always"); + add_any_subject_alternate_names(*cert, issuer_cert, params.subject_info.subject_alt_names); + + if (::X509_sign(cert.get(), params.issuer_key->native_key(), ::EVP_sha256()) == 0) { + throw CryptoException("X509_sign"); + } + return std::make_shared<X509Certificate>(std::move(cert)); +} + +vespalib::string X509Certificate::to_pem() const { + BioPtr bio = new_memory_bio(); + // TODO this API is const-broken, revisit in the future... + auto* mutable_cert = const_cast<::X509*>(_cert.get()); + if (::PEM_write_bio_X509(bio.get(), mutable_cert) != 1) { + throw CryptoException("PEM_write_bio_X509"); + } + return bio_to_string(*bio); +} + + +X509Certificate::DistinguishedName::DistinguishedName() = default; +X509Certificate::DistinguishedName::DistinguishedName(const DistinguishedName&) = default; +X509Certificate::DistinguishedName& X509Certificate::DistinguishedName::operator=(const DistinguishedName&) = default; +X509Certificate::DistinguishedName::DistinguishedName(DistinguishedName&&) = default; +X509Certificate::DistinguishedName& X509Certificate::DistinguishedName::operator=(DistinguishedName&&) = default; +X509Certificate::DistinguishedName::~DistinguishedName() = default; + +X509Certificate::Params::Params() = default; +X509Certificate::Params::~Params() = default; + +X509Certificate::Params +X509Certificate::Params::self_signed(SubjectInfo subject, + std::shared_ptr<PrivateKey> key) { + Params params; + params.subject_info = std::move(subject); + params.subject_key = key; + params.issuer_key = std::move(key); // self-signed, subject == issuer + params.is_ca = true; + return params; +} + +X509Certificate::Params +X509Certificate::Params::issued_by(SubjectInfo subject, + std::shared_ptr<PrivateKey> subject_key, + std::shared_ptr<X509Certificate> issuer, + std::shared_ptr<PrivateKey> issuer_key) { + Params params; + params.subject_info = std::move(subject); + params.issuer = std::move(issuer); + params.subject_key = std::move(subject_key); + params.issuer_key = std::move(issuer_key); + params.is_ca = false; // By default, caller can change for intermediate CAs + return params; +} + +CertKeyWrapper::CertKeyWrapper(std::shared_ptr<X509Certificate> cert_, + std::shared_ptr<PrivateKey> key_) + : cert(std::move(cert_)), + key(std::move(key_)) +{} + +CertKeyWrapper::~CertKeyWrapper() = default; + +} diff --git a/vespalib/src/tests/net/tls/openssl_impl/crypto_utils.h b/vespalib/src/tests/net/tls/openssl_impl/crypto_utils.h new file mode 100644 index 00000000000..017dfbdbc12 --- /dev/null +++ b/vespalib/src/tests/net/tls/openssl_impl/crypto_utils.h @@ -0,0 +1,134 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include <vespa/vespalib/stllike/string.h> +#include <vespa/vespalib/net/tls/impl/openssl_typedefs.h> +#include <chrono> +#include <memory> +#include <vector> + +// TODOs +// - add unit testing +// - extend interfaces (separate PublicKey etc) +// - hide all OpenSSL details from header +// - move to appropriate new namespace/directory somewhere under vespalib + +namespace vespalib::net::tls::impl { + +class PrivateKey { +public: + enum class Type { + EC, + RSA // TODO implement support..! + }; +private: + EvpPkeyPtr _pkey; + Type _type; +public: + PrivateKey(EvpPkeyPtr pkey, Type type) + : _pkey(std::move(pkey)), + _type(type) + {} + + ::EVP_PKEY* native_key() noexcept { return _pkey.get(); } + const ::EVP_PKEY* native_key() const noexcept { return _pkey.get(); } + + Type type() const noexcept { return _type; } + vespalib::string private_to_pem() const; + + static std::shared_ptr<PrivateKey> generate_p256_ec_key(); +}; + + +class X509Certificate { + X509Ptr _cert; +public: + explicit X509Certificate(X509Ptr cert) : _cert(std::move(cert)) {} + + ::X509* native_cert() noexcept { return _cert.get(); } + const ::X509* native_cert() const noexcept { return _cert.get(); } + + struct DistinguishedName { + vespalib::string _country; // "C" + vespalib::string _state; // "ST" + vespalib::string _locality; // "L" + vespalib::string _organization; // "O" + vespalib::string _organizational_unit; // "OU" + // Should only be 1 entry in normal certs, but X509 supports more and + // we want to be able to test this edge case. + std::vector<vespalib::string> _common_names; // "CN" + + DistinguishedName(); + DistinguishedName(const DistinguishedName&); + DistinguishedName& operator=(const DistinguishedName&); + // TODO make these noexcept once vespalib::string has noexcept move.. or move at all! + DistinguishedName(DistinguishedName&&); + DistinguishedName& operator=(DistinguishedName&&); + ~DistinguishedName(); + + // TODO could add rvalue overloads as well... + DistinguishedName& country(vespalib::stringref c) { _country = c; return *this; } + DistinguishedName& state(vespalib::stringref st) { _state = st; return *this; } + DistinguishedName& locality(vespalib::stringref l) { _locality = l; return *this; } + DistinguishedName& organization(vespalib::stringref o) { _organization = o; return *this; } + DistinguishedName& organizational_unit(vespalib::stringref ou) { + _organizational_unit = ou; + return *this; + } + DistinguishedName& add_common_name(vespalib::stringref cn) { + _common_names.emplace_back(cn); + return *this; + } + }; + + struct SubjectInfo { + DistinguishedName dn; + std::vector<vespalib::string> subject_alt_names; + + SubjectInfo() = default; + explicit SubjectInfo(DistinguishedName dn_) + : dn(std::move(dn_)), + subject_alt_names() + {} + + SubjectInfo& add_subject_alt_name(vespalib::string san) { + subject_alt_names.emplace_back(std::move(san)); + return *this; + } + }; + + struct Params { + Params(); + ~Params(); + + SubjectInfo subject_info; + // TODO make public key, but private key has both and this is currently just for testing. + std::shared_ptr<PrivateKey> subject_key; + std::shared_ptr<X509Certificate> issuer; // May be nullptr for self-signed certs + std::shared_ptr<PrivateKey> issuer_key; + std::chrono::seconds valid_for = std::chrono::hours(24); + bool is_ca = false; + + static Params self_signed(SubjectInfo subject, std::shared_ptr<PrivateKey> key); + // TODO only need _public_ key from subject, but this is simplified + static Params issued_by(SubjectInfo subject, + std::shared_ptr<PrivateKey> subject_key, + std::shared_ptr<X509Certificate> issuer, + std::shared_ptr<PrivateKey> issuer_key); + }; + + // Generates an X509 certificate using a SHA-256 digest + static std::shared_ptr<X509Certificate> generate_from(Params params); + vespalib::string to_pem() const; +}; + +struct CertKeyWrapper { + std::shared_ptr<X509Certificate> cert; + std::shared_ptr<PrivateKey> key; + + CertKeyWrapper(std::shared_ptr<X509Certificate> cert_, + std::shared_ptr<PrivateKey> key_); + ~CertKeyWrapper(); +}; + +} diff --git a/vespalib/src/tests/net/tls/openssl_impl/openssl_impl_test.cpp b/vespalib/src/tests/net/tls/openssl_impl/openssl_impl_test.cpp index e19d99ee305..423ea6222a2 100644 --- a/vespalib/src/tests/net/tls/openssl_impl/openssl_impl_test.cpp +++ b/vespalib/src/tests/net/tls/openssl_impl/openssl_impl_test.cpp @@ -1,4 +1,5 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#include "crypto_utils.h" #include <vespa/vespalib/testkit/test_kit.h> #include <vespa/vespalib/data/smart_buffer.h> #include <vespa/vespalib/net/tls/tls_context.h> @@ -8,10 +9,12 @@ #include <vespa/vespalib/net/tls/impl/openssl_tls_context_impl.h> #include <vespa/vespalib/test/make_tls_options_for_testing.h> #include <iostream> +#include <stdexcept> #include <stdlib.h> using namespace vespalib; using namespace vespalib::net::tls; +using namespace vespalib::net::tls::impl; const char* decode_state_to_str(DecodeResult::State state) noexcept { switch (state) { @@ -53,7 +56,7 @@ void print_decode_result(const char* mode, const DecodeResult& res) { struct Fixture { TransportSecurityOptions tls_opts; - std::unique_ptr<TlsContext> tls_ctx; + std::shared_ptr<TlsContext> tls_ctx; std::unique_ptr<CryptoCodec> client; std::unique_ptr<CryptoCodec> server; SmartBuffer client_to_server; @@ -62,11 +65,12 @@ struct Fixture { Fixture() : tls_opts(vespalib::test::make_tls_options_for_testing()), tls_ctx(TlsContext::create_default_context(tls_opts)), - client(create_openssl_codec(*tls_ctx, CryptoCodec::Mode::Client)), - server(create_openssl_codec(*tls_ctx, CryptoCodec::Mode::Server)), + client(create_openssl_codec(tls_ctx, CryptoCodec::Mode::Client)), + server(create_openssl_codec(tls_ctx, CryptoCodec::Mode::Server)), client_to_server(64 * 1024), server_to_client(64 * 1024) {} + ~Fixture(); static TransportSecurityOptions create_options_without_own_peer_cert() { auto source_opts = vespalib::test::make_tls_options_for_testing(); @@ -76,13 +80,13 @@ struct Fixture { static std::unique_ptr<CryptoCodec> create_openssl_codec( const TransportSecurityOptions& opts, CryptoCodec::Mode mode) { auto ctx = TlsContext::create_default_context(opts); - return create_openssl_codec(*ctx, mode); + return create_openssl_codec(ctx, mode); } static std::unique_ptr<CryptoCodec> create_openssl_codec( - const TlsContext& ctx, CryptoCodec::Mode mode) { - return std::make_unique<impl::OpenSslCryptoCodecImpl>( - *dynamic_cast<const impl::OpenSslTlsContextImpl&>(ctx).native_context(), mode); + const std::shared_ptr<TlsContext>& ctx, CryptoCodec::Mode mode) { + auto ctx_impl = std::dynamic_pointer_cast<impl::OpenSslTlsContextImpl>(ctx); + return std::make_unique<impl::OpenSslCryptoCodecImpl>(std::move(ctx_impl), mode); } EncodeResult do_encode(CryptoCodec& codec, Output& buffer, vespalib::stringref plaintext) { @@ -155,7 +159,10 @@ struct Fixture { } }; +Fixture::~Fixture() = default; + TEST_F("client and server can complete handshake", Fixture) { + fprintf(stderr, "Compiled with %s\n", OPENSSL_VERSION_TEXT); EXPECT_TRUE(f.handshake()); } @@ -304,6 +311,158 @@ TEST_F("Can specify multiple trusted CA certs in transport options", Fixture) { EXPECT_TRUE(f.handshake()); } +struct CertFixture : Fixture { + CertKeyWrapper root_ca; + + static CertKeyWrapper make_root_ca() { + auto dn = X509Certificate::DistinguishedName() + .country("US").state("CA").locality("Sunnyvale") + .organization("ACME, Inc.") + .organizational_unit("ACME Root CA") + .add_common_name("acme.example.com"); + auto subject = X509Certificate::SubjectInfo(std::move(dn)); + auto key = PrivateKey::generate_p256_ec_key(); + auto params = X509Certificate::Params::self_signed(std::move(subject), key); + auto cert = X509Certificate::generate_from(std::move(params)); + return {std::move(cert), std::move(key)}; + } + + CertFixture() + : Fixture(), + root_ca(make_root_ca()) + {} + ~CertFixture(); + + CertKeyWrapper create_ca_issued_peer_cert(const std::vector<vespalib::string>& common_names, + const std::vector<vespalib::string>& sans) { + auto dn = X509Certificate::DistinguishedName() + .country("US").state("CA").locality("Sunnyvale") + .organization("Wile E. Coyote, Ltd.") + .organizational_unit("Personal Rocketry Division"); + for (auto& cn : common_names) { + dn.add_common_name(cn); + } + auto subject = X509Certificate::SubjectInfo(std::move(dn)); + for (auto& san : sans) { + subject.add_subject_alt_name(san); + } + auto key = PrivateKey::generate_p256_ec_key(); + auto params = X509Certificate::Params::issued_by(std::move(subject), key, root_ca.cert, root_ca.key); + auto cert = X509Certificate::generate_from(std::move(params)); + return {std::move(cert), std::move(key)}; + } + + void reset_client_with_cert_opts(const CertKeyWrapper& ck, std::shared_ptr<CertificateVerificationCallback> cert_cb) { + TransportSecurityOptions client_opts(root_ca.cert->to_pem(), ck.cert->to_pem(), + ck.key->private_to_pem(), std::move(cert_cb)); + client = create_openssl_codec(client_opts, CryptoCodec::Mode::Client); + } + + void reset_server_with_cert_opts(const CertKeyWrapper& ck, std::shared_ptr<CertificateVerificationCallback> cert_cb) { + TransportSecurityOptions server_opts(root_ca.cert->to_pem(), ck.cert->to_pem(), + ck.key->private_to_pem(), std::move(cert_cb)); + server = create_openssl_codec(server_opts, CryptoCodec::Mode::Server); + } +}; + +CertFixture::~CertFixture() = default; + +struct PrintingCertificateCallback : CertificateVerificationCallback { + bool verify(const PeerCredentials& peer_creds) const override { + if (!peer_creds.common_name.empty()) { + fprintf(stderr, "Got a CN: %s\n", peer_creds.common_name.c_str()); + } + for (auto& dns : peer_creds.dns_sans) { + fprintf(stderr, "Got a DNS SAN entry: %s\n", dns.c_str()); + } + return true; + } +}; + +// Single-use mock verifier +struct MockCertificateCallback : CertificateVerificationCallback { + mutable PeerCredentials creds; // only used in single thread testing context + bool verify(const PeerCredentials& peer_creds) const override { + creds = peer_creds; + return true; + } +}; + +struct AlwaysFailVerifyCallback : CertificateVerificationCallback { + bool verify([[maybe_unused]] const PeerCredentials& peer_creds) const override { + fprintf(stderr, "Rejecting certificate, none shall pass!\n"); + return false; + } +}; + +struct ExceptionThrowingCallback : CertificateVerificationCallback { + bool verify([[maybe_unused]] const PeerCredentials& peer_creds) const override { + throw std::runtime_error("oh no what is going on"); + } +}; + +TEST_F("Certificate verification callback returning false breaks handshake", CertFixture) { + auto ck = f.create_ca_issued_peer_cert({"hello.world.example.com"}, {}); + + f.reset_client_with_cert_opts(ck, std::make_shared<PrintingCertificateCallback>()); + f.reset_server_with_cert_opts(ck, std::make_shared<AlwaysFailVerifyCallback>()); + EXPECT_FALSE(f.handshake()); +} + +TEST_F("Exception during verification callback processing breaks handshake", CertFixture) { + auto ck = f.create_ca_issued_peer_cert({"hello.world.example.com"}, {}); + + f.reset_client_with_cert_opts(ck, std::make_shared<PrintingCertificateCallback>()); + f.reset_server_with_cert_opts(ck, std::make_shared<ExceptionThrowingCallback>()); + EXPECT_FALSE(f.handshake()); +} + +TEST_F("certificate verification callback observes CN and DNS SANs", CertFixture) { + auto ck = f.create_ca_issued_peer_cert( + {{"rockets.wile.example.com"}}, + {{"DNS:crash.wile.example.com"}, {"DNS:burn.wile.example.com"}}); + + fprintf(stderr, "certs:\n%s%s\n", f.root_ca.cert->to_pem().c_str(), ck.cert->to_pem().c_str()); + + f.reset_client_with_cert_opts(ck, std::make_shared<PrintingCertificateCallback>()); + auto server_cb = std::make_shared<MockCertificateCallback>(); + f.reset_server_with_cert_opts(ck, server_cb); + ASSERT_TRUE(f.handshake()); + + auto& creds = server_cb->creds; + EXPECT_EQUAL("rockets.wile.example.com", creds.common_name); + ASSERT_EQUAL(2u, creds.dns_sans.size()); + EXPECT_EQUAL("crash.wile.example.com", creds.dns_sans[0]); + EXPECT_EQUAL("burn.wile.example.com", creds.dns_sans[1]); +} + +TEST_F("last occurring CN is given to verification callback if multiple CNs are present", CertFixture) { + auto ck = f.create_ca_issued_peer_cert( + {{"foo.wile.example.com"}, {"bar.wile.example.com"}, {"baz.wile.example.com"}}, {}); + + f.reset_client_with_cert_opts(ck, std::make_shared<PrintingCertificateCallback>()); + auto server_cb = std::make_shared<MockCertificateCallback>(); + f.reset_server_with_cert_opts(ck, server_cb); + ASSERT_TRUE(f.handshake()); + + auto& creds = server_cb->creds; + EXPECT_EQUAL("baz.wile.example.com", creds.common_name); +} + +// TODO we are likely to want IPADDR SANs at some point +TEST_F("Only DNS SANs are enumerated", CertFixture) { + auto ck = f.create_ca_issued_peer_cert({}, {"IP:127.0.0.1"}); + + f.reset_client_with_cert_opts(ck, std::make_shared<PrintingCertificateCallback>()); + auto server_cb = std::make_shared<MockCertificateCallback>(); + f.reset_server_with_cert_opts(ck, server_cb); + ASSERT_TRUE(f.handshake()); + EXPECT_EQUAL(0u, server_cb->creds.dns_sans.size()); +} + +// TODO we can't test embedded nulls since the OpenSSL v3 extension APIs +// take in null terminated strings as arguments... :I + /* * TODO tests: * - handshakes with multi frame writes |