summaryrefslogtreecommitdiffstats
path: root/vespalib/src/tests/net
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/src/tests/net
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/src/tests/net')
-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
4 files changed, 642 insertions, 7 deletions
diff --git a/vespalib/src/tests/net/tls/openssl_impl/CMakeLists.txt b/vespalib/src/tests/net/tls/openssl_impl/CMakeLists.txt
index 799e2291d7c..e8f77d36e16 100644
--- a/vespalib/src/tests/net/tls/openssl_impl/CMakeLists.txt
+++ b/vespalib/src/tests/net/tls/openssl_impl/CMakeLists.txt
@@ -1,6 +1,7 @@
# Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
vespa_add_executable(vespalib_net_tls_openssl_impl_test_app TEST
SOURCES
+ crypto_utils.cpp
openssl_impl_test.cpp
DEPENDS
vespalib
diff --git a/vespalib/src/tests/net/tls/openssl_impl/crypto_utils.cpp b/vespalib/src/tests/net/tls/openssl_impl/crypto_utils.cpp
new file mode 100644
index 00000000000..14755360b51
--- /dev/null
+++ b/vespalib/src/tests/net/tls/openssl_impl/crypto_utils.cpp
@@ -0,0 +1,341 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#include "crypto_utils.h"
+#include <vespa/vespalib/net/tls/crypto_exception.h>
+#include <cassert>
+#include <openssl/bn.h>
+#include <openssl/rand.h>
+#include <openssl/x509v3.h>
+
+namespace vespalib::net::tls::impl {
+
+namespace {
+
+struct EvpPkeyCtxDeleter {
+ void operator()(::EVP_PKEY_CTX* ctx) const noexcept {
+ ::EVP_PKEY_CTX_free(ctx);
+ }
+};
+
+using EvpPkeyCtxPtr = std::unique_ptr<::EVP_PKEY_CTX, EvpPkeyCtxDeleter>;
+
+struct BignumDeleter {
+ void operator()(::BIGNUM* bn) const noexcept {
+ ::BN_free(bn);
+ }
+};
+
+using BignumPtr = std::unique_ptr<::BIGNUM, BignumDeleter>;
+
+struct Asn1IntegerDeleter {
+ void operator()(::ASN1_INTEGER* i) const noexcept {
+ ::ASN1_INTEGER_free(i);
+ }
+};
+
+using Asn1IntegerPtr = std::unique_ptr<::ASN1_INTEGER, Asn1IntegerDeleter>;
+
+struct X509ExtensionDeleter {
+ void operator()(X509_EXTENSION* ext) const noexcept {
+ ::X509_EXTENSION_free(ext);
+ }
+};
+
+using X509ExtensionPtr = std::unique_ptr<::X509_EXTENSION, X509ExtensionDeleter>;
+
+vespalib::string bio_to_string(BIO& bio) {
+ int written = BIO_pending(&bio);
+ assert(written >= 0);
+ vespalib::string pem_str(written, '\0');
+ if (::BIO_read(&bio, &pem_str[0], written) != written) {
+ throw CryptoException("BIO_read did not copy all PEM data");
+ }
+ return pem_str;
+}
+
+BioPtr new_memory_bio() {
+ BioPtr bio(::BIO_new(::BIO_s_mem()));
+ if (!bio) {
+ throw CryptoException("BIO_new(BIO_s_mem())");
+ }
+ return bio;
+}
+
+void assign_random_positive_serial_number(::X509& cert) {
+ /*
+ * From RFC3280, section 4.1.2.2:
+ * "The serial number MUST be a positive integer assigned by the CA to
+ * each certificate. It MUST be unique for each certificate issued by a
+ * given CA (i.e., the issuer name and serial number identify a unique
+ * certificate). CAs MUST force the serialNumber to be a non-negative
+ * integer.
+ * Given the uniqueness requirements above, serial numbers can be
+ * expected to contain long integers. Certificate users MUST be able to
+ * handle serialNumber values up to 20 octets. (...)"
+ */
+ unsigned char rand_buf[20];
+ // Could also have used BN_rand() for this, but RAND_bytes is just as simple
+ // for our purposes.
+ if (::RAND_bytes(rand_buf, sizeof(rand_buf)) != 1) {
+ throw CryptoException("RAND_bytes");
+ }
+ // X509 serial numbers must be positive, so clear the MSB of the
+ // bignum-to-be. Binary buffer to BIGNUM is interpreted as big endian.
+ rand_buf[0] &= 0x7f;
+ BignumPtr bn(::BN_bin2bn(rand_buf, sizeof(rand_buf), nullptr));
+ if (!bn) {
+ throw CryptoException("BN_bin2bn");
+ }
+ Asn1IntegerPtr bn_as_asn1(::BN_to_ASN1_INTEGER(bn.get(), nullptr));
+ if (!bn_as_asn1) {
+ throw CryptoException("BN_to_ASN1_INTEGER");
+ }
+ if (!::X509_set_serialNumber(&cert, bn_as_asn1.get())) { // Makes internal copy of bignum
+ throw CryptoException("X509_set_serialNumber");
+ }
+}
+
+void set_certificate_expires_from_now(::X509& cert, std::chrono::seconds valid_for) {
+ if (::X509_gmtime_adj(X509_get_notBefore(&cert), 0) == nullptr) {
+ throw CryptoException("X509_gmtime_adj(X509_get_notBefore())");
+ }
+ if (::X509_gmtime_adj(X509_get_notAfter(&cert), valid_for.count()) == nullptr) {
+ throw CryptoException("X509_gmtime_adj(X509_get_notAfter())");
+ }
+}
+
+void set_name_entry_if_non_empty(::X509_NAME& name, const char* field, vespalib::stringref entry) {
+ if (entry.empty()) {
+ return;
+ }
+ assert(entry.size() <= INT_MAX);
+ auto* entry_buf = reinterpret_cast<const unsigned char*>(entry.data());
+ auto entry_len = static_cast<int>(entry.size());
+ if (::X509_NAME_add_entry_by_txt(&name, field, MBSTRING_UTF8, entry_buf, entry_len, -1, 0) != 1) {
+ throw CryptoException("X509_NAME_add_entry_by_txt");
+ }
+}
+
+void assign_subject_distinguished_name(::X509_NAME& name, const X509Certificate::DistinguishedName& dn) {
+ set_name_entry_if_non_empty(name, "C", dn._country);
+ set_name_entry_if_non_empty(name, "ST", dn._state);
+ set_name_entry_if_non_empty(name, "L", dn._locality);
+ set_name_entry_if_non_empty(name, "O", dn._organization);
+ set_name_entry_if_non_empty(name, "OU", dn._organizational_unit);
+ for (auto& cn : dn._common_names) {
+ set_name_entry_if_non_empty(name, "CN", cn);
+ }
+}
+
+// `value` string is taken by value since X509V3_EXT_conf_nid takes in a mutable char*
+// and who knows what terrible things it might do to it (we must also ensure null
+// termination of the string).
+void add_v3_ext(::X509& subject, ::X509& issuer, int nid, vespalib::string value) {
+ // We are now reaching a point where the API we need to use is not properly documented.
+ // This functionality is inferred from https://opensource.apple.com/source/OpenSSL/OpenSSL-22/openssl/demos/x509/mkcert.c
+ ::X509V3_CTX ctx;
+ X509V3_set_ctx_nodb(&ctx);
+ // Need to set the certificate(s) so that e.g. subjectKeyIdentifier can find
+ // the correct public key to hash.
+ ::X509V3_set_ctx(&ctx, &issuer, &subject, /*CSR*/nullptr, /*CRL*/nullptr, /*flags*/0);
+ X509ExtensionPtr ext(::X509V3_EXT_conf_nid(nullptr, &ctx, nid, &value[0]));
+ if (!ext) {
+ throw CryptoException("X509V3_EXT_conf_nid");
+ }
+ // Makes internal copy of ext.
+ // Return value semantics not documented; inferred from reading source...
+ if (::X509_add_ext(&subject, ext.get(), -1) != 1) {
+ throw CryptoException("X509_add_ext");
+ }
+}
+
+void add_any_subject_alternate_names(::X509& subject, ::X509& issuer,
+ const std::vector<vespalib::string>& sans) {
+ // There can only be 1 SAN entry in a valid cert, but it can have multiple
+ // logical entries separated by commas in a single string.
+ vespalib::string san_csv;
+ for (auto& san : sans) {
+ if (!san_csv.empty()) {
+ san_csv.append(',');
+ }
+ san_csv.append(san);
+ }
+ if (!san_csv.empty()) {
+ add_v3_ext(subject, issuer, NID_subject_alt_name, std::move(san_csv));
+ }
+}
+
+} // anon ns
+
+vespalib::string PrivateKey::private_to_pem() const {
+ BioPtr bio = new_memory_bio();
+ // TODO this API is const-broken even on 1.1.1, revisit in the future...
+ auto* mutable_pkey = const_cast<::EVP_PKEY*>(_pkey.get());
+ if (::PEM_write_bio_PrivateKey(bio.get(), mutable_pkey, nullptr, nullptr,
+ 0, nullptr, nullptr) != 1) {
+ throw CryptoException("PEM_write_bio_PrivateKey");
+ }
+ return bio_to_string(*bio);
+}
+
+std::shared_ptr<PrivateKey> PrivateKey::generate_p256_ec_key() {
+ // We first have to generate an EVP context for the keygen _parameters_...
+ EvpPkeyCtxPtr params_ctx(::EVP_PKEY_CTX_new_id(EVP_PKEY_EC, nullptr));
+ if (!params_ctx) {
+ throw CryptoException("EVP_PKEY_CTX_new_id");
+ }
+ if (::EVP_PKEY_paramgen_init(params_ctx.get()) != 1) {
+ throw CryptoException("EVP_PKEY_paramgen_init");
+ }
+ // Set EC keygen parameters to use prime256v1, which is the same as P-256
+ if (EVP_PKEY_CTX_set_ec_paramgen_curve_nid(params_ctx.get(), NID_X9_62_prime256v1) <= 0) {
+ throw CryptoException("EVP_PKEY_CTX_set_ec_paramgen_curve_nid");
+ }
+#if (OPENSSL_VERSION_NUMBER >= 0x10100000L)
+ // Must tag _explicitly_ as a named curve or many won't accept our pretty keys.
+ // If we don't do this, explicit curve parameters are included with the key,
+ // and this is not widely supported nor needed since we're generating a key on
+ // a standardized curve.
+ if (EVP_PKEY_CTX_set_ec_param_enc(params_ctx.get(), OPENSSL_EC_NAMED_CURVE) <= 0) {
+ throw CryptoException("EVP_PKEY_CTX_set_ec_param_enc");
+ }
+#endif
+ // Note: despite being an EVP_PKEY this is not an actual key, just key parameters!
+ ::EVP_PKEY* params_raw = nullptr;
+ if (::EVP_PKEY_paramgen(params_ctx.get(), &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