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 | |
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')
21 files changed, 931 insertions, 45 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 diff --git a/vespalib/src/vespa/vespalib/net/tls/CMakeLists.txt b/vespalib/src/vespa/vespalib/net/tls/CMakeLists.txt index 87b7cff1c13..8fd92220abb 100644 --- a/vespalib/src/vespa/vespalib/net/tls/CMakeLists.txt +++ b/vespalib/src/vespa/vespalib/net/tls/CMakeLists.txt @@ -6,6 +6,7 @@ vespa_add_library(vespalib_vespalib_net_tls OBJECT crypto_exception.cpp maybe_tls_crypto_engine.cpp maybe_tls_crypto_socket.cpp + peer_credentials.cpp protocol_snooping.cpp tls_context.cpp tls_crypto_engine.cpp diff --git a/vespalib/src/vespa/vespalib/net/tls/certificate_verification_callback.h b/vespalib/src/vespa/vespalib/net/tls/certificate_verification_callback.h new file mode 100644 index 00000000000..7ea4d22bab4 --- /dev/null +++ b/vespalib/src/vespa/vespalib/net/tls/certificate_verification_callback.h @@ -0,0 +1,28 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +#pragma once + +#include "peer_credentials.h" + +namespace vespalib::net::tls { + +// Verification callback invoked when a signed X509 certificate is presented +// from a peer during TLS handshaking. +// Only invoked for the leaf peer certificate, _not_ for any CAs (root or intermediate). +// Only invoked iff the certificate has already passed OpenSSL pre-verification. +struct CertificateVerificationCallback { + virtual ~CertificateVerificationCallback() = default; + // Return true iff the peer credentials pass verification, false otherwise. + // Must be thread safe. + virtual bool verify(const PeerCredentials& peer_creds) const = 0; +}; + +// Simplest possible certificate verification callback which accepts the certificate +// iff all its pre-verification by OpenSSL has passed. This means its chain is valid +// and it is signed by a trusted CA. +struct AcceptAllPreVerifiedCertificates : CertificateVerificationCallback { + bool verify([[maybe_unused]] const PeerCredentials& peer_creds) const override { + return true; // yolo + } +}; + +} diff --git a/vespalib/src/vespa/vespalib/net/tls/crypto_codec.cpp b/vespalib/src/vespa/vespalib/net/tls/crypto_codec.cpp index b36913d20e3..680d6472470 100644 --- a/vespalib/src/vespa/vespalib/net/tls/crypto_codec.cpp +++ b/vespalib/src/vespa/vespalib/net/tls/crypto_codec.cpp @@ -6,10 +6,10 @@ namespace vespalib::net::tls { -std::unique_ptr<CryptoCodec> CryptoCodec::create_default_codec(TlsContext& ctx, Mode mode) { - auto* ssl_ctx = dynamic_cast<impl::OpenSslTlsContextImpl*>(&ctx); - assert(ssl_ctx != nullptr); - return std::make_unique<impl::OpenSslCryptoCodecImpl>(*ssl_ctx->native_context(), mode); +std::unique_ptr<CryptoCodec> CryptoCodec::create_default_codec(std::shared_ptr<TlsContext> ctx, Mode mode) { + auto ctx_impl = std::dynamic_pointer_cast<impl::OpenSslTlsContextImpl>(ctx); // only takes by const ref + assert(ctx_impl); + return std::make_unique<impl::OpenSslCryptoCodecImpl>(std::move(ctx_impl), mode); } } diff --git a/vespalib/src/vespa/vespalib/net/tls/crypto_codec.h b/vespalib/src/vespa/vespalib/net/tls/crypto_codec.h index 2df2c9f4741..3ec05e207d3 100644 --- a/vespalib/src/vespa/vespalib/net/tls/crypto_codec.h +++ b/vespalib/src/vespa/vespalib/net/tls/crypto_codec.h @@ -119,7 +119,7 @@ public: * * Throws CryptoException if resources cannot be allocated for the codec. */ - static std::unique_ptr<CryptoCodec> create_default_codec(TlsContext& ctx, Mode mode); + static std::unique_ptr<CryptoCodec> create_default_codec(std::shared_ptr<TlsContext> ctx, Mode mode); }; } diff --git a/vespalib/src/vespa/vespalib/net/tls/impl/openssl_crypto_codec_impl.cpp b/vespalib/src/vespa/vespalib/net/tls/impl/openssl_crypto_codec_impl.cpp index 4f48f60b70c..25eb12e25bd 100644 --- a/vespalib/src/vespa/vespalib/net/tls/impl/openssl_crypto_codec_impl.cpp +++ b/vespalib/src/vespa/vespalib/net/tls/impl/openssl_crypto_codec_impl.cpp @@ -145,8 +145,9 @@ vespalib::string ssl_error_from_stack() { } // anon ns -OpenSslCryptoCodecImpl::OpenSslCryptoCodecImpl(::SSL_CTX& ctx, Mode mode) - : _ssl(::SSL_new(&ctx)), +OpenSslCryptoCodecImpl::OpenSslCryptoCodecImpl(std::shared_ptr<OpenSslTlsContextImpl> ctx, Mode mode) + : _ctx(std::move(ctx)), + _ssl(::SSL_new(_ctx->native_context())), _mode(mode) { if (!_ssl) { @@ -192,6 +193,8 @@ OpenSslCryptoCodecImpl::OpenSslCryptoCodecImpl(::SSL_CTX& ctx, Mode mode) } } +OpenSslCryptoCodecImpl::~OpenSslCryptoCodecImpl() = default; + // TODO remove spammy logging once code is stable HandshakeResult OpenSslCryptoCodecImpl::handshake(const char* from_peer, size_t from_peer_buf_size, diff --git a/vespalib/src/vespa/vespalib/net/tls/impl/openssl_crypto_codec_impl.h b/vespalib/src/vespa/vespalib/net/tls/impl/openssl_crypto_codec_impl.h index 53693dd7f40..572377827d6 100644 --- a/vespalib/src/vespa/vespalib/net/tls/impl/openssl_crypto_codec_impl.h +++ b/vespalib/src/vespa/vespalib/net/tls/impl/openssl_crypto_codec_impl.h @@ -10,6 +10,8 @@ namespace vespalib::net::tls { class TlsContext; } namespace vespalib::net::tls::impl { +class OpenSslTlsContextImpl; + /* * Frame-level OpenSSL-backed TLSv1.2/TLSv1.3 (depending on OpenSSL version) * crypto codec implementation. @@ -18,12 +20,16 @@ namespace vespalib::net::tls::impl { * used by different threads safely. */ class OpenSslCryptoCodecImpl : public CryptoCodec { + // The context maintains shared verification callback state, so it must be + // kept alive explictly for at least as long as any codecs. + std::shared_ptr<OpenSslTlsContextImpl> _ctx; SslPtr _ssl; ::BIO* _input_bio; // Owned by _ssl ::BIO* _output_bio; // Owned by _ssl Mode _mode; public: - OpenSslCryptoCodecImpl(::SSL_CTX& ctx, Mode mode); + OpenSslCryptoCodecImpl(std::shared_ptr<OpenSslTlsContextImpl> ctx, Mode mode); + ~OpenSslCryptoCodecImpl() override; /* * From RFC 8449 (Record Size Limit Extension for TLS), section 1: diff --git a/vespalib/src/vespa/vespalib/net/tls/impl/openssl_tls_context_impl.cpp b/vespalib/src/vespa/vespalib/net/tls/impl/openssl_tls_context_impl.cpp index 050fd2be7e5..bc981bccb96 100644 --- a/vespalib/src/vespa/vespalib/net/tls/impl/openssl_tls_context_impl.cpp +++ b/vespalib/src/vespa/vespalib/net/tls/impl/openssl_tls_context_impl.cpp @@ -11,6 +11,8 @@ #include <openssl/ssl.h> #include <openssl/crypto.h> #include <openssl/err.h> +#include <openssl/x509v3.h> +#include <openssl/asn1.h> #include <openssl/pem.h> #include <vespa/log/log.h> @@ -18,7 +20,7 @@ LOG_SETUP(".vespalib.net.tls.openssl_tls_context_impl"); #if (OPENSSL_VERSION_NUMBER < 0x10000000L) // < 1.0 requires explicit thread ID callback support. -# error "Provided OpenSSL version is too darn old, need at least 1.0" +# error "Provided OpenSSL version is too darn old, need at least 1.0.0" #endif namespace vespalib::net::tls::impl { @@ -29,7 +31,7 @@ namespace { std::vector<std::unique_ptr<std::mutex>> _g_mutexes; -// Some works on OpenSSL legacy locking: OpenSSL does not implement locking +// Some words on OpenSSL legacy locking: OpenSSL does not implement locking // itself internally, deferring to user code callbacks that Do The Needful(tm). // The `n` parameter refers to the nth mutex, which is always < CRYPTO_num_locks(). void openssl_locking_cb(int mode, int n, [[maybe_unused]] const char *file, [[maybe_unused]] int line) { @@ -61,7 +63,7 @@ OpenSslLibraryResources::OpenSslLibraryResources() { if (!::CRYPTO_get_locking_callback()) { const int num_locks = ::CRYPTO_num_locks(); LOG_ASSERT(num_locks > 0); - _g_mutexes.reserve(num_locks); + _g_mutexes.reserve(static_cast<size_t>(num_locks)); for (int i = 0; i < num_locks; ++i) { _g_mutexes.emplace_back(std::make_unique<std::mutex>()); } @@ -101,7 +103,7 @@ BioPtr bio_from_string(vespalib::stringref str) { } bool has_pem_eof_on_stack() { - const auto err = ERR_peek_last_error(); + const auto err = ::ERR_peek_last_error(); if (!err) { return false; } @@ -111,7 +113,7 @@ bool has_pem_eof_on_stack() { vespalib::string ssl_error_from_stack() { char buf[256]; - ERR_error_string_n(ERR_get_error(), buf, sizeof(buf)); + ::ERR_error_string_n(::ERR_get_error(), buf, sizeof(buf)); return vespalib::string(buf); } @@ -129,8 +131,8 @@ vespalib::string ssl_error_from_stack() { // Neat! // // Bonus points for being non-const as well. -constexpr inline void *empty_passphrase() { - return const_cast<void *>(static_cast<const void *>("")); +constexpr inline void* empty_passphrase() { + return const_cast<void*>(static_cast<const void*>("")); } void verify_pem_ok_or_eof(::X509* x509) { @@ -184,11 +186,13 @@ SslCtxPtr new_tls_ctx_with_auto_init() { } // anon ns OpenSslTlsContextImpl::OpenSslTlsContextImpl(const TransportSecurityOptions& ts_opts) - : _ctx(new_tls_ctx_with_auto_init()) + : _ctx(new_tls_ctx_with_auto_init()), + _cert_verify_callback(ts_opts.cert_verify_callback()) { if (!_ctx) { throw CryptoException("Failed to create new TLS context"); } + LOG_ASSERT(_cert_verify_callback.get() != nullptr); add_certificate_authorities(ts_opts.ca_certs_pem()); if (!ts_opts.cert_chain_pem().empty() && !ts_opts.private_key_pem().empty()) { add_certificate_chain(ts_opts.cert_chain_pem()); @@ -198,11 +202,19 @@ OpenSslTlsContextImpl::OpenSslTlsContextImpl(const TransportSecurityOptions& ts_ enable_ephemeral_key_exchange(); disable_compression(); enforce_peer_certificate_verification(); + set_provided_certificate_verification_callback(); // TODO set accepted cipher suites! // TODO `--> If not set in options, use Modern spec from https://wiki.mozilla.org/Security/Server_Side_TLS } -OpenSslTlsContextImpl::~OpenSslTlsContextImpl() = default; +OpenSslTlsContextImpl::~OpenSslTlsContextImpl() { + void* cb_data = SSL_CTX_get_app_data(_ctx.get()); + if (cb_data) { + // Referenced callback is kept in a shared_ptr, so lifetime is ensured. + // Either way, clean up after ourselves. + SSL_CTX_set_app_data(_ctx.get(), nullptr); + } +} void OpenSslTlsContextImpl::add_certificate_authorities(vespalib::stringref ca_pem) { auto bio = bio_from_string(ca_pem); @@ -270,7 +282,7 @@ void OpenSslTlsContextImpl::enable_ephemeral_key_exchange() { throw CryptoException("SSL_CTX_set_ecdh_auto"); } // New ECDH key per connection. - ::SSL_CTX_set_options(_ctx.get(), SSL_OP_SINGLE_ECDH_USE); + SSL_CTX_set_options(_ctx.get(), SSL_OP_SINGLE_ECDH_USE); # else // Set explicit P-256 curve used for ECDH purposes. EcKeyPtr ec_curve(::EC_KEY_new_by_curve_name(NID_X9_62_prime256v1)); @@ -287,14 +299,151 @@ void OpenSslTlsContextImpl::enable_ephemeral_key_exchange() { void OpenSslTlsContextImpl::disable_compression() { // TLS stream compression is vulnerable to a host of chosen plaintext // attacks (CRIME, BREACH etc), so disable it. - ::SSL_CTX_set_options(_ctx.get(), SSL_OP_NO_COMPRESSION); + SSL_CTX_set_options(_ctx.get(), SSL_OP_NO_COMPRESSION); +} + +namespace { + +// There's no good reason for entries to contain embedded nulls, aside from +// trying to be sneaky. See Moxie Marlinspike's Blackhat USA 2009 presentation +// for context. +bool has_embedded_nulls(const char* data, size_t size) { + return (memchr(data, '\0', size) != nullptr); } +// Normally there should only be 1 CN entry in a certificate, but it's possible +// to specify multiple. We'll only report the last occurring one. +bool fill_certificate_common_name(::X509* cert, PeerCredentials& creds) { + // We're only after CN entries of the subject name + ::X509_NAME* subj_name = ::X509_get_subject_name(cert); // _not_ owned by us, never nullptr + int pos = -1; + // X509_NAME_get_index_by_NID returns -1 if there are no further indices containing + // an entry with the given NID _after_ pos. -1 must be passed as the initial pos value, + // since index 0 might be valid. + while ((pos = ::X509_NAME_get_index_by_NID(subj_name, NID_commonName, pos)) >= 0) { + ::X509_NAME_ENTRY* entry = ::X509_NAME_get_entry(subj_name, pos); + if (!entry) { + LOG(error, "Got X509 peer certificate with invalid CN entry"); + return false; + } + ::ASN1_STRING* cn_asn1 = ::X509_NAME_ENTRY_get_data(entry); + if ((cn_asn1 != nullptr) && (cn_asn1->data != nullptr) && (cn_asn1->length > 0)) { + const auto* data = reinterpret_cast<const char*>(cn_asn1->data); + const auto size = static_cast<size_t>(cn_asn1->length); + if (has_embedded_nulls(data, size)) { + LOG(warning, "Got X509 peer certificate with embedded nulls in CN field"); + return false; + } + creds.common_name.assign(data, size); + } + } + return true; +} + +struct GeneralNamesDeleter { + void operator()(::GENERAL_NAMES* names) { + ::GENERAL_NAMES_free(names); + } +}; + +using GeneralNamesPtr = std::unique_ptr<::GENERAL_NAMES, GeneralNamesDeleter>; + +bool fill_certificate_subject_alternate_names(::X509* cert, PeerCredentials& creds) { + GeneralNamesPtr san_names(static_cast<GENERAL_NAMES*>( + ::X509_get_ext_d2i(cert, NID_subject_alt_name, nullptr, nullptr))); + if (san_names) { + for (int i = 0; i < sk_GENERAL_NAME_num(san_names.get()); ++i) { + auto* value = sk_GENERAL_NAME_value(san_names.get(), i); + if (value->type == GEN_DNS) { + auto* dns_name = value->d.dNSName; // const or non-const depending on version... + if ((dns_name->type == V_ASN1_IA5STRING) && (dns_name->data != nullptr) && (dns_name->length > 0)) { +#if (OPENSSL_VERSION_NUMBER >= 0x10100000L) + const char* data = reinterpret_cast<const char*>(::ASN1_STRING_get0_data(dns_name)); +#else + const char* data = reinterpret_cast<const char*>(::ASN1_STRING_data(dns_name)); +#endif + const auto length = static_cast<size_t>(::ASN1_STRING_length(dns_name)); + if (has_embedded_nulls(data, length)) { + LOG(warning, "Got X509 peer certificate with embedded nulls in SAN field"); + return false; + } + creds.dns_sans.emplace_back(data, length); + } + } // TODO support GEN_IPADD SAN? + } + } + return true; +} + +// TODO if/when we want to move per-connection peer credentials into the crypto codec/socket +// itself, we probably need to set the verification callback (data) on _SSL_, not _SSL_CTX_..! +// Note: we try to be as conservative as possible. If anything looks out of place, we fail +// secure by denying the connection. +// +// References: +// https://github.com/boostorg/asio/blob/develop/include/boost/asio/ssl/impl/context.ipp +// https://github.com/boostorg/asio/blob/develop/include/boost/asio/ssl/impl/rfc2818_verification.ipp +int verify_cb_wrapper(int preverified_ok, ::X509_STORE_CTX* store_ctx) { + if (!preverified_ok) { + return 0; // If it's already known to be broken, we won't do anything more. + } + // The verify callback is invoked with every certificate in the chain, starting + // with a root CA, then any intermediate CAs, then finally the peer's own certificate + // at depth 0. We currently aren't interested in anything except the peer cert + // since we trust the intermediates to have done their job. + const bool is_peer_cert = (::X509_STORE_CTX_get_error_depth(store_ctx) == 0); + if (!is_peer_cert) { + return 1; // OK for root/intermediate cert. + } + // Fetch the SSL instance associated with the X509_STORE_CTX + const void* data = ::X509_STORE_CTX_get_ex_data(store_ctx, ::SSL_get_ex_data_X509_STORE_CTX_idx()); + if (!data) { + return 0; + } + const auto* ssl = static_cast<const ::SSL*>(data); + const ::SSL_CTX* ssl_ctx = ::SSL_get_SSL_CTX(ssl); + if (!ssl_ctx) { + return 0; + } + auto* cert_validator = static_cast<CertificateVerificationCallback*>(SSL_CTX_get_app_data(ssl_ctx)); + if (!cert_validator) { + return 0; + } + ::X509* cert = ::X509_STORE_CTX_get_current_cert(store_ctx); // _not_ owned by us + if (!cert) { + LOG(error, "Got X509_STORE_CTX with preverified_ok == 1 but no current cert"); + return 0; + } + PeerCredentials creds; + if (!fill_certificate_common_name(cert, creds)) { + return 0; + } + if (!fill_certificate_subject_alternate_names(cert, creds)) { + return 0; + } + try { + const bool verified_by_cb = cert_validator->verify(creds); + if (!verified_by_cb) { + LOG(debug, "Connection rejected by certificate verification callback"); + return 0; + } + } catch (std::exception& e) { + LOG(error, "Got exception during certificate verification callback: %s", e.what()); + return 0; + } // we don't expect any non-std::exception derived exceptions, so let them terminate the process. + return 1; +} + +} // anon ns + void OpenSslTlsContextImpl::enforce_peer_certificate_verification() { // We require full mutual certificate verification. No way to configure // out of this, at least not for the time being. - // TODO verification callback for custom CN/SAN etc checks. - SSL_CTX_set_verify(_ctx.get(), SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, nullptr); + ::SSL_CTX_set_verify(_ctx.get(), SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, verify_cb_wrapper); +} + +void OpenSslTlsContextImpl::set_provided_certificate_verification_callback() { + SSL_CTX_set_app_data(_ctx.get(), _cert_verify_callback.get()); } } diff --git a/vespalib/src/vespa/vespalib/net/tls/impl/openssl_tls_context_impl.h b/vespalib/src/vespa/vespalib/net/tls/impl/openssl_tls_context_impl.h index 72f9f3b570d..e6de28043d6 100644 --- a/vespalib/src/vespa/vespalib/net/tls/impl/openssl_tls_context_impl.h +++ b/vespalib/src/vespa/vespalib/net/tls/impl/openssl_tls_context_impl.h @@ -3,12 +3,15 @@ #include "openssl_typedefs.h" #include <vespa/vespalib/net/tls/tls_context.h> +#include <vespa/vespalib/net/tls/certificate_verification_callback.h> #include <vespa/vespalib/stllike/string.h> namespace vespalib::net::tls::impl { class OpenSslTlsContextImpl : public TlsContext { SslCtxPtr _ctx; + // Callback provided by options + std::shared_ptr<CertificateVerificationCallback> _cert_verify_callback; public: explicit OpenSslTlsContextImpl(const TransportSecurityOptions&); ~OpenSslTlsContextImpl() override; @@ -24,6 +27,7 @@ private: void enable_ephemeral_key_exchange(); void disable_compression(); void enforce_peer_certificate_verification(); + void set_provided_certificate_verification_callback(); }; } diff --git a/vespalib/src/vespa/vespalib/net/tls/peer_credentials.cpp b/vespalib/src/vespa/vespalib/net/tls/peer_credentials.cpp new file mode 100644 index 00000000000..ec8bb7ef9cd --- /dev/null +++ b/vespalib/src/vespa/vespalib/net/tls/peer_credentials.cpp @@ -0,0 +1,10 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include "peer_credentials.h" + +namespace vespalib::net::tls { + +PeerCredentials::PeerCredentials() = default; +PeerCredentials::~PeerCredentials() = default; + +} diff --git a/vespalib/src/vespa/vespalib/net/tls/peer_credentials.h b/vespalib/src/vespa/vespalib/net/tls/peer_credentials.h new file mode 100644 index 00000000000..802d5c0bd27 --- /dev/null +++ b/vespalib/src/vespa/vespalib/net/tls/peer_credentials.h @@ -0,0 +1,21 @@ +// 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 <vector> + +namespace vespalib::net::tls { + +// Simple wrapper of the information most useful to certificate verification code. +struct PeerCredentials { + // The last occurring (i.e. "most specific") CN present in the certificate, + // or the empty string if no CN is given (or if the CN is curiously empty). + vespalib::string common_name; + // 0-n DNS SAN entries. Note: "DNS:" prefix is not present in strings. + std::vector<vespalib::string> dns_sans; + + PeerCredentials(); + ~PeerCredentials(); +}; + +} diff --git a/vespalib/src/vespa/vespalib/net/tls/tls_context.cpp b/vespalib/src/vespa/vespalib/net/tls/tls_context.cpp index 467838975e7..cafa61898d7 100644 --- a/vespalib/src/vespa/vespalib/net/tls/tls_context.cpp +++ b/vespalib/src/vespa/vespalib/net/tls/tls_context.cpp @@ -4,8 +4,8 @@ namespace vespalib::net::tls { -std::unique_ptr<TlsContext> TlsContext::create_default_context(const TransportSecurityOptions& opts) { - return std::make_unique<impl::OpenSslTlsContextImpl>(opts); +std::shared_ptr<TlsContext> TlsContext::create_default_context(const TransportSecurityOptions& opts) { + return std::make_shared<impl::OpenSslTlsContextImpl>(opts); } } diff --git a/vespalib/src/vespa/vespalib/net/tls/tls_context.h b/vespalib/src/vespa/vespalib/net/tls/tls_context.h index 7292f43f88c..ce71d3e2ddb 100644 --- a/vespalib/src/vespa/vespalib/net/tls/tls_context.h +++ b/vespalib/src/vespa/vespalib/net/tls/tls_context.h @@ -10,7 +10,7 @@ class TransportSecurityOptions; struct TlsContext { virtual ~TlsContext() = default; - static std::unique_ptr<TlsContext> create_default_context(const TransportSecurityOptions&); + static std::shared_ptr<TlsContext> create_default_context(const TransportSecurityOptions&); }; } diff --git a/vespalib/src/vespa/vespalib/net/tls/tls_crypto_engine.cpp b/vespalib/src/vespa/vespalib/net/tls/tls_crypto_engine.cpp index 285a53dabc5..912fea534b2 100644 --- a/vespalib/src/vespa/vespalib/net/tls/tls_crypto_engine.cpp +++ b/vespalib/src/vespa/vespalib/net/tls/tls_crypto_engine.cpp @@ -15,7 +15,7 @@ std::unique_ptr<TlsCryptoSocket> TlsCryptoEngine::create_tls_crypto_socket(SocketHandle socket, bool is_server) { auto mode = is_server ? net::tls::CryptoCodec::Mode::Server : net::tls::CryptoCodec::Mode::Client; - auto codec = net::tls::CryptoCodec::create_default_codec(*_tls_ctx, mode); + auto codec = net::tls::CryptoCodec::create_default_codec(_tls_ctx, mode); return std::make_unique<net::tls::CryptoCodecAdapter>(std::move(socket), std::move(codec)); } diff --git a/vespalib/src/vespa/vespalib/net/tls/tls_crypto_engine.h b/vespalib/src/vespa/vespalib/net/tls/tls_crypto_engine.h index b6fda7fd577..07715b6995f 100644 --- a/vespalib/src/vespa/vespalib/net/tls/tls_crypto_engine.h +++ b/vespalib/src/vespa/vespalib/net/tls/tls_crypto_engine.h @@ -15,9 +15,9 @@ namespace vespalib { class TlsCryptoEngine : public CryptoEngine { private: - std::unique_ptr<net::tls::TlsContext> _tls_ctx; + std::shared_ptr<net::tls::TlsContext> _tls_ctx; public: - TlsCryptoEngine(net::tls::TransportSecurityOptions tls_opts); + explicit TlsCryptoEngine(net::tls::TransportSecurityOptions tls_opts); std::unique_ptr<TlsCryptoSocket> create_tls_crypto_socket(SocketHandle socket, bool is_server); CryptoSocket::UP create_crypto_socket(SocketHandle socket, bool is_server) override { return create_tls_crypto_socket(std::move(socket), is_server); diff --git a/vespalib/src/vespa/vespalib/net/tls/transport_security_options.cpp b/vespalib/src/vespa/vespalib/net/tls/transport_security_options.cpp index 4e39fe4d7fa..e828010019c 100644 --- a/vespalib/src/vespa/vespalib/net/tls/transport_security_options.cpp +++ b/vespalib/src/vespa/vespalib/net/tls/transport_security_options.cpp @@ -2,9 +2,32 @@ #include "transport_security_options.h" #include <openssl/crypto.h> +#include <cassert> namespace vespalib::net::tls { +TransportSecurityOptions::TransportSecurityOptions(vespalib::string ca_certs_pem, + vespalib::string cert_chain_pem, + vespalib::string private_key_pem) + : _ca_certs_pem(std::move(ca_certs_pem)), + _cert_chain_pem(std::move(cert_chain_pem)), + _private_key_pem(std::move(private_key_pem)), + _cert_verify_callback(std::make_shared<AcceptAllPreVerifiedCertificates>()) +{ +} + +TransportSecurityOptions::TransportSecurityOptions(vespalib::string ca_certs_pem, + vespalib::string cert_chain_pem, + vespalib::string private_key_pem, + std::shared_ptr<CertificateVerificationCallback> cert_verify_callback) + : _ca_certs_pem(std::move(ca_certs_pem)), + _cert_chain_pem(std::move(cert_chain_pem)), + _private_key_pem(std::move(private_key_pem)), + _cert_verify_callback(std::move(cert_verify_callback)) +{ + assert(_cert_verify_callback.get() != nullptr); +} + TransportSecurityOptions::~TransportSecurityOptions() { OPENSSL_cleanse(&_private_key_pem[0], _private_key_pem.size()); } diff --git a/vespalib/src/vespa/vespalib/net/tls/transport_security_options.h b/vespalib/src/vespa/vespalib/net/tls/transport_security_options.h index 0a228388791..2cc3e701724 100644 --- a/vespalib/src/vespa/vespalib/net/tls/transport_security_options.h +++ b/vespalib/src/vespa/vespalib/net/tls/transport_security_options.h @@ -2,7 +2,9 @@ #pragma once +#include "certificate_verification_callback.h" #include <vespa/vespalib/stllike/string.h> +#include <memory> namespace vespalib::net::tls { @@ -10,21 +12,28 @@ class TransportSecurityOptions { vespalib::string _ca_certs_pem; vespalib::string _cert_chain_pem; vespalib::string _private_key_pem; + std::shared_ptr<CertificateVerificationCallback> _cert_verify_callback; public: TransportSecurityOptions() = default; + // Construct transport options with a default certificate verification callback + // which accepts all certificates correctly signed by the given CA(s). TransportSecurityOptions(vespalib::string ca_certs_pem, vespalib::string cert_chain_pem, - vespalib::string private_key_pem) - : _ca_certs_pem(std::move(ca_certs_pem)), - _cert_chain_pem(std::move(cert_chain_pem)), - _private_key_pem(std::move(private_key_pem)) - {} + vespalib::string private_key_pem); + + TransportSecurityOptions(vespalib::string ca_certs_pem, + vespalib::string cert_chain_pem, + vespalib::string private_key_pem, + std::shared_ptr<CertificateVerificationCallback> cert_verify_callback); ~TransportSecurityOptions(); const vespalib::string& ca_certs_pem() const noexcept { return _ca_certs_pem; } const vespalib::string& cert_chain_pem() const noexcept { return _cert_chain_pem; } const vespalib::string& private_key_pem() const noexcept { return _private_key_pem; } + const std::shared_ptr<CertificateVerificationCallback>& cert_verify_callback() const noexcept { + return _cert_verify_callback; + } }; } diff --git a/vespalib/src/vespa/vespalib/test/make_tls_options_for_testing.cpp b/vespalib/src/vespa/vespalib/test/make_tls_options_for_testing.cpp index e70914dec2f..c685bffc23e 100644 --- a/vespalib/src/vespa/vespalib/test/make_tls_options_for_testing.cpp +++ b/vespalib/src/vespa/vespalib/test/make_tls_options_for_testing.cpp @@ -5,14 +5,14 @@ /* * Generated with the following commands: * - * openssl ecparam -name prime256v1 -genkey -out ca.key + * openssl ecparam -name prime256v1 -genkey -noout -out ca.key * * openssl req -new -x509 -nodes -key ca.key \ * -sha256 -out ca.pem \ * -subj '/C=US/L=LooneyVille/O=ACME/OU=ACME test CA/CN=acme.example.com' \ * -days 10000 * - * openssl ecparam -name prime256v1 -genkey -out host.key + * openssl ecparam -name prime256v1 -genkey -noout -out host.key * * openssl req -new -key host.key -out host.csr \ * -subj '/C=US/L=LooneyVille/O=Wile. E. Coyote, Ltd./CN=wile.example.com' \ @@ -59,10 +59,7 @@ zL06UPI= -----END CERTIFICATE-----)"; // host.key -constexpr const char* key_pem = R"(-----BEGIN EC PARAMETERS----- -BggqhkjOPQMBBw== ------END EC PARAMETERS----- ------BEGIN EC PRIVATE KEY----- +constexpr const char* key_pem = R"(-----BEGIN EC PRIVATE KEY----- MHcCAQEEID6di2PFYn8hPrxPbkFDGkSqF+K8L520In7nx3g0jwzOoAoGCCqGSM49 AwEHoUQDQgAEe+Y4hxt66em0STviGUj6ZDbxzoLoubXWRml8JDFrEc2S2433KWw2 npxYSKVCyo3a/Vo33V8/H0WgOXioKEZJxA== |