Commit c2bb1f3d2a for openssl.org
commit c2bb1f3d2a04b978a8d040a6fa0a2a724b1d32df
Author: Bob Beck <beck@openssl.org>
Date: Mon Aug 24 10:04:54 2026 -0600
Limit certificate path length to a reasonable value.
In theory, this can be the limit of an ASN1 integer, in practice
that's utterly silly, as setting no limit already means it is
unlimited, and large chains are already ridiculously expensive.
While a practical limit likely never to be seen might be 64, There
seems to be precedent in other places (Chromium and others) to limit
this to the range of 0-255. Let's do that. Nothing non-synthetic will
ever hit this.
Reviewed-by: Neil Horman <nhorman@openssl.org>
Reviewed-by: Mounir Idrassi <mounir.idrassi@idrix.fr>
Merge-date: Mon Oct 5 10:17:26 2026
Merged-from: https://github.com/openssl/openssl/pull/32471
diff --git a/crypto/err/openssl.txt b/crypto/err/openssl.txt
index adf38e601a..1ec2ebe3bf 100644
--- a/crypto/err/openssl.txt
+++ b/crypto/err/openssl.txt
@@ -1827,6 +1827,7 @@ X509V3_R_NO_PUBLIC_KEY:114:no public key
X509V3_R_NO_SUBJECT_DETAILS:125:no subject details
X509V3_R_OPERATION_NOT_DEFINED:148:operation not defined
X509V3_R_OTHERNAME_ERROR:147:othername error
+X509V3_R_PATHLEN_TOO_LARGE:175:pathlen too large
X509V3_R_POLICY_LANGUAGE_ALREADY_DEFINED:155:policy language already defined
X509V3_R_POLICY_PATH_LENGTH:156:policy path length
X509V3_R_POLICY_PATH_LENGTH_ALREADY_DEFINED:157:\
diff --git a/crypto/x509/v3_purp.c b/crypto/x509/v3_purp.c
index a312d0519d..ac8899c580 100644
--- a/crypto/x509/v3_purp.c
+++ b/crypto/x509/v3_purp.c
@@ -585,6 +585,11 @@ int ossl_x509v3_cache_extensions(const X509 *const_x)
tmp_ex_flags |= EXFLAG_INVALID;
} else {
tmp_ex_pathlen = ASN1_INTEGER_get(bs->pathlen);
+ if (tmp_ex_pathlen < 0 || tmp_ex_pathlen > 255) {
+ ERR_raise(ERR_LIB_X509V3, X509V3_R_PATHLEN_TOO_LARGE);
+ tmp_ex_flags |= EXFLAG_INVALID;
+ tmp_ex_pathlen = -1;
+ }
}
}
BASIC_CONSTRAINTS_free(bs);
diff --git a/doc/man3/X509_get_extension_flags.pod b/doc/man3/X509_get_extension_flags.pod
index 21bf952e75..aa37d23fd1 100644
--- a/doc/man3/X509_get_extension_flags.pod
+++ b/doc/man3/X509_get_extension_flags.pod
@@ -36,7 +36,12 @@ These functions retrieve information related to commonly used certificate extens
X509_get_pathlen() retrieves the path length extension from a certificate.
This extension is used to limit the length of a cert chain that may be
-issued from that CA.
+issued from that CA. A path length constraint, whether from the basic
+constraints extension or the B<pcPathLengthConstraint> of a proxy
+certificate, must be in the range 0 to 255 inclusive. A certificate
+carrying a value outside this range is treated as invalid, as if the
+extension could not be parsed, and the corresponding getter reports no
+constraint.
X509_get_extension_flags() retrieves general information about a certificate,
it will return one or more of the following flags ored together.
@@ -176,7 +181,8 @@ X509_get_ext_d2i().
=head1 RETURN VALUES
X509_get_pathlen() returns the path length value, or -1 if the extension
-is not present.
+is not present, is out of range, or the certificate's extensions could
+not be parsed.
X509_get_extension_flags(), X509_get_key_usage() and
X509_get_extended_key_usage() return sets of flags corresponding to the
diff --git a/test/v3ext.c b/test/v3ext.c
index aa55b36702..33a25f279a 100644
--- a/test/v3ext.c
+++ b/test/v3ext.c
@@ -113,6 +113,51 @@ end:
return ret;
}
+static const struct {
+ long pathlen; /* Value of the basic constraints path length */
+ int valid; /* Nonzero if a cert carrying it is accepted */
+} pathlen_range_values[] = {
+ { 0, 1 },
+ { 254, 1 },
+ { 255, 1 },
+ { 256, 0 },
+ { 65535, 0 },
+ { -1, 0 },
+ { -2, 0 },
+};
+
+/*
+ * Build a CA cert whose basic constraints extension carries the given
+ * path length constraint and check whether extension caching accepts it.
+ * Values outside 0..255 must be rejected and reported as no constraint.
+ */
+static int test_pathlen_range_value(int idx)
+{
+ long pathlen = pathlen_range_values[idx].pathlen;
+ int valid = pathlen_range_values[idx].valid;
+ X509 *cert = NULL;
+ BASIC_CONSTRAINTS *bs = NULL;
+ int ret = 0;
+
+ if (!TEST_ptr(cert = X509_new())
+ || !TEST_true(X509_set_version(cert, X509_VERSION_3))
+ || !TEST_ptr(bs = BASIC_CONSTRAINTS_new())
+ || !TEST_ptr(bs->pathlen = ASN1_INTEGER_new())
+ || !TEST_true(ASN1_INTEGER_set(bs->pathlen, pathlen)))
+ goto end;
+ bs->ca = 255;
+ if (!TEST_int_gt(X509_add1_ext_i2d(cert, NID_basic_constraints, bs, 1, 0), 0)
+ || !TEST_int_eq(X509_check_purpose(cert, -1, 0), valid ? 1 : -1)
+ || !TEST_long_eq(X509_get_pathlen(cert), valid ? pathlen : -1))
+ goto end;
+
+ ret = 1;
+end:
+ BASIC_CONSTRAINTS_free(bs);
+ X509_free(cert);
+ return ret;
+}
+
static int test_pathlen(void)
{
X509 *x = NULL;
@@ -1341,6 +1386,7 @@ int setup_tests(void)
return 0;
ADD_TEST(test_pathlen);
+ ADD_ALL_TESTS(test_pathlen_range_value, OSSL_NELEM(pathlen_range_values));
ADD_ALL_TESTS(test_duplicate_field, OSSL_NELEM(duplicate_field_configs));
ADD_MFAIL_TEST(test_asn1_multi_mfail);
#ifndef OPENSSL_NO_RFC3779
diff --git a/test/verify_extra_test.c b/test/verify_extra_test.c
index 37ac46356a..661d127df8 100644
--- a/test/verify_extra_test.c
+++ b/test/verify_extra_test.c
@@ -15,6 +15,7 @@
#include <openssl/x509v3.h>
#include <openssl/pem.h>
#include <openssl/err.h>
+#include "internal/nelem.h"
#include "testutil.h"
static const char *certs_dir;
@@ -766,6 +767,180 @@ static int test_purpose_any(void)
return do_test_purpose(X509_PURPOSE_ANY, 1);
}
+static const char *ca_cert_pathlen_254[] = {
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIIDADCCAeigAwIBAgIBAjANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdSb290\n"
+ "IENBMCAXDTI2MDkwMzE5MjgxM1oYDzIxMjYwOTA0MTkyODEzWjANMQswCQYDVQQD\n"
+ "DAJDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJadpD0ASxxfxsvd\n"
+ "j9IxsogVzMSGLFziaYuE9KejU9+R479RifvwfBANO62sNWJ19X//9G5UjwWmkiOz\n"
+ "n1k50DkYsBBA3mJzik6wjt/c58lBIlSEgAgpvDU8ht8w3t20JP9+YqXAeugqFj/W\n"
+ "l9rFQtsvaWSRywjXVlp5fxuEQelNnXcJEKhsKTNExsBUZebo4/J1BWpklWzA9P0l\n"
+ "YW5INvDAAwcF1nzlEf0Y6Eot03IMNyg2MTE4hehxjdgCSci8GYnFirE/ojXqqpAc\n"
+ "ZGh7r2dqWgZUD1Dh+bT2vjrUzj8eTH3GdzI+oljt29102JIUaqj3yzRYkah8FLF9\n"
+ "CLNNsUcCAwEAAaNkMGIwEwYDVR0TAQH/BAkwBwEB/wICAP4wCwYDVR0PBAQDAgEG\n"
+ "MB0GA1UdDgQWBBS0ETPx1+Je91OeICIQT4YGvx/JXjAfBgNVHSMEGDAWgBSO9SWv\n"
+ "HptrhD18gJrJU5xNcvejUjANBgkqhkiG9w0BAQsFAAOCAQEASsSh8GLBpXEHpobO\n"
+ "lCoH3PER3ndBl7LzgEWxc9vcTXewcdPMDX6k4JrdLyM7SvaVBGw87ox0RZEv0AgN\n"
+ "aKL4jU0SvenIP5cOzUbhblOd0DCEyf3BWc54cSIB1y9AuXOTGCtTxNd1CPscQncD\n"
+ "STSxZAChvdxgWMa2ICBWOgIP6kZMkWI/O424u27NMaMnzuNcPikWFRqAfNAJZTR6\n"
+ "0zyX3WKNBRSEz4bsVwy9uYk0rcg2q6NzpdGmYYLbhSWARRHwCD8tBnzpTWCZZO71\n"
+ "84LsfV61OvZdPE6JJyoBJ09ZQe3BpIT/q/hj+oE9l+Or4dVUkSg0mkjwEUjYGk3d\n"
+ "cRczBQ==\n"
+ "-----END CERTIFICATE-----\n",
+ NULL,
+};
+
+static const char *ca_cert_pathlen_255[] = {
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIIDADCCAeigAwIBAgIBAjANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdSb290\n"
+ "IENBMCAXDTI2MDkwMzE5MjAwMVoYDzIxMjYwOTA0MTkyMDAxWjANMQswCQYDVQQD\n"
+ "DAJDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJadpD0ASxxfxsvd\n"
+ "j9IxsogVzMSGLFziaYuE9KejU9+R479RifvwfBANO62sNWJ19X//9G5UjwWmkiOz\n"
+ "n1k50DkYsBBA3mJzik6wjt/c58lBIlSEgAgpvDU8ht8w3t20JP9+YqXAeugqFj/W\n"
+ "l9rFQtsvaWSRywjXVlp5fxuEQelNnXcJEKhsKTNExsBUZebo4/J1BWpklWzA9P0l\n"
+ "YW5INvDAAwcF1nzlEf0Y6Eot03IMNyg2MTE4hehxjdgCSci8GYnFirE/ojXqqpAc\n"
+ "ZGh7r2dqWgZUD1Dh+bT2vjrUzj8eTH3GdzI+oljt29102JIUaqj3yzRYkah8FLF9\n"
+ "CLNNsUcCAwEAAaNkMGIwEwYDVR0TAQH/BAkwBwEB/wICAP8wCwYDVR0PBAQDAgEG\n"
+ "MB0GA1UdDgQWBBS0ETPx1+Je91OeICIQT4YGvx/JXjAfBgNVHSMEGDAWgBSO9SWv\n"
+ "HptrhD18gJrJU5xNcvejUjANBgkqhkiG9w0BAQsFAAOCAQEAShszCEdORas+xwIR\n"
+ "C6cMnP8NgwUl17qyBgTcyNcPWJxFZ5mBBiOg3H3GMFjTp5qeTzxj62GylzCcv28R\n"
+ "32WBOWSFSyIKRW76TbIuGtNHAsU2MpO8QNK9vNdRNARvkHd99oBQ/achQ0q6UpjJ\n"
+ "7vBJSEkQLTeoFzGSHo2xSllXln2s3dzP9cuyP0gNn540Fu5PTgEDHIgRNBP1axip\n"
+ "MCoQBefqNoSMczzSRN75VLO4NcDE9F7vIRqRtp/ij9ktXjNzAMblnzzDqnQGCQrh\n"
+ "GqsQvTqO1jNO6DXgTvfb5pfORXytO6okCsVin/gcA2LTQQf072L+2w3AtYakx4gc\n"
+ "9wGndg==\n"
+ "-----END CERTIFICATE-----\n",
+ NULL,
+};
+
+static const char *ca_cert_pathlen_max[] = {
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIIDBjCCAe6gAwIBAgIBAjANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdSb290\n"
+ "IENBMCAXDTI2MDkwMzE5MjAwMloYDzIxMjYwOTA0MTkyMDAyWjANMQswCQYDVQQD\n"
+ "DAJDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJadpD0ASxxfxsvd\n"
+ "j9IxsogVzMSGLFziaYuE9KejU9+R479RifvwfBANO62sNWJ19X//9G5UjwWmkiOz\n"
+ "n1k50DkYsBBA3mJzik6wjt/c58lBIlSEgAgpvDU8ht8w3t20JP9+YqXAeugqFj/W\n"
+ "l9rFQtsvaWSRywjXVlp5fxuEQelNnXcJEKhsKTNExsBUZebo4/J1BWpklWzA9P0l\n"
+ "YW5INvDAAwcF1nzlEf0Y6Eot03IMNyg2MTE4hehxjdgCSci8GYnFirE/ojXqqpAc\n"
+ "ZGh7r2dqWgZUD1Dh+bT2vjrUzj8eTH3GdzI+oljt29102JIUaqj3yzRYkah8FLF9\n"
+ "CLNNsUcCAwEAAaNqMGgwGQYDVR0TAQH/BA8wDQEB/wIIf/////////8wCwYDVR0P\n"
+ "BAQDAgEGMB0GA1UdDgQWBBS0ETPx1+Je91OeICIQT4YGvx/JXjAfBgNVHSMEGDAW\n"
+ "gBSO9SWvHptrhD18gJrJU5xNcvejUjANBgkqhkiG9w0BAQsFAAOCAQEA04vW/HVJ\n"
+ "7fg54oODO5WGxGyzVu6DTh9vTcFbDWhhGeeMlPIyzGJcCmW7rRGNHqmBjiH9dben\n"
+ "LTG/96XO96kOcKXG7Yqb1dKx/TVKLtfDxhrBUE3KdUn5B7uKGcXlh1YfINgdqH/4\n"
+ "WNwvJRDa/J9nhbAe+/SQamHkuAEet1go/hbss1YYQxrNxDel9NaJP+KXHrmNKAf3\n"
+ "Aib7SwN1r3Fr7g02pcm0gA7ah0uU+vHhRFhfawKbidWpH8iOVkRtHN+aYctyf6OD\n"
+ "NMNbV1+8lyIaFqzCauYZ93OP+uhXd/x0htmpSVSvpc75sE92oECO02udD65Bwaix\n"
+ "qE35gsZQkofHoQ==\n"
+ "-----END CERTIFICATE-----\n",
+ NULL,
+};
+
+/* Verification time for the path length chains, within all cert lifetimes */
+#define PATHLEN_CHAIN_TIME 1790000000
+
+/*
+ * A certificate for a path length chain: either a file in the certs
+ * directory or an inline PEM, exactly one of which is set.
+ */
+typedef struct pathlen_cert_st {
+ const char *file; /* File name in certs_dir */
+ const char **pem; /* PEM lines for X509_from_strings() */
+} PATHLEN_CERT;
+
+/*
+ * Chains anchored at root-cert, exercising path length constraints at the
+ * limit of the accepted range and beyond it, through X509_verify_cert().
+ * The intermediates were all issued from ca-key, so ee-cert and ee-client
+ * chain to each of them.
+ */
+static const struct {
+ PATHLEN_CERT leaf; /* Certificate being verified */
+ PATHLEN_CERT untrusted[2]; /* Untrusted chain certs, terminated by {0} */
+ int allow_proxy; /* Nonzero to set X509_V_FLAG_ALLOW_PROXY_CERTS */
+ int expected_err; /* X509_STORE_CTX error after verification */
+} pathlen_chains[] = {
+ { { "ee-cert.pem", NULL }, { { NULL, ca_cert_pathlen_254 } }, 0,
+ X509_V_OK },
+ { { "ee-cert.pem", NULL }, { { NULL, ca_cert_pathlen_255 } }, 0,
+ X509_V_OK },
+ { { "ee-cert.pem", NULL }, { { NULL, ca_cert_pathlen_max } }, 0,
+ X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY },
+ { { "pc1-cert.pem", NULL },
+ { { "ee-client.pem", NULL }, { NULL, ca_cert_pathlen_max } }, 1,
+ X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY },
+};
+
+static X509 *load_pathlen_cert(const PATHLEN_CERT *pc)
+{
+ char *path;
+ X509 *cert;
+
+ if (pc->pem != NULL)
+ return X509_from_strings(pc->pem);
+ if ((path = test_mk_file_path(certs_dir, pc->file)) == NULL)
+ return NULL;
+ cert = load_cert_pem(path, NULL);
+ OPENSSL_free(path);
+ return cert;
+}
+
+static int test_pathlen_chain(int idx)
+{
+ static const PATHLEN_CERT root = { "root-cert.pem", NULL };
+ X509 *leaf = NULL;
+ X509 *cert = NULL;
+ STACK_OF(X509) *trusted = NULL;
+ STACK_OF(X509) *untrusted = NULL;
+ X509_STORE_CTX *ctx = NULL;
+ X509_VERIFY_PARAM *vpm;
+ size_t i;
+ int ret = 0;
+
+ if (!TEST_ptr(leaf = load_pathlen_cert(&pathlen_chains[idx].leaf))
+ || !TEST_ptr(trusted = sk_X509_new_null())
+ || !TEST_ptr(untrusted = sk_X509_new_null())
+ || !TEST_ptr(ctx = X509_STORE_CTX_new())
+ || !TEST_ptr(cert = load_pathlen_cert(&root))
+ || !TEST_true(sk_X509_push(trusted, cert)))
+ goto err;
+ cert = NULL;
+ for (i = 0; i < OSSL_NELEM(pathlen_chains[idx].untrusted); i++) {
+ const PATHLEN_CERT *pc = &pathlen_chains[idx].untrusted[i];
+
+ if (pc->file == NULL && pc->pem == NULL)
+ break;
+ if (!TEST_ptr(cert = load_pathlen_cert(pc))
+ || !TEST_true(sk_X509_push(untrusted, cert)))
+ goto err;
+ cert = NULL;
+ }
+
+ if (!TEST_true(X509_STORE_CTX_init(ctx, NULL, leaf, untrusted)))
+ goto err;
+ X509_STORE_CTX_set0_trusted_stack(ctx, trusted);
+ vpm = X509_STORE_CTX_get0_param(ctx);
+ X509_VERIFY_PARAM_set_time(vpm, PATHLEN_CHAIN_TIME);
+ if (pathlen_chains[idx].allow_proxy
+ && !TEST_true(X509_VERIFY_PARAM_set_flags(vpm, X509_V_FLAG_ALLOW_PROXY_CERTS)))
+ goto err;
+
+ if (!TEST_int_eq(X509_verify_cert(ctx),
+ pathlen_chains[idx].expected_err == X509_V_OK)
+ || !TEST_int_eq(X509_STORE_CTX_get_error(ctx),
+ pathlen_chains[idx].expected_err))
+ goto err;
+
+ ret = 1;
+err:
+ X509_free(cert);
+ X509_free(leaf);
+ OSSL_STACK_OF_X509_free(trusted);
+ OSSL_STACK_OF_X509_free(untrusted);
+ X509_STORE_CTX_free(ctx);
+ return ret;
+}
+
OPT_TEST_DECLARE_USAGE("certs-dir\n")
int setup_tests(void)
@@ -800,6 +975,7 @@ int setup_tests(void)
ADD_TEST(test_purpose_any);
ADD_TEST(test_multiname_selfsigned);
ADD_TEST(test_vpm_input_validation);
+ ADD_ALL_TESTS(test_pathlen_chain, OSSL_NELEM(pathlen_chains));
return 1;
err:
cleanup_tests();