Commit 0e94c5b18b for openssl.org

commit 0e94c5b18b4e0118f8feb6a41b11572c0d4d02fa
Author: Mounir IDRASSI <mounir.idrassi@idrix.fr>
Date:   Thu Sep 10 22:56:31 2026 +0900

    Fix EXFLAG_SS set on self-issued rollover CA certificates

    ossl_x509v3_cache_extensions() computes the SKID and AKID into
    temporaries and publishes them under the write lock since
    792a760ac2, but the EXFLAG_SS decision still calls
    X509_check_akid() on the certificate, whose cached SKID is NULL on
    first build. The keyid comparison inside X509_check_akid() is then
    skipped, so a self-issued CA rollover certificate (SKID of the new
    key, AKID keyid of the old key) is wrongly cached as self-signed.

    That misclassification rejects valid rollover chains (error 19 at
    depth 1 when the transition certificate is untrusted) and promotes
    a stored transition certificate to a trust anchor without
    PARTIAL_CHAIN, silently skipping checks provisioned on the real
    anchor.

    Compare the freshly decoded tmp_akid against tmp_skid directly so
    the decision doesn't depend on cache publication timing. The
    serial number and issuer name checks of X509_check_akid() and the
    missing-identifier behavior are retained.

    Add regression tests with a rollover chain: accept through the
    untrusted transition certificate, reject when only the transition
    certificate is trusted, and accept the transition certificate as
    an explicit PARTIAL_CHAIN anchor. The new certificates are
    generated by test/certs/setup.sh from the existing test keys.

    Fixes #32793
    Assisted-by: pi:kimi-k3

    Reviewed-by: Eugene Syromiatnikov <esyr@openssl.org>
    Reviewed-by: Bob Beck <beck@openssl.org>
    Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
    Merge-date: Mon Sep 21 09:37:08 2026
    Merged-from: https://github.com/openssl/openssl/pull/32794

diff --git a/crypto/x509/v3_purp.c b/crypto/x509/v3_purp.c
index 3ee4369b2a..f13d785a38 100644
--- a/crypto/x509/v3_purp.c
+++ b/crypto/x509/v3_purp.c
@@ -709,8 +709,15 @@ int ossl_x509v3_cache_extensions(const X509 *const_x)
          * we could afford doing the (accurate) actual self-signature check, but
          * decided against it for efficiency reasons and according to RFC 5280,
          * CA certs MUST have an SKID and non-root certs MUST have an AKID.
+         *
+         * The cached const_x->skid is not populated until the write-lock
+         * publication below, so the keyid comparison is done directly
+         * against tmp_skid.  X509_check_akid() is retained for its serial
+         * number and issuer name checks.
          */
-        if (X509_check_akid(const_x, tmp_akid) == X509_V_OK
+        if ((tmp_akid == NULL || tmp_akid->keyid == NULL || tmp_skid == NULL
+                || ASN1_OCTET_STRING_cmp(tmp_akid->keyid, tmp_skid) == 0)
+            && X509_check_akid(const_x, tmp_akid) == X509_V_OK
             && check_sig_alg_match(X509_get0_pubkey(const_x), const_x) == X509_V_OK) {
             /*
              * Assume self-signed if the signature alg matches the pkey alg and
diff --git a/test/certs/rollover-ca.pem b/test/certs/rollover-ca.pem
new file mode 100644
index 0000000000..01c357a3e4
--- /dev/null
+++ b/test/certs/rollover-ca.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDFDCCAfygAwIBAgICA+kwDQYJKoZIhvcNAQELBQAwGzEZMBcGA1UEAwwQVGVz
+dCBSb2xsb3ZlciBDQTAgFw0yMDAxMDEwMDAwMDBaGA8yMTI2MDkxMjAxMDAzN1ow
+GzEZMBcGA1UEAwwQVGVzdCBSb2xsb3ZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBAJadpD0ASxxfxsvdj9IxsogVzMSGLFziaYuE9KejU9+R479R
+ifvwfBANO62sNWJ19X//9G5UjwWmkiOzn1k50DkYsBBA3mJzik6wjt/c58lBIlSE
+gAgpvDU8ht8w3t20JP9+YqXAeugqFj/Wl9rFQtsvaWSRywjXVlp5fxuEQelNnXcJ
+EKhsKTNExsBUZebo4/J1BWpklWzA9P0lYW5INvDAAwcF1nzlEf0Y6Eot03IMNyg2
+MTE4hehxjdgCSci8GYnFirE/ojXqqpAcZGh7r2dqWgZUD1Dh+bT2vjrUzj8eTH3G
+dzI+oljt29102JIUaqj3yzRYkah8FLF9CLNNsUcCAwEAAaNgMF4wDwYDVR0TAQH/
+BAUwAwEB/zALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFLQRM/HX4l73U54gIhBPhga/
+H8leMB8GA1UdIwQYMBaAFI71Ja8em2uEPXyAmslTnE1y96NSMA0GCSqGSIb3DQEB
+CwUAA4IBAQAh0VU8trrRoT8l3EC+EqcVsrtjxCdL/+0f3sfue0tc/WICCEFH2AFi
+HxSlMd4xdJ0TMBLwLbRI8Jpnt1ORFw4Vf7hkWEdewFTXfqM79zMGQZd3YEko1ron
+CWNWcQ7314xl+C+ND/HCQXMFuCINOHh7Tufwc4NTphiuFli2tQCoeTfjQTRDKWjL
+OyqFx7OTG7Doh/8yHv5ZuSQnV2OUlJP4AJ0jJRZZvRC7EqV2CEyHXc+zASxOheeM
+GejDtJ0fDNTU1lN7wWbrICy4/vLt54ihNvMFGO5vLre8lss8T8zrIgVCM0wJDrkI
+mULWdN8itYwLYYRXZsxVNHn90yToOFOK
+-----END CERTIFICATE-----
diff --git a/test/certs/rollover-ee.pem b/test/certs/rollover-ee.pem
new file mode 100644
index 0000000000..a09af78d40
--- /dev/null
+++ b/test/certs/rollover-ee.pem
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDMDCCAhigAwIBAgICA+owDQYJKoZIhvcNAQELBQAwGzEZMBcGA1UEAwwQVGVz
+dCBSb2xsb3ZlciBDQTAgFw0yMDAxMDEwMDAwMDBaGA8yMTI2MDkxMjAxMDAzN1ow
+GzEZMBcGA1UEAwwQVGVzdCBSb2xsb3ZlciBFRTCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBAKj/iVhhha7e2ywP1XP74reoG3p1YCvUfTxzdrWu3pMvfySQ
+bckc9Io4zZ+igBZWy7Qsu5PlFx//DcZD/jE0+CjYdemju4iC76Ny4lNiBUVN4DGX
+76qdENJYDZ4GnjK7GwhWXWUPP2aOwjagEf/AWTX9SRzdHEIzBniuBDgj5ed1Z9OU
+rVqpQB+sWRD1DMFkrUrExjVTs5ZqghsVi9GZq+Seb5Sq0pblV/uMkWSKPCQWxtIZ
+voJgEztisO0+HbPK+WvfMbl6nktHaKcpxz9K4iIntO+QY9fv0HJJPlutuRvUK2+G
+aN3VcxK4Q8ncQQ+io0ZPi2eIhA9h/nk0H0qJH7cCAwEAAaN8MHowDAYDVR0TAQH/
+BAIwADALBgNVHQ8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMC
+MB0GA1UdDgQWBBTnm+IqrYpsOst2UeWOB5gil+FzojAfBgNVHSMEGDAWgBS0ETPx
+1+Je91OeICIQT4YGvx/JXjANBgkqhkiG9w0BAQsFAAOCAQEAPijP7kjgPCA0BxhU
+mqOOxRS36O4r0Yb+h2t1acvz0NnzlrkjrbVjK2fZcEDHT+brtA177T73iFKRMwrD
+MIJzYDCBPeLhVDrydtOEzB9AFDrBGPXVzfTDMNI/uY9216RGe7mb1lojUe+OhNCj
+LdIb/2nDnyemRGLb7+eBLabv31idrKtiCzzCxNkORBvBa1ocl0dkLCv2jksTWdVH
+S6rh6sjFumTygsqHQbA1ErDBtRkBb1VDk79EhcS02V2Md3vW/XLHMJhlCr94/3rb
+0vPzYXqT4BAfmyh5huBSOHmo2dxVKJ9Reay5itEiwzh+IUfXvNgaowZ5viEBH+zM
+YYF1uA==
+-----END CERTIFICATE-----
diff --git a/test/certs/rollover-root.pem b/test/certs/rollover-root.pem
new file mode 100644
index 0000000000..bdd70c9679
--- /dev/null
+++ b/test/certs/rollover-root.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDFDCCAfygAwIBAgICA+gwDQYJKoZIhvcNAQELBQAwGzEZMBcGA1UEAwwQVGVz
+dCBSb2xsb3ZlciBDQTAgFw0yMDAxMDEwMDAwMDBaGA8yMTI2MDkxMjAxMDAzN1ow
+GzEZMBcGA1UEAwwQVGVzdCBSb2xsb3ZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBAOHmAPUGvKBGOHkPPx5xGRNtAt8rm3Zr/KywIe3WkQhCO6Vj
+NexSW6CiSsXWAJQDl1o9uWco0n3jIVyk7cY8jY6E0Z1Uwz3ZdKKWdmdx+cYaUHez
+/XjuW+DjjIkjwpoi7D7UN54HzcArVREXOjRCHGkNOhiw7RWUXsb9nofGHOeUGpLA
+XwXBc0PlA94JkckkztiOi34u4DFI0YYqalUmeugLNk6XseCkydpcaUsDgAhWg6Mf
+siq4wUz+xbFN1MABqu2+ziW97mmt9gfNbiuhiVT1aOuYCe3JYGbLM2JKA7Bo1g6r
+X8E1VX79Ru6669y2oqPthX9337VoIkN+ZiQjr8UCAwEAAaNgMF4wDwYDVR0TAQH/
+BAUwAwEB/zALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFI71Ja8em2uEPXyAmslTnE1y
+96NSMB8GA1UdIwQYMBaAFI71Ja8em2uEPXyAmslTnE1y96NSMA0GCSqGSIb3DQEB
+CwUAA4IBAQAjkLgEO/nYCmDHMv6R76lNFeERjwk/c/JsCOUpDg+5G+GRQtNR42p7
+BTreJerDEqxEDadEIgyLq9gyw8+iNeII1UZymIYAZmAtVcjP1ecBoW7lCI8PwApu
+cf5NweIDEcAkm7X0g3p9iToy/4z/0KYqV8sRkhcqbJQci/TlO887pSAebTEAogH4
+e2GuYGDOEnmuEP4+L+FSk0rSOmOPrrkYXcUe8bbRM0ypxdxkJitUS5sOR+Kshu2k
+37Q0uKxwwW8HKa5ZNiPaTh9Iq3Si/UYT40LG58ZfORVSV2oeeCd2B2uzddwiSg/9
+/z1HxedkOo/0MqvDP7vjcKL5d/ms+lQL
+-----END CERTIFICATE-----
diff --git a/test/certs/setup.sh b/test/certs/setup.sh
index e30e6012ab..e3dcc5b43c 100755
--- a/test/certs/setup.sh
+++ b/test/certs/setup.sh
@@ -86,6 +86,39 @@ openssl x509 -in sroot-cert.pem -trustout \
 ./mkcert.sh genca "CA2" ca-key ca-name2 root-key root-cert
 ./mkcert.sh genca "CA" ca-key ca-root2 root-key2 root-cert2
 DAYS=-1 ./mkcert.sh genca "CA" ca-key ca-expired root-key root-cert
+# CA key rollover chain: a self-issued transition certificate carries the
+# new CA key (ca-key) but is signed by the old key (root-key), so its SKID
+# differs from its AKID keyIdentifier and it must not be classified as
+# self-signed.  The old root reuses root-key, the leaf ee-key.  Explicit
+# serials 1000-1002 keep the same-named issuers of these certificates
+# distinct for X509_STORE lookups.
+./mkcert.sh req root-key "CN = Test Rollover CA" |
+    openssl x509 -req -sha256 -signkey root-key.pem -set_serial 1000 \
+        -not_before 20200101000000Z -days 36525 -out rollover-root.pem \
+        -extfile <(printf "%s\n" \
+            "basicConstraints = critical,CA:true" \
+            "keyUsage = keyCertSign,cRLSign" \
+            "subjectKeyIdentifier = hash" \
+            "authorityKeyIdentifier = keyid")
+./mkcert.sh req ca-key "CN = Test Rollover CA" |
+    openssl x509 -req -sha256 -CA rollover-root.pem -CAkey root-key.pem \
+        -set_serial 1001 -not_before 20200101000000Z -days 36525 \
+        -out rollover-ca.pem \
+        -extfile <(printf "%s\n" \
+            "basicConstraints = critical,CA:true" \
+            "keyUsage = keyCertSign,cRLSign" \
+            "subjectKeyIdentifier = hash" \
+            "authorityKeyIdentifier = keyid")
+./mkcert.sh req ee-key "CN = Test Rollover EE" |
+    openssl x509 -req -sha256 -CA rollover-ca.pem -CAkey ca-key.pem \
+        -set_serial 1002 -not_before 20200101000000Z -days 36525 \
+        -out rollover-ee.pem \
+        -extfile <(printf "%s\n" \
+            "basicConstraints = critical,CA:false" \
+            "keyUsage = digitalSignature,keyEncipherment" \
+            "extendedKeyUsage = serverAuth,clientAuth" \
+            "subjectKeyIdentifier = hash" \
+            "authorityKeyIdentifier = keyid")
 # trust variants: +serverAuth, -serverAuth, +clientAuth, -clientAuth
 openssl x509 -in ca-cert.pem -trustout \
     -addtrust serverAuth -out ca+serverAuth.pem
diff --git a/test/recipes/25-test_verify.t b/test/recipes/25-test_verify.t
index 0893f7b724..99757b99be 100644
--- a/test/recipes/25-test_verify.t
+++ b/test/recipes/25-test_verify.t
@@ -80,7 +80,7 @@ EOF
              "-out", $crl]));
 }

-plan tests => 227;
+plan tests => 230;

 # Canonical success
 ok(verify("ee-cert", "sslserver", ["root-cert"], ["ca-cert"]),
@@ -200,6 +200,19 @@ ok(!verify("ee-cert", "sslserver", [qw(root-cert)], [qw(ca-name2)]),
    "fail wrong intermediate CA DN");
 ok(!verify("ee-cert", "sslserver", [qw(root-cert)], [qw(ca-root2)]),
    "fail wrong intermediate CA issuer");
+
+# CA key rollover: the self-issued transition certificate is not
+# self-signed (regression from 792a760ac2).  It must neither break
+# chain building to the old root nor act as an anchor on its own,
+# while explicit PARTIAL_CHAIN trust in it must keep working.
+ok(verify("rollover-ee", "", ["rollover-root"], ["rollover-ca"],
+          "-attime", "1800000000"),
+   "accept rollover chain through untrusted transition cert");
+ok(!verify("rollover-ee", "", ["rollover-ca"], [], "-attime", "1800000000"),
+   "reject rollover chain when only transition cert is trusted");
+ok(verify("rollover-ee", "", ["rollover-ca"], [],
+          "-partial_chain", "-attime", "1800000000"),
+   "accept rollover transition with explicit partial-chain trust");
 ok(!verify("ee-cert", "sslserver", [], [qw(ca-cert)], "-partial_chain"),
    "fail untrusted partial chain");
 ok(verify("ee-cert", "sslserver", [qw(ca-cert)], [], "-partial_chain"),