aboutsummaryrefslogtreecommitdiffstats
path: root/vespalib
diff options
context:
space:
mode:
authorTor Brede Vekterli <vekterli@oath.com>2018-10-12 14:45:40 +0000
committerTor Brede Vekterli <vekterli@oath.com>2018-10-15 13:23:11 +0000
commit0cca964deebf35edf2d918dbcebff4bf33f36ccd (patch)
tree1a37fb5b007782932dba3a1fee2883bf2fb662f7 /vespalib
parentc8e17ae332cae0ddeaf50a60eaf1ba5a7dc589b3 (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')
-rw-r--r--vespalib/src/tests/net/tls/openssl_impl/CMakeLists.txt1
-rw-r--r--vespalib/src/tests/net/tls/openssl_impl/crypto_utils.cpp341
-rw-r--r--vespalib/src/tests/net/tls/openssl_impl/crypto_utils.h134
-rw-r--r--vespalib/src/tests/net/tls/openssl_impl/openssl_impl_test.cpp173
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/CMakeLists.txt1
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/certificate_verification_callback.h28
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/crypto_codec.cpp8
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/crypto_codec.h2
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/impl/openssl_crypto_codec_impl.cpp7
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/impl/openssl_crypto_codec_impl.h8
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/impl/openssl_tls_context_impl.cpp175
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/impl/openssl_tls_context_impl.h4
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/peer_credentials.cpp10
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/peer_credentials.h21
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/tls_context.cpp4
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/tls_context.h2
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/tls_crypto_engine.cpp2
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/tls_crypto_engine.h4
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/transport_security_options.cpp23
-rw-r--r--vespalib/src/vespa/vespalib/net/tls/transport_security_options.h19
-rw-r--r--vespalib/src/vespa/vespalib/test/make_tls_options_for_testing.cpp9
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(), &params_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==