Commit fe26a8fc90 for openssl.org

commit fe26a8fc907106bb66aca4fb056b9bb75bf0341e
Author: Tim Perry <pimterry@gmail.com>
Date:   Sun Mar 15 17:00:48 2026 +0100

    Add CTLOG_STORE_add0_log() to add CTLOGs to a store programmatically

    Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
    Reviewed-by: Eugene Syromiatnikov <esyr@openssl.org>
    MergeDate: Thu Mar 19 20:45:34 2026
    (Merged from https://github.com/openssl/openssl/pull/30427)

diff --git a/CHANGES.md b/CHANGES.md
index 90069f94d7..6df6c90a67 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -41,6 +41,10 @@ OpenSSL Releases

    *Paul Louvel*

+ * Added `CTLOG_STORE_add0_log()` to add individual CT logs to a `CTLOG_STORE`.
+
+   *Tim Perry*
+
  * Dropped `no-ecdsa` and `no-ecdh` options from `Configure` as these options
    did not really disable the implementations. Use `no-ec` to disable the
    elliptic curve support.
diff --git a/crypto/ct/ct_log.c b/crypto/ct/ct_log.c
index b9990dc9a0..6fad6a6bd4 100644
--- a/crypto/ct/ct_log.c
+++ b/crypto/ct/ct_log.c
@@ -315,6 +315,20 @@ EVP_PKEY *CTLOG_get0_public_key(const CTLOG *log)
     return log->public_key;
 }

+int CTLOG_STORE_add0_log(CTLOG_STORE *store, CTLOG *log)
+{
+    if (store == NULL || log == NULL) {
+        ERR_raise(ERR_LIB_CT, ERR_R_PASSED_NULL_PARAMETER);
+        return 0;
+    }
+
+    if (!sk_CTLOG_push(store->logs, log)) {
+        ERR_raise(ERR_LIB_CT, ERR_R_CRYPTO_LIB);
+        return 0;
+    }
+    return 1;
+}
+
 /*
  * Given a log ID, finds the matching log.
  * Returns NULL if no match found.
diff --git a/doc/man3/CTLOG_STORE_new.pod b/doc/man3/CTLOG_STORE_new.pod
index 361eda57b1..f342637012 100644
--- a/doc/man3/CTLOG_STORE_new.pod
+++ b/doc/man3/CTLOG_STORE_new.pod
@@ -4,6 +4,7 @@

 CTLOG_STORE_new_ex,
 CTLOG_STORE_new, CTLOG_STORE_free,
+CTLOG_STORE_add0_log,
 CTLOG_STORE_load_default_file, CTLOG_STORE_load_file -
 Create and populate a Certificate Transparency log list

@@ -15,14 +16,17 @@ Create and populate a Certificate Transparency log list
  CTLOG_STORE *CTLOG_STORE_new(void);
  void CTLOG_STORE_free(CTLOG_STORE *store);

+ int CTLOG_STORE_add0_log(CTLOG_STORE *store, CTLOG *log);
+
  int CTLOG_STORE_load_default_file(CTLOG_STORE *store);
  int CTLOG_STORE_load_file(CTLOG_STORE *store, const char *file);

 =head1 DESCRIPTION

 A CTLOG_STORE is a container for a list of CTLOGs (Certificate Transparency
-logs). The list can be loaded from one or more files and then searched by LogID
-(see RFC 6962, Section 3.2, for the definition of a LogID).
+logs). The list can be loaded from one or more files, or populated
+programmatically, and then searched by LogID (see RFC 6962, Section 3.2, for
+the definition of a LogID).

 CTLOG_STORE_new_ex() creates an empty list of CT logs associated with
 the library context I<libctx> and the property query string I<propq>.
@@ -30,8 +34,10 @@ the library context I<libctx> and the property query string I<propq>.
 CTLOG_STORE_new() does the same thing as CTLOG_STORE_new_ex() but with
 the default library context and property query string.

-The CTLOG_STORE is then populated by CTLOG_STORE_load_default_file() or
-CTLOG_STORE_load_file(). CTLOG_STORE_load_default_file() loads from the default
+The CTLOG_STORE is then populated by CTLOG_STORE_load_default_file(),
+CTLOG_STORE_load_file(), or CTLOG_STORE_add0_log().
+
+CTLOG_STORE_load_default_file() loads from the default
 file, which is named F<ct_log_list.cnf> in OPENSSLDIR (see the output of
 L<openssl-version(1)>). This can be overridden using an environment variable
 named B<CTLOG_FILE>. CTLOG_STORE_load_file() loads from a caller-specified file
@@ -50,6 +56,11 @@ The expected format of the file is:
  description = Log 2
  key = <base64-encoded DER SubjectPublicKeyInfo here>

+CTLOG_STORE_add0_log() adds a single CTLOG (see L<CTLOG_new(3)>) to the store.
+On success, the store takes ownership of I<log> and the caller must not free
+it. On failure, the caller retains ownership and is responsible for freeing
+I<log>.
+
 Once a CTLOG_STORE is no longer required, it should be passed to
 CTLOG_STORE_free(). This will delete all of the CTLOGs stored within, along
 with the CTLOG_STORE itself. If the argument is NULL, nothing is done.
@@ -62,6 +73,8 @@ invalid if it is missing a "key" or "description" field.

 =head1 RETURN VALUES

+B<CTLOG_STORE_add0_log> returns 1 on success, 0 on failure.
+
 Both B<CTLOG_STORE_load_default_file> and B<CTLOG_STORE_load_file> return 1 if
 all CT logs in the file are successfully parsed and loaded, 0 otherwise.

@@ -73,8 +86,8 @@ L<SSL_CTX_set_ctlog_list_file(3)>

 =head1 HISTORY

-CTLOG_STORE_new_ex was added in OpenSSL 3.0. All other functions were
-added in OpenSSL 1.1.0.
+CTLOG_STORE_add0_log was added in OpenSSL 4.1. CTLOG_STORE_new_ex was added in
+OpenSSL 3.0. All other functions were added in OpenSSL 1.1.0.

 =head1 COPYRIGHT

diff --git a/include/openssl/ct.h.in b/include/openssl/ct.h.in
index a6103980d4..8c83f53a26 100644
--- a/include/openssl/ct.h.in
+++ b/include/openssl/ct.h.in
@@ -499,6 +499,14 @@ CTLOG_STORE *CTLOG_STORE_new(void);
  */
 void CTLOG_STORE_free(CTLOG_STORE *store);

+/*
+ * Adds a CT log to a CTLOG_STORE.
+ * Takes ownership of the CTLOG on success - the caller must not free it after
+ * a successful call. On failure, the caller retains ownership.
+ * Returns 1 on success, 0 on failure.
+ */
+__owur int CTLOG_STORE_add0_log(CTLOG_STORE *store, CTLOG *log);
+
 /*
  * Finds a CT log in the store based on its log ID.
  * Returns the CT log, or NULL if no match is found.
diff --git a/test/ct_test.c b/test/ct_test.c
index 62c7b9490f..51cad3eaae 100644
--- a/test/ct_test.c
+++ b/test/ct_test.c
@@ -508,6 +508,131 @@ static int test_ctlog_from_base64(void)
         return 0;
     return 1;
 }
+
+static int test_ctlog_store_add0_log(void)
+{
+    CTLOG_STORE *store = NULL;
+    CTLOG *log = NULL;
+    const CTLOG *found = NULL;
+    const uint8_t *log_id = NULL;
+    size_t log_id_len = 0;
+    int result = 0;
+    const char pkey_base64[] = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEmXg8sUUzwBYaWrRb+V0IopzQ6o3U"
+                               "yEJ04r5ZrRXGdpYM8K+hB0pXrGRLI0eeWz+3skXrS0IO83AhA3GpRL6s6w==";
+    const char name[] = "test log";
+
+    if (!TEST_ptr(store = CTLOG_STORE_new()))
+        goto end;
+    if (!TEST_true(CTLOG_new_from_base64(&log, pkey_base64, name)))
+        goto end;
+
+    CTLOG_get0_log_id(log, &log_id, &log_id_len);
+    if (!TEST_size_t_eq(log_id_len, CT_V1_HASHLEN))
+        goto end;
+    if (!TEST_ptr_null(CTLOG_STORE_get0_log_by_id(store, log_id, log_id_len)))
+        goto end;
+
+    if (!TEST_true(CTLOG_STORE_add0_log(store, log)))
+        goto end;
+    log = NULL;
+
+    found = CTLOG_STORE_get0_log_by_id(store, log_id, log_id_len);
+    if (!TEST_ptr(found))
+        goto end;
+    if (!TEST_str_eq(CTLOG_get0_name(found), name))
+        goto end;
+
+    result = 1;
+
+end:
+    CTLOG_STORE_free(store);
+    CTLOG_free(log);
+    return result;
+}
+
+static int test_ctlog_store_add0_log_validates_sct(void)
+{
+    CTLOG_STORE *store = NULL;
+    CTLOG *log = NULL;
+    CT_POLICY_EVAL_CTX *ct_policy_ctx = NULL;
+    X509 *cert = NULL, *issuer = NULL;
+    STACK_OF(SCT) *scts = NULL;
+    const X509_EXTENSION *sct_extension = NULL;
+    int result = 0;
+    const char pkey_base64[] = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEmXg8sUUzwBYaWrRb+V0IopzQ6o3U"
+                               "yEJ04r5ZrRXGdpYM8K+hB0pXrGRLI0eeWz+3skXrS0IO83AhA3GpRL6s6w==";
+
+    if (!TEST_ptr(store = CTLOG_STORE_new()))
+        goto end;
+    if (!TEST_true(CTLOG_new_from_base64(&log, pkey_base64, "test")))
+        goto end;
+    if (!TEST_true(CTLOG_STORE_add0_log(store, log)))
+        goto end;
+    log = NULL;
+
+    if (!TEST_ptr(ct_policy_ctx = CT_POLICY_EVAL_CTX_new()))
+        goto end;
+    CT_POLICY_EVAL_CTX_set_shared_CTLOG_STORE(ct_policy_ctx, store);
+    CT_POLICY_EVAL_CTX_set_time(ct_policy_ctx, 1580335307000ULL);
+
+    if (!TEST_ptr(cert = load_pem_cert(certs_dir, "embeddedSCTs1.pem")))
+        goto end;
+    if (!TEST_ptr(issuer = load_pem_cert(certs_dir, "embeddedSCTs1_issuer.pem")))
+        goto end;
+    CT_POLICY_EVAL_CTX_set1_cert(ct_policy_ctx, cert);
+    CT_POLICY_EVAL_CTX_set1_issuer(ct_policy_ctx, issuer);
+
+    sct_extension = X509_get_ext(cert,
+        X509_get_ext_by_NID(cert, NID_ct_precert_scts, -1));
+    if (!TEST_ptr(sct_extension))
+        goto end;
+    if (!TEST_ptr(scts = X509V3_EXT_d2i(sct_extension)))
+        goto end;
+    if (!TEST_int_eq(sk_SCT_num(scts), 1))
+        goto end;
+    if (!TEST_int_ge(SCT_LIST_validate(scts, ct_policy_ctx), 0))
+        goto end;
+    if (!TEST_int_eq(SCT_get_validation_status(sk_SCT_value(scts, 0)),
+            SCT_VALIDATION_STATUS_VALID))
+        goto end;
+
+    result = 1;
+
+end:
+    CTLOG_STORE_free(store);
+    CTLOG_free(log);
+    CT_POLICY_EVAL_CTX_free(ct_policy_ctx);
+    X509_free(cert);
+    X509_free(issuer);
+    SCT_LIST_free(scts);
+    return result;
+}
+
+static int test_ctlog_store_add0_log_null(void)
+{
+    CTLOG_STORE *store = NULL;
+    CTLOG *log = NULL;
+    int result = 0;
+    const char pkey_base64[] = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEmXg8sUUzwBYaWrRb+V0IopzQ6o3U"
+                               "yEJ04r5ZrRXGdpYM8K+hB0pXrGRLI0eeWz+3skXrS0IO83AhA3GpRL6s6w==";
+
+    if (!TEST_ptr(store = CTLOG_STORE_new()))
+        goto end;
+    if (!TEST_false(CTLOG_STORE_add0_log(store, NULL)))
+        goto end;
+
+    if (!TEST_true(CTLOG_new_from_base64(&log, pkey_base64, "test")))
+        goto end;
+    if (!TEST_false(CTLOG_STORE_add0_log(NULL, log)))
+        goto end;
+
+    result = 1;
+
+end:
+    CTLOG_STORE_free(store);
+    CTLOG_free(log);
+    return result;
+}
 #endif

 int setup_tests(void)
@@ -528,6 +653,9 @@ int setup_tests(void)
     ADD_TEST(test_encode_tls_sct);
     ADD_TEST(test_default_ct_policy_eval_ctx_time_is_now);
     ADD_TEST(test_ctlog_from_base64);
+    ADD_TEST(test_ctlog_store_add0_log);
+    ADD_TEST(test_ctlog_store_add0_log_validates_sct);
+    ADD_TEST(test_ctlog_store_add0_log_null);
 #else
     printf("No CT support\n");
 #endif
diff --git a/util/libcrypto.num b/util/libcrypto.num
index 8cbf49ac77..dc7da949c2 100644
--- a/util/libcrypto.num
+++ b/util/libcrypto.num
@@ -5715,3 +5715,4 @@ OPENSSL_sk_set_cmp_thunks               ?	4_0_0	EXIST::FUNCTION:
 ASN1_BIT_STRING_set1                    ?	4_0_0	EXIST::FUNCTION:
 OSSL_ESS_check_signing_certs_ex         ?	4_0_0	EXIST::FUNCTION:
 X509v3_delete_extension                 ?	4_0_0	EXIST::FUNCTION:
+CTLOG_STORE_add0_log                    ?	4_0_0	EXIST::FUNCTION:CT