Commit e3c18094e3 for openssl.org

commit e3c18094e301cb547ff7fefeaae38a0ec2481fc5
Author: Bob Beck <beck@openssl.org>
Date:   Sat Sep 5 11:06:58 2026 -0600

    Cache the signed encoding when signing an ASN.1 item

    ASN1_item_sign_ctx() re-encodes the part being signed, signs the bytes
    and discards them, leaving the object's cached encoding marked stale.
    A freshly signed certificate, CRL or request therefore stayed
    "modified" for life, unlike a decoded one: i2d re-encoded it on every
    use and X509_cmp() and X509_CRL_match() skipped the encoding compare.

    Save the signed bytes into the item's ASN1_ENCODING once the signature
    succeeds, so a signed object is in the same state as a decoded one.
    Items without an encoding cache are unaffected, and modifying the
    object afterwards marks the encoding stale again, as before.

    Reviewed-by: Mounir Idrassi <mounir.idrassi@idrix.fr>
    Reviewed-by: Nikola Pajkovsky <nikolap@openssl.org>
    Merge-date: Thu Sep 17 16:44:30 2026
    Merged-from: https://github.com/openssl/openssl/pull/32686

diff --git a/crypto/asn1/a_sign.c b/crypto/asn1/a_sign.c
index 58d58f81ff..3bfc32cd99 100644
--- a/crypto/asn1/a_sign.c
+++ b/crypto/asn1/a_sign.c
@@ -21,6 +21,7 @@
 #include <openssl/core_names.h>
 #include "crypto/asn1.h"
 #include "crypto/evp.h"
+#include "asn1_local.h"

 #ifndef OPENSSL_NO_DEPRECATED_3_0

@@ -282,6 +283,14 @@ int ASN1_item_sign_ctx(const ASN1_ITEM *it, X509_ALGOR *algor1,
         ERR_raise(ERR_LIB_ASN1, ERR_R_EVP_LIB);
         goto err;
     }
+    /* Only a sequence item carries a cached encoding */
+    if ((it->itype == ASN1_ITYPE_SEQUENCE
+            || it->itype == ASN1_ITYPE_NDEF_SEQUENCE)
+        && !ossl_asn1_enc_save((ASN1_VALUE **)&data, buf_in, buf_len, it)) {
+        outl = 0;
+        ERR_raise(ERR_LIB_ASN1, ERR_R_ASN1_LIB);
+        goto err;
+    }
     ASN1_STRING_set0(signature, buf_out, (int)outl);
     buf_out = NULL;
     /*
diff --git a/test/x509_internal_test.c b/test/x509_internal_test.c
index 1e91b5bbd4..d74846e5c4 100644
--- a/test/x509_internal_test.c
+++ b/test/x509_internal_test.c
@@ -1273,8 +1273,91 @@ err:
     return ret;
 }

+/*
+ * Signing leaves the cached encoding of the signed part current and equal
+ * to the decoded one; modifying the object afterwards marks it stale.
+ */
+static int test_sign_caches_encoding(void)
+{
+    EVP_PKEY *pkey = NULL;
+    X509_NAME *name = NULL;
+    X509 *cert = NULL, *cert_copy = NULL;
+    X509_CRL *crl = NULL, *crl_copy = NULL;
+    X509_REQ *req = NULL, *req_copy = NULL;
+    int ret = 0;
+
+    if (!TEST_ptr(pkey = EVP_PKEY_Q_keygen(NULL, NULL, "RSA", (size_t)2048))
+        || !TEST_ptr(name = X509_NAME_new())
+        || !TEST_true(X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC,
+            (const unsigned char *)"sign test", -1, -1, 0)))
+        goto err;
+
+    /* Certificate */
+    if (!TEST_ptr(cert = X509_new())
+        || !TEST_true(cert->cert_info.enc.modified)
+        || !TEST_true(X509_set_subject_name(cert, name))
+        || !TEST_true(X509_set_issuer_name(cert, name))
+        || !TEST_ptr(X509_gmtime_adj(X509_getm_notBefore(cert), 0))
+        || !TEST_ptr(X509_gmtime_adj(X509_getm_notAfter(cert), 3600))
+        || !TEST_true(X509_set_pubkey(cert, pkey))
+        || !TEST_int_gt(X509_sign(cert, pkey, EVP_sha256()), 0)
+        || !TEST_false(cert->cert_info.enc.modified)
+        || !TEST_ptr(cert_copy = X509_dup(cert))
+        || !TEST_false(cert_copy->cert_info.enc.modified)
+        || !TEST_mem_eq(cert->cert_info.enc.enc,
+            (size_t)cert->cert_info.enc.len,
+            cert_copy->cert_info.enc.enc,
+            (size_t)cert_copy->cert_info.enc.len)
+        || !TEST_int_eq(X509_cmp(cert, cert_copy), 0)
+        || !TEST_true(X509_set_version(cert, X509_VERSION_2))
+        || !TEST_true(cert->cert_info.enc.modified)
+        || !TEST_int_gt(X509_sign(cert, pkey, EVP_sha256()), 0)
+        || !TEST_false(cert->cert_info.enc.modified)
+        || !TEST_int_ne(X509_cmp(cert, cert_copy), 0))
+        goto err;
+
+    /* CRL */
+    if (!TEST_ptr(crl = X509_CRL_new())
+        || !TEST_true(crl->crl.enc.modified)
+        || !TEST_true(X509_CRL_set_issuer_name(crl, name))
+        || !TEST_true(X509_CRL_set1_lastUpdate(crl, X509_getm_notBefore(cert)))
+        || !TEST_int_gt(X509_CRL_sign(crl, pkey, EVP_sha256()), 0)
+        || !TEST_false(crl->crl.enc.modified)
+        || !TEST_ptr(crl_copy = X509_CRL_dup(crl))
+        || !TEST_false(crl_copy->crl.enc.modified)
+        || !TEST_mem_eq(crl->crl.enc.enc, (size_t)crl->crl.enc.len,
+            crl_copy->crl.enc.enc, (size_t)crl_copy->crl.enc.len))
+        goto err;
+
+    /* Request */
+    if (!TEST_ptr(req = X509_REQ_new())
+        || !TEST_true(req->req_info.enc.modified)
+        || !TEST_true(X509_REQ_set_subject_name(req, name))
+        || !TEST_true(X509_REQ_set_pubkey(req, pkey))
+        || !TEST_int_gt(X509_REQ_sign(req, pkey, EVP_sha256()), 0)
+        || !TEST_false(req->req_info.enc.modified)
+        || !TEST_ptr(req_copy = X509_REQ_dup(req))
+        || !TEST_false(req_copy->req_info.enc.modified)
+        || !TEST_mem_eq(req->req_info.enc.enc, (size_t)req->req_info.enc.len,
+            req_copy->req_info.enc.enc, (size_t)req_copy->req_info.enc.len))
+        goto err;
+
+    ret = 1;
+err:
+    X509_REQ_free(req_copy);
+    X509_REQ_free(req);
+    X509_CRL_free(crl_copy);
+    X509_CRL_free(crl);
+    X509_free(cert_copy);
+    X509_free(cert);
+    X509_NAME_free(name);
+    EVP_PKEY_free(pkey);
+    return ret;
+}
+
 int setup_tests(void)
 {
+    ADD_TEST(test_sign_caches_encoding);
     ADD_TEST(test_standard_exts);
     ADD_ALL_TESTS(test_a2i_ipaddress, OSSL_NELEM(a2i_ipaddress_tests));
     ADD_ALL_TESTS(test_ipaddr_to_asc, OSSL_NELEM(ipaddr_to_asc_tests));