Commit ebb1c1a509 for openssl.org

commit ebb1c1a509b1cd5e375f55f0ab183a8adb421869
Author: Mounir IDRASSI <mounir.idrassi@idrix.fr>
Date:   Sun May 31 23:16:03 2026 +0900

    Skip partial-chain trust anchors in revocation checks

    When a non-self-signed certificate is accepted as the trust anchor via
    X509_V_FLAG_PARTIAL_CHAIN, revocation checking should stop before that
    certificate.

    Use ctx->num_untrusted as the boundary for CRL_CHECK_ALL and
    OCSP_RESP_CHECK_ALL in partial-chain verification, and cover the
    PARTIAL_CHAIN CRL_CHECK_ALL case with a focused regression test.

    Reviewed-by: David von Oheimb <david.von.oheimb@siemens.com>
    Reviewed-by: Andrew Dinh <andrewd@openssl.org>
    MergeDate: Tue Aug 18 15:44:18 2026
    (Merged from https://github.com/openssl/openssl/pull/30945)

diff --git a/crypto/x509/x509_vfy.c b/crypto/x509/x509_vfy.c
index ff06e402dc..6186ef676a 100644
--- a/crypto/x509/x509_vfy.c
+++ b/crypto/x509/x509_vfy.c
@@ -58,6 +58,7 @@ static int check_name_constraints(X509_STORE_CTX *ctx);
 static int check_id(X509_STORE_CTX *ctx);
 static int check_trust(X509_STORE_CTX *ctx, int num_untrusted);
 static int check_revocation(X509_STORE_CTX *ctx);
+static int revocation_check_end(X509_STORE_CTX *ctx, int check_all);
 #ifndef OPENSSL_NO_OCSP
 static int check_cert_ocsp_resp(X509_STORE_CTX *ctx);
 #endif
@@ -1173,6 +1174,20 @@ trusted:
     return X509_TRUST_UNTRUSTED;
 }

+/*
+ * Return the last chain depth whose revocation status should be checked.
+ * With X509_V_FLAG_*_CHECK_ALL and X509_V_FLAG_PARTIAL_CHAIN, revocation
+ * checking stops before the first trusted certificate in the chain.
+ */
+static int revocation_check_end(X509_STORE_CTX *ctx, int check_all)
+{
+    if (!check_all)
+        return 0;
+    return (ctx->param->flags & X509_V_FLAG_PARTIAL_CHAIN) == 0
+        ? sk_X509_num(ctx->chain) - 1
+        : ctx->num_untrusted - 1;
+}
+
 /* Sadly, returns 0 also on internal error. */
 static int check_revocation(X509_STORE_CTX *ctx)
 {
@@ -1190,10 +1205,10 @@ static int check_revocation(X509_STORE_CTX *ctx)
         /*
          * certificate status checking with OCSP
          */
-        if (ocsp_check_all_enabled)
-            last = sk_X509_num(ctx->chain) - 1;
-        else if (!crl_check_all_enabled && ctx->parent != NULL)
+        if (!ocsp_check_all_enabled && !crl_check_all_enabled
+            && ctx->parent != NULL)
             return 1; /* If checking CRL paths this isn't the EE certificate */
+        last = revocation_check_end(ctx, ocsp_check_all_enabled);

         for (i = 0; i <= last; i++) {
             ctx->error_depth = i;
@@ -1256,14 +1271,9 @@ static int check_revocation(X509_STORE_CTX *ctx)

     if (crl_check_enabled && !ocsp_check_all_enabled) {
         /* certificate status check with CRLs */
-        if (crl_check_all_enabled) {
-            last = sk_X509_num(ctx->chain) - 1;
-        } else {
-            /* If checking CRL paths this isn't the EE certificate */
-            if (ctx->parent != NULL)
-                return 1;
-            last = 0;
-        }
+        if (!crl_check_all_enabled && ctx->parent != NULL)
+            return 1; /* If checking CRL paths this isn't the EE certificate */
+        last = revocation_check_end(ctx, crl_check_all_enabled);

         /*
          * in the case that OCSP is only enabled for the server certificate
diff --git a/test/recipes/25-test_verify.t b/test/recipes/25-test_verify.t
index 6efe24087a..53cc4f9795 100644
--- a/test/recipes/25-test_verify.t
+++ b/test/recipes/25-test_verify.t
@@ -30,7 +30,47 @@ sub verify {
     run(app([@args]));
 }

-plan tests => 221;
+sub make_empty_crl {
+    my ($prefix, $ca_cert, $ca_key, $crl) = @_;
+    my $index = "$prefix-index.txt";
+    my $serial = "$prefix-serial.txt";
+    my $cnf = "$prefix.cnf";
+    my $ca_cert_file = "$prefix-ca-cert.pem";
+    my $ca_key_file = "$prefix-ca-key.pem";
+
+    open my $index_fh, ">", $index or return 0;
+    close $index_fh;
+    open my $serial_fh, ">", $serial or return 0;
+    print $serial_fh "01\n";
+    close $serial_fh;
+    copy($ca_cert, $ca_cert_file) or return 0;
+    copy($ca_key, $ca_key_file) or return 0;
+    open my $cnf_fh, ">", $cnf or return 0;
+    print $cnf_fh <<"EOF";
+[ ca ]
+default_ca = test_ca
+
+[ test_ca ]
+database = $index
+serial = $serial
+new_certs_dir = .
+certificate = $ca_cert_file
+private_key = $ca_key_file
+default_md = sha256
+default_days = 365
+default_crl_days = 365
+policy = policy_any
+
+[ policy_any ]
+commonName = optional
+EOF
+    close $cnf_fh;
+
+    run(app(["openssl", "ca", "-batch", "-config", $cnf, "-gencrl",
+             "-out", $crl]));
+}
+
+plan tests => 222;

 # Canonical success
 ok(verify("ee-cert", "sslserver", ["root-cert"], ["ca-cert"]),
@@ -146,6 +186,13 @@ ok(!verify("ee-cert", "sslserver", [], [qw(ca-cert)], "-partial_chain"),
    "fail untrusted partial chain");
 ok(verify("ee-cert", "sslserver", [qw(ca-cert)], [], "-partial_chain"),
    "accept trusted partial chain");
+ok(make_empty_crl("partial-chain-ca", srctop_file(@certspath, "ca-cert.pem"),
+                  srctop_file(@certspath, "ca-key.pem"),
+                  "partial-chain-ca.crl")
+   && verify("ee-cert", "sslserver", [qw(ca-cert)], [],
+             "-partial_chain", "-crl_check_all", "-CRLfile",
+             "partial-chain-ca.crl"),
+   "accept trusted partial chain with CRL_CHECK_ALL");
 ok(!verify("ee-cert", "sslserver", [qw(ca-expired)], [], "-partial_chain"),
    "reject expired trusted partial chain"); # this check is beyond RFC 5280
 ok(!verify("ee-cert", "sslserver", [qw(root-expired)], [qw(ca-cert)]),