diff options
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== |