Commit 97fd91933d for openssl.org
commit 97fd91933df73cfdc73b53832a41b8b9d0d9cefd
Author: Paul Grubbs <paulgrub@umich.edu>
Date: Wed Aug 26 14:24:53 2026 -0400
Accept CRLs whose IDP names the issuer of a certificate without CDP
When a certificate has no CRL distribution points extension (or none of
its distribution points matches the CRL), crl_crldp_check() accepted a
CRL issued by the certificate issuer only if the CRL had no issuing
distribution point extension or the IDP carried no distribution point
name. A CRL whose IDP names the certificate issuer was rejected with
X509_V_ERR_DIFFERENT_CRL_SCOPE.
RFC 5280, section 6.3.3, last paragraph, requires the opposite:
If the revocation status has not been determined, repeat the process
above with any available CRLs not specified in a distribution point
but issued by the certificate issuer. For the processing of such a
CRL, assume a DP with both the reasons and the cRLIssuer fields
omitted and a distribution point name of the certificate issuer.
That is, the sequence of names in fullName is generated from the
certificate issuer field as well as the certificate issuerAltName
extension.
Step (b)(2)(i) of the same section then requires that "one of the names
in the IDP matches one of the names in the DP", i.e. the IDP is in scope
if one of its names is the certificate issuer name or one of the names
in the certificate's issuerAltName extension.
Implement that check in a new idp_check_issuer() and use it in the
fallback at the end of crl_crldp_check(). An IDP that only carries a
URI, or a nameRelativeToCRLIssuer, does not match the certificate issuer
and is still rejected as out of scope, as before.
Extend make_empty_crl() in test/recipes/25-test_verify.t to accept CRL
extensions and add two cases: a CRL whose IDP fullName is the CA's
directoryName is accepted for a certificate without CDP, and one whose
IDP fullName is only a URI is still rejected with "different CRL scope".
Fixes #23325
Assisted-by: Claude:claude-fable-5
Reviewed-by: Jakub Zelenka <jakub.zelenka@openssl.foundation>
Reviewed-by: David von Oheimb <david.von.oheimb@siemens.com>
Merge-date: Mon Sep 7 18:38:10 2026
Merged-from: https://github.com/openssl/openssl/pull/32553
diff --git a/CHANGES.md b/CHANGES.md
index be18343f0b..0fa4bc2aa0 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -479,6 +479,15 @@ OpenSSL 4.1
*Dimitri John Ledkov*
+ * Fixed CRL scope checking for certificates without a CRL distribution
+ points extension. A CRL having an issuing distribution point extension
+ including a name that matches the certificate issuer name or any
+ issuerAltName of the certificate is now accepted, as required
+ by the default distribution point rule at the end of RFC 5280 section 6.3.3,
+ instead of being rejected with X509_V_ERR_DIFFERENT_CRL_SCOPE.
+
+ *Paul Grubbs*
+
OpenSSL 4.0
-----------
diff --git a/crypto/x509/x509_vfy.c b/crypto/x509/x509_vfy.c
index 1163698c73..973b586dac 100644
--- a/crypto/x509/x509_vfy.c
+++ b/crypto/x509/x509_vfy.c
@@ -1939,6 +1939,52 @@ static int crldp_check_crlissuer(DIST_POINT *dp, X509_CRL *crl, int crl_score)
return 0;
}
+/*
+ * Check whether the distribution point name |idpname| of a CRL's IDP extension
+ * matches the default distribution point that RFC 5280, section 6.3.3, assumes
+ * for CRLs not specified in any of the certificate's distribution points: one
+ * whose fullName consists of the certificate issuer name and the names in the
+ * certificate's issuerAltName extension. The assumed distribution point is
+ * constructed and matched with idp_check_dp().
+ */
+static int idp_check_issuer(DIST_POINT_NAME *idpname, X509 *x)
+{
+ DIST_POINT_NAME dpname;
+ GENERAL_NAMES *gens;
+ GENERAL_NAME *gen = NULL;
+ X509_NAME *iname = NULL;
+ int ret = 0;
+
+ /*
+ * An undecodable issuerAltName extension is treated as an absent one;
+ * any decoding errors are not left in the error queue.
+ */
+ ERR_set_mark();
+ gens = X509_get_ext_d2i(x, NID_issuer_alt_name, NULL, NULL);
+ ERR_pop_to_mark();
+ if (gens == NULL && (gens = sk_GENERAL_NAME_new_null()) == NULL)
+ return 0;
+ if ((gen = GENERAL_NAME_new()) == NULL
+ || (iname = X509_NAME_dup(X509_get_issuer_name(x))) == NULL)
+ goto end;
+ GENERAL_NAME_set0_value(gen, GEN_DIRNAME, iname);
+ iname = NULL; /* now owned by |gen| */
+ if (!sk_GENERAL_NAME_push(gens, gen))
+ goto end;
+ gen = NULL; /* now owned by |gens| */
+
+ dpname.type = 0; /* fullName */
+ dpname.name.fullname = gens;
+ dpname.dpname = NULL;
+ ret = idp_check_dp(&dpname, idpname);
+
+end:
+ GENERAL_NAME_free(gen);
+ X509_NAME_free(iname);
+ GENERAL_NAMES_free(gens);
+ return ret;
+}
+
/* Check CRLDP and IDP */
static int crl_crldp_check(X509 *x, X509_CRL *crl, int crl_score,
unsigned int *preasons)
@@ -1966,8 +2012,16 @@ static int crl_crldp_check(X509 *x, X509_CRL *crl, int crl_score,
}
}
}
- return (crl->idp == NULL || crl->idp->distpoint == NULL)
- && (crl_score & CRL_SCORE_ISSUER_NAME) != 0;
+ /*
+ * The CRL is not specified in any distribution point of the certificate.
+ * RFC 5280, section 6.3.3, allows such a CRL if it is issued by the
+ * certificate issuer, assuming a distribution point with the reasons and
+ * cRLIssuer fields omitted and a distribution point name consisting of
+ * the certificate issuer name and any issuerAltName entries.
+ */
+ return (crl_score & CRL_SCORE_ISSUER_NAME) != 0
+ && (crl->idp == NULL || crl->idp->distpoint == NULL
+ || idp_check_issuer(crl->idp->distpoint, x));
}
/*
diff --git a/test/recipes/25-test_verify.t b/test/recipes/25-test_verify.t
index 040b36383e..97053561b6 100644
--- a/test/recipes/25-test_verify.t
+++ b/test/recipes/25-test_verify.t
@@ -19,19 +19,26 @@ use OpenSSL::Test::Utils;
setup("test_verify");
my @certspath = qw(test certs);
+my $ca_cert = srctop_file(@certspath, "ca-cert.pem");
+my $ca_key = srctop_file(@certspath, "ca-key.pem");
sub verify {
my ($cert, $purpose, $trusted, $untrusted, @opts) = @_;
+ # An option hash at the end of @opts is passed on to app(),
+ # e.g., to tap stderr via { stderr => $file }.
+ my %app_opts = @opts > 0 && ref($opts[-1]) eq 'HASH' ? %{pop @opts} : ();
my @args = qw(openssl verify -auth_level 1);
push(@args, "-purpose", $purpose) if $purpose ne "";
push(@args, @opts);
for (@$trusted) { push(@args, "-trusted", srctop_file(@certspath, "$_.pem")) }
for (@$untrusted) { push(@args, "-untrusted", srctop_file(@certspath, "$_.pem")) }
- push(@args, srctop_file(@certspath, "$cert.pem"));
- run(app([@args]));
+ # A certificate generated by the test recipe itself is used in place
+ # of one from @certspath.
+ push(@args, -f "$cert.pem" ? "$cert.pem" : srctop_file(@certspath, "$cert.pem"));
+ run(app([@args], %app_opts));
}
sub make_empty_crl {
- my ($prefix, $ca_cert, $ca_key, $crl) = @_;
+ my ($prefix, $ca_cert, $ca_key, $crl, $crlexts) = @_;
my $index = "$prefix-index.txt";
my $serial = "$prefix-serial.txt";
my $cnf = "$prefix.cnf";
@@ -64,13 +71,16 @@ policy = policy_any
[ policy_any ]
commonName = optional
EOF
+ # Optional CRL extension sections; the section is named "crl_ext".
+ print $cnf_fh $crlexts if defined $crlexts;
close $cnf_fh;
run(app(["openssl", "ca", "-batch", "-config", $cnf, "-gencrl",
+ (defined $crlexts ? ("-crlexts", "crl_ext") : ()),
"-out", $crl]));
}
-plan tests => 222;
+plan tests => 225;
# Canonical success
ok(verify("ee-cert", "sslserver", ["root-cert"], ["ca-cert"]),
@@ -186,8 +196,7 @@ 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"),
+ok(make_empty_crl("partial-chain-ca", $ca_cert, $ca_key,
"partial-chain-ca.crl")
&& verify("ee-cert", "sslserver", [qw(ca-cert)], [],
"-partial_chain", "-crl_check_all", "-CRLfile",
@@ -686,6 +695,66 @@ run(app(["openssl", "verify",
ok(grep(/CRL is not yet valid/, do { open my $fh, '<', $cve_28388_stderr; <$fh> }),
"CVE-2026-28388");
+# A certificate without a CRL distribution points extension is checked against
+# CRLs issued by its issuer as if it had a distribution point whose fullName
+# consists of the certificate issuer name and any issuerAltName entries of the
+# certificate (RFC 5280, section 6.3.3, last paragraph). A CRL whose issuing
+# distribution point matches one of those names is therefore in scope, while
+# one that only names a URI not present in an issuerAltName is not.
+my $idp_dirname_exts = <<"EOF";
+[ crl_ext ]
+issuingDistributionPoint = critical, \@idp_section
+
+[ idp_section ]
+fullname = dirName:idp_dn
+
+[ idp_dn ]
+CN = CA
+EOF
+ok(make_empty_crl("idp-dirname", $ca_cert, $ca_key,
+ "idp-dirname.crl", $idp_dirname_exts)
+ && verify("ee-cert", "", [qw(root-cert)], [qw(ca-cert)],
+ "-crl_check", "-CRLfile", "idp-dirname.crl"),
+ "accept CRL whose IDP names the certificate issuer for a certificate without CDP");
+
+my $idp_uri = "http://example.com/ca.crl";
+my $idp_uri_exts = <<"EOF";
+[ crl_ext ]
+issuingDistributionPoint = critical, \@idp_section
+
+[ idp_section ]
+fullname = URI:$idp_uri
+EOF
+my $idp_uri_stderr = "idp-uri.err";
+ok(make_empty_crl("idp-uri", $ca_cert, $ca_key,
+ "idp-uri.crl", $idp_uri_exts)
+ && !verify("ee-cert", "", [qw(root-cert)], [qw(ca-cert)],
+ "-crl_check", "-CRLfile", "idp-uri.crl",
+ { stderr => $idp_uri_stderr })
+ && grep(/different CRL scope/,
+ do { open my $fh, '<', $idp_uri_stderr; <$fh> }),
+ "reject CRL whose IDP names only a URI for a certificate without CDP");
+
+# The URI-only CRL is in scope for a certificate whose issuerAltName extension
+# contains that URI.
+my $ian_cnf = "ian-cert.cnf";
+open(my $ian_fh, '>', $ian_cnf) or die "cannot write $ian_cnf: $!";
+print $ian_fh <<"EOF";
+[ ext ]
+issuerAltName = URI:$idp_uri
+EOF
+close($ian_fh);
+ok(run(app(["openssl", "req", "-new",
+ "-key", srctop_file(@certspath, "ee-key.pem"),
+ "-subj", "/CN=issuerAltName test", "-out", "ian-cert.csr"]))
+ && run(app(["openssl", "x509", "-req", "-in", "ian-cert.csr",
+ "-CA", $ca_cert, "-CAkey", $ca_key, "-set_serial", "99",
+ "-days", "3650", "-extfile", $ian_cnf, "-extensions", "ext",
+ "-out", "ian-cert.pem"]))
+ && verify("ian-cert", "", [qw(root-cert)], [qw(ca-cert)],
+ "-crl_check", "-CRLfile", "idp-uri.crl"),
+ "accept CRL whose IDP matches an issuerAltName of a certificate without CDP");
+
# Delta CRLs must not be accepted as complete CRLs
my $delta_crl_as_complete_stderr = "delta-crl-as-complete.err";
ok(!run(app(["openssl", "verify", "-auth_level", "1",