This is an automated email from the ASF dual-hosted git repository.

JosiahWI pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/master by this push:
     new 56ddbbb34f Add tests for OpenSSL cert loading (#13350)
56ddbbb34f is described below

commit 56ddbbb34fbe7db767035190831920cfa8754a86
Author: JosiahWI <[email protected]>
AuthorDate: Fri Jul 31 10:59:45 2026 -0500

    Add tests for OpenSSL cert loading (#13350)
    
    This does not cover the ENGINE support, because that is a complicated test 
and the support will be removed for OpenSSL 4.
---
 src/iocore/net/unit_tests/test_SSLDHParams.cc | 183 ++++++++++++++++++++++++--
 src/iocore/net/unit_tests/unit_test_main.cc   |   5 +
 2 files changed, 175 insertions(+), 13 deletions(-)

diff --git a/src/iocore/net/unit_tests/test_SSLDHParams.cc 
b/src/iocore/net/unit_tests/test_SSLDHParams.cc
index 6a32a7458b..3e75f5fe09 100644
--- a/src/iocore/net/unit_tests/test_SSLDHParams.cc
+++ b/src/iocore/net/unit_tests/test_SSLDHParams.cc
@@ -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
 
@@ -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()
 {
@@ -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:
@@ -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")
@@ -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));
+}
diff --git a/src/iocore/net/unit_tests/unit_test_main.cc 
b/src/iocore/net/unit_tests/unit_test_main.cc
index 25b355f0b6..41b96438d9 100644
--- a/src/iocore/net/unit_tests/unit_test_main.cc
+++ b/src/iocore/net/unit_tests/unit_test_main.cc
@@ -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"
@@ -55,6 +56,10 @@ public:
     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);
 

Reply via email to