Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 170 additions & 13 deletions src/iocore/net/unit_tests/test_SSLDHParams.cc
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
/** @file

Catch based unit tests for the DH-parameter handling behavior of
SSLMultiCertConfigLoader::init_server_ssl_ctx, which is the inknet
public boundary that transitively invokes ssl_context_enable_dhe
and (when a file is configured) load_dhparams_file.
Catch based unit tests for two pieces of inknet SSL_CTX setup, each
exercised through its public SSLMultiCertConfigLoader boundary:

* The DH-parameter handling of init_server_ssl_ctx, which transitively
invokes ssl_context_enable_dhe and (when a file is configured)
load_dhparams_file.

* The private key handling of load_certs, which transitively invokes the
file-static SSLPrivateKeyHandler.

@section license License

Expand Down Expand Up @@ -38,14 +43,25 @@
#include <openssl/core_names.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/rsa.h>
#include <openssl/ssl.h>
#include <openssl/x509.h>

#include <cstdio>
#include <cstring>
#include <string>

namespace
{

std::string
bio_to_string(BIO *bio)
{
BUF_MEM *bm = nullptr;
REQUIRE(1 == BIO_get_mem_ptr(bio, &bm));
return std::string{bm->data, bm->length};
}

std::string
make_valid_dh_pem()
{
Expand All @@ -62,31 +78,77 @@ make_valid_dh_pem()
REQUIRE(EVP_PKEY_generate(pctx, &pkey) > 0);

BIO *bio = BIO_new(BIO_s_mem());
REQUIRE(bio != nullptr);
REQUIRE(PEM_write_bio_Parameters(bio, pkey) == 1);
BUF_MEM *bm = nullptr;
BIO_get_mem_ptr(bio, &bm);
std::string out{bm->data, bm->length};
std::string const out{bio_to_string(bio)};
BIO_free(bio);
EVP_PKEY_free(pkey);
EVP_PKEY_CTX_free(pctx);
return out;
}

// PEM-encodes pkey as a private key, optionally encrypting it with the given
// cipher and passphrase (cipher==nullptr leaves it unencrypted).
std::string
key_to_pem(EVP_PKEY *pkey, EVP_CIPHER const *cipher, char *pass)
{
BIO *bio = BIO_new(BIO_s_mem());
REQUIRE(bio != nullptr);
int passlen{pass ? static_cast<int>(std::strlen(pass)) : 0};
REQUIRE(PEM_write_bio_PrivateKey(bio, pkey, cipher, reinterpret_cast<unsigned char *>(pass), passlen, nullptr, nullptr) == 1);
std::string out{bio_to_string(bio)};
BIO_free(bio);
return out;
}

std::string
make_rsa_pem()
{
EVP_PKEY *pkey = EVP_RSA_gen(2048);
REQUIRE(pkey != nullptr);
BIO *bio = BIO_new(BIO_s_mem());
REQUIRE(PEM_write_bio_PrivateKey(bio, pkey, nullptr, nullptr, 0, nullptr, nullptr) == 1);
BUF_MEM *bm = nullptr;
BIO_get_mem_ptr(bio, &bm);
std::string out{bm->data, bm->length};
BIO_free(bio);
std::string const out{key_to_pem(pkey, nullptr, nullptr)};
EVP_PKEY_free(pkey);
return out;
}

// A self-signed certificate paired with the matching 2048-bit RSA private key,
// both PEM-encoded. Each call produces a fresh, independent key pair. When a
// cipher is given the key PEM is encrypted under the passphrase.
struct CertAndKey {
std::string cert_pem;
std::string key_pem;
};

CertAndKey
make_cert_and_key(EVP_CIPHER const *cipher = nullptr, char *pass = nullptr)
{
EVP_PKEY *pkey = EVP_RSA_gen(2048);
REQUIRE(pkey != nullptr);

X509 *x509 = X509_new();
REQUIRE(x509 != nullptr);
ASN1_INTEGER_set(X509_get_serialNumber(x509), 1);
X509_gmtime_adj(X509_getm_notBefore(x509), 0);
X509_gmtime_adj(X509_getm_notAfter(x509), 60L * 60L * 24L * 365L);
REQUIRE(X509_set_pubkey(x509, pkey) == 1);

X509_NAME *name = X509_get_subject_name(x509);
X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, reinterpret_cast<unsigned char const *>("ats-test"), -1, -1, 0);
REQUIRE(X509_set_issuer_name(x509, name) == 1);
REQUIRE(X509_sign(x509, pkey, EVP_sha256()) > 0);

BIO *cert_bio = BIO_new(BIO_s_mem());
REQUIRE(cert_bio != nullptr);
REQUIRE(PEM_write_bio_X509(cert_bio, x509) == 1);
std::string const cert_pem{bio_to_string(cert_bio)};
BIO_free(cert_bio);
X509_free(x509);

std::string const key_pem{key_to_pem(pkey, cipher, pass)};
EVP_PKEY_free(pkey);
return {cert_pem, key_pem};
}

class TempFile
{
public:
Expand Down Expand Up @@ -140,6 +202,55 @@ init_with_dhparams(char const *dhparams_file)
return ok;
}

// A fixed-passphrase callback, matching how SSLPrivateKeyHandler consults the
// SSL_CTX default password callback to decrypt an encrypted private key.
char test_passphrase[]{"ats-secret-pass"};

int
fixed_passphrase_cb(char *buf, int size, int /* rwflag */, void * /* u */)
{
int len{static_cast<int>(std::strlen(test_passphrase))};
if (len > size) {
len = size;
}
std::memcpy(buf, test_passphrase, len);
return len;
}

// Drives SSLPrivateKeyHandler via the public static load_certs boundary,
// holding the certificate fixed and valid so the only variable under test is
// the private key material. The certificate and key are read from real files,
// exactly as a production ssl_multicert entry would be, so that the file-load
// path (load_rsa_pkey_from_file) is genuinely exercised.
//
// An empty key_path selects the "key bundled in the certificate file" branch,
// where the file load is skipped and the key is read from the certificate
// secret. A non-null passwd_cb is installed as the SSL_CTX default password
// callback, exactly as init_server_ssl_ctx's dialog setup would do for an
// encrypted key.
bool
load_key_via_load_certs(char const *cert_path, char const *key_path, pem_password_cb *passwd_cb = nullptr)
{
SSLConfigParams params;
SSLMultiCertConfigParams settings;
settings.cert = ats_strdup(cert_path);

SSLMultiCertConfigLoader::CertLoadData data;
data.cert_names_list.emplace_back(cert_path);
data.key_list.emplace_back(key_path);

SSL_CTX *ctx = SSL_CTX_new(TLS_server_method());
REQUIRE(ctx != nullptr);
if (passwd_cb != nullptr) {
SSL_CTX_set_default_passwd_cb(ctx, passwd_cb);
}

bool ok = SSLMultiCertConfigLoader::load_certs(ctx, data.cert_names_list, data.key_list, data, &params, &settings);

SSL_CTX_free(ctx);
return ok;
}

} // namespace

TEST_CASE("ssl_context_enable_dhe: nullptr dhparams file falls back to built-in DH parameters")
Expand Down Expand Up @@ -184,3 +295,49 @@ TEST_CASE("ssl_context_enable_dhe: truncated DH PEM (missing END marker) is reje
TempFile truncated{pem.substr(0, end)};
CHECK_FALSE(init_with_dhparams(truncated.get_path()));
}

TEST_CASE("SSLPrivateKeyHandler: a key file matching the certificate is loaded")
{
CertAndKey ck = make_cert_and_key();
TempFile cert{ck.cert_pem};
TempFile key{ck.key_pem};
CHECK(load_key_via_load_certs(cert.get_path(), key.get_path()));
}

TEST_CASE("SSLPrivateKeyHandler: an empty key path loads the key bundled in the certificate file")
{
CertAndKey ck = make_cert_and_key();
TempFile cert{ck.cert_pem + ck.key_pem};
CHECK(load_key_via_load_certs(cert.get_path(), ""));
}

TEST_CASE("SSLPrivateKeyHandler: a valid key file not matching the certificate is rejected")
{
TempFile cert{make_cert_and_key().cert_pem};
TempFile key{make_cert_and_key().key_pem};
CHECK_FALSE(load_key_via_load_certs(cert.get_path(), key.get_path()));
}

TEST_CASE("SSLPrivateKeyHandler: an unparseable key file is rejected")
{
TempFile cert{make_cert_and_key().cert_pem};
TempFile key{"-----BEGIN PRIVATE KEY-----\nnot base64\n-----END PRIVATE KEY-----\n"};
CHECK_FALSE(load_key_via_load_certs(cert.get_path(), key.get_path()));
}

TEST_CASE("SSLPrivateKeyHandler: an encrypted key file is decrypted via the SSL_CTX password callback")
{
CertAndKey ck = make_cert_and_key(EVP_aes_256_cbc(), test_passphrase);
TempFile cert{ck.cert_pem};
TempFile key{ck.key_pem};
CHECK(load_key_via_load_certs(cert.get_path(), key.get_path(), fixed_passphrase_cb));
}

TEST_CASE("SSLPrivateKeyHandler: an encrypted key file with the wrong passphrase is rejected")
{
char wrong_pass[]{"the-wrong-passphrase"};
CertAndKey ck = make_cert_and_key(EVP_aes_256_cbc(), wrong_pass);
TempFile cert{ck.cert_pem};
TempFile key{ck.key_pem};
CHECK_FALSE(load_key_via_load_certs(cert.get_path(), key.get_path(), fixed_passphrase_cb));
}
5 changes: 5 additions & 0 deletions src/iocore/net/unit_tests/unit_test_main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

#include "iocore/eventsystem/EventSystem.h"
#include "../P_SSLConfig.h"
#include "api/LifecycleAPIHooks.h"
#include "records/RecordsConfig.h"
#include "tscore/BaseLogFile.h"
#include "tscore/Diags.h"
Expand Down Expand Up @@ -55,6 +56,10 @@ class EventProcessorListener final : public Catch::EventListenerBase
RecProcessInit();
LibRecordsConfigInit();

// SSLSecret::loadSecret consults the global lifecycle hooks for the
// SSL_SECRET hook, so they must be allocated before any secret is loaded.
init_global_lifecycle_hooks();

ink_event_system_init(EVENT_SYSTEM_MODULE_PUBLIC_VERSION);
eventProcessor.start(test_threads);

Expand Down