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)]),