Commit 507c152ce4 for openssl.org

commit 507c152ce408bbef7031f9e2b25d3f8c1a7dc98d
Author: Bob Beck <beck@openssl.org>
Date:   Mon Aug 24 10:31:48 2026 -0600

    Limit proxy pathlen to 0..255

    Treat an out-of-range pcPathLengthConstraint as invalid, as already
    done for basicConstraints. X509_set_proxy_pathlen() now rejects
    out-of-range values, failing closed to 0.

    Reviewed-by: Neil Horman <nhorman@openssl.org>
    Reviewed-by: Mounir Idrassi <mounir.idrassi@idrix.fr>
    Merge-date: Mon Oct  5 10:17:27 2026
    Merged-from: https://github.com/openssl/openssl/pull/32471

diff --git a/crypto/x509/v3_purp.c b/crypto/x509/v3_purp.c
index ac8899c580..9832a98a70 100644
--- a/crypto/x509/v3_purp.c
+++ b/crypto/x509/v3_purp.c
@@ -605,10 +605,22 @@ int ossl_x509v3_cache_extensions(const X509 *const_x)
             || X509_get_ext_by_NID(const_x, NID_issuer_alt_name, -1) >= 0) {
             tmp_ex_flags |= EXFLAG_INVALID;
         }
-        if (pci->pcPathLengthConstraint != NULL)
-            tmp_ex_pcpathlen = ASN1_INTEGER_get(pci->pcPathLengthConstraint);
-        else
+        if (pci->pcPathLengthConstraint != NULL) {
+            if (pci->pcPathLengthConstraint->type == V_ASN1_NEG_INTEGER) {
+                ERR_raise(ERR_LIB_X509V3, X509V3_R_NEGATIVE_PATHLEN);
+                tmp_ex_flags |= EXFLAG_INVALID;
+                tmp_ex_pcpathlen = -1;
+            } else {
+                tmp_ex_pcpathlen = ASN1_INTEGER_get(pci->pcPathLengthConstraint);
+                if (tmp_ex_pcpathlen < 0 || tmp_ex_pcpathlen > 255) {
+                    ERR_raise(ERR_LIB_X509V3, X509V3_R_PATHLEN_TOO_LARGE);
+                    tmp_ex_flags |= EXFLAG_INVALID;
+                    tmp_ex_pcpathlen = -1;
+                }
+            }
+        } else {
             tmp_ex_pcpathlen = -1;
+        }
         PROXY_CERT_INFO_EXTENSION_free(pci);
         tmp_ex_flags |= EXFLAG_PROXY;
     } else if (i != -1) {
@@ -884,6 +896,14 @@ void X509_set_proxy_flag(X509 *x)

 void X509_set_proxy_pathlen(X509 *x, long l)
 {
+    /*
+     * Out of range values cannot be reported from a void function, so
+     * fail closed: the caller asked for a constraint, and 0 permits no
+     * further delegation and can never be more permissive than the
+     * requested value.
+     */
+    if (l < -1 || l > 255)
+        l = 0;
     x->ex_pcpathlen = l;
 }

diff --git a/doc/man3/X509_get_extension_flags.pod b/doc/man3/X509_get_extension_flags.pod
index aa37d23fd1..44f6d2bf96 100644
--- a/doc/man3/X509_get_extension_flags.pod
+++ b/doc/man3/X509_get_extension_flags.pod
@@ -27,7 +27,7 @@ X509_get_proxy_pathlen - retrieve certificate extension data
  const GENERAL_NAMES *X509_get0_authority_issuer(const X509 *x);
  const ASN1_INTEGER *X509_get0_authority_serial(const X509 *x);
  void X509_set_proxy_flag(X509 *x);
- void X509_set_proxy_pathlen(int l);
+ void X509_set_proxy_pathlen(X509 *x, long l);
  long X509_get_proxy_pathlen(const X509 *x);

 =head1 DESCRIPTION
@@ -156,6 +156,9 @@ such, as OpenSSL only detects RFC3820 compliant ones.
 X509_set_proxy_pathlen() sets the proxy certificate path length for the given
 certificate B<x>.  This is for the users who need to mark non-RFC3820 proxy
 certificates as such, as OpenSSL only detects RFC3820 compliant ones.
+B<l> must be between 0 and 255 inclusive, or -1 to indicate that no
+path length constraint is set.  For any other value the path length is
+instead set to 0, permitting no further delegation.

 X509_get_proxy_pathlen() returns the proxy certificate path length for the
 given certificate B<x> if it is a proxy certificate.
@@ -193,7 +196,9 @@ pointer to an B<ASN1_OCTET_STRING> structure or B<NULL> if the extension
 is absent or an error occurred during parsing.

 X509_get_proxy_pathlen() returns the path length value if the given
-certificate is a proxy one and has a path length set, and -1 otherwise.
+certificate is a proxy one and has a path length set, and -1 otherwise,
+including when the path length is out of range or the certificate's
+extensions could not be parsed.

 =head1 SEE ALSO

diff --git a/test/v3ext.c b/test/v3ext.c
index 33a25f279a..ba8e3d25f7 100644
--- a/test/v3ext.c
+++ b/test/v3ext.c
@@ -158,6 +158,62 @@ end:
     return ret;
 }

+/*
+ * Build a proxy cert whose proxyCertInfo extension carries the given
+ * pcPathLengthConstraint and check whether extension caching accepts it.
+ * Values outside 0..255 must be rejected and reported as no constraint.
+ */
+static int test_proxy_pathlen_range_value(int idx)
+{
+    long pathlen = pathlen_range_values[idx].pathlen;
+    int valid = pathlen_range_values[idx].valid;
+    X509 *cert = NULL;
+    PROXY_CERT_INFO_EXTENSION *pci = NULL;
+    int ret = 0;
+
+    if (!TEST_ptr(cert = X509_new())
+        || !TEST_true(X509_set_version(cert, X509_VERSION_3))
+        || !TEST_ptr(pci = PROXY_CERT_INFO_EXTENSION_new())
+        || !TEST_ptr(pci->pcPathLengthConstraint = ASN1_INTEGER_new())
+        || !TEST_true(ASN1_INTEGER_set(pci->pcPathLengthConstraint, pathlen)))
+        goto end;
+    pci->proxyPolicy->policyLanguage = OBJ_nid2obj(NID_id_ppl_inheritAll);
+    if (!TEST_int_gt(X509_add1_ext_i2d(cert, NID_proxyCertInfo, pci, 1, 0), 0)
+        || !TEST_int_eq(X509_check_purpose(cert, -1, 0), valid ? 1 : -1)
+        || !TEST_long_eq(X509_get_proxy_pathlen(cert), valid ? pathlen : -1))
+        goto end;
+
+    ret = 1;
+end:
+    PROXY_CERT_INFO_EXTENSION_free(pci);
+    X509_free(cert);
+    return ret;
+}
+
+/*
+ * Check that X509_set_proxy_pathlen() accepts -1 (no constraint) and
+ * 0..255, and fails closed to a constraint of 0 for anything else.
+ */
+static int test_set_proxy_pathlen_value(int idx)
+{
+    long pathlen = pathlen_range_values[idx].pathlen;
+    long expected = pathlen >= -1 && pathlen <= 255 ? pathlen : 0;
+    X509 *cert = NULL;
+    int ret = 0;
+
+    if (!TEST_ptr(cert = X509_new()))
+        goto end;
+    X509_set_proxy_flag(cert);
+    X509_set_proxy_pathlen(cert, pathlen);
+    if (!TEST_long_eq(X509_get_proxy_pathlen(cert), expected))
+        goto end;
+
+    ret = 1;
+end:
+    X509_free(cert);
+    return ret;
+}
+
 static int test_pathlen(void)
 {
     X509 *x = NULL;
@@ -1387,6 +1443,8 @@ int setup_tests(void)

     ADD_TEST(test_pathlen);
     ADD_ALL_TESTS(test_pathlen_range_value, OSSL_NELEM(pathlen_range_values));
+    ADD_ALL_TESTS(test_proxy_pathlen_range_value, OSSL_NELEM(pathlen_range_values));
+    ADD_ALL_TESTS(test_set_proxy_pathlen_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 661d127df8..aa86fda6ee 100644
--- a/test/verify_extra_test.c
+++ b/test/verify_extra_test.c
@@ -836,6 +836,30 @@ static const char *ca_cert_pathlen_max[] = {
     NULL,
 };

+static const char *bad_pc7_cert[] = {
+    "-----BEGIN CERTIFICATE-----\n"
+    "MIIDUzCCAjugAwIBAgIBAjANBgkqhkiG9w0BAQsFADAZMRcwFQYDVQQDDA5zZXJ2\n"
+    "ZXIuZXhhbXBsZTAgFw0yNjA5MDMxOTIwMDJaGA8yMTI2MDkwNDE5MjAwMlowKzEX\n"
+    "MBUGA1UEAwwOc2VydmVyLmV4YW1wbGUxEDAOBgNVBAMMB3Byb3h5IDcwggEiMA0G\n"
+    "CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC/cun2FqC9pusaIZIPZFe9rEfEqMgC\n"
+    "7dnLm83Uxnr4KPjZ5GlygYCb9Zm3zKzL4XXEkHh//1slh51jgA9XiNpVoQGJ2TAg\n"
+    "pQ4ZhFOCIo4FOByinqQpKpPZu8/ODi6Ap9mGtvgqpPzouQZiADQr8kX+jkVeiSoO\n"
+    "DJcMLK9Ym+urY30dJWHQAV1jg7V+y4KC0gZPE7FCgZ1lkqnnR/75vq/A1L8WQiLI\n"
+    "1gXmzz5JOxZIlcpJeFtWsBpVPYlKbls1kXmLWL1CFUrg+0JrgH1kBeNe8xN+YJzz\n"
+    "dmcpzEQ2rT3a59xk3i788pLjvG8zksST4Q7iX9DU8ngg4pF3JlSW5c99AgMBAAGj\n"
+    "gZEwgY4wHQYDVR0OBBYEFCRqfkUTT5aj35YYpcN+XRVWS5XpMDUGA1UdIwQuMCyA\n"
+    "FOeb4iqtimw6y3ZR5Y4HmCKX4XOioRGkDzANMQswCQYDVQQDDAJDQYIBAjAJBgNV\n"
+    "HRMEAjAAMCsGCCsGAQUFBwEOAQH/BBwwGgIIf/////////8wDgYIKwYBBQUHFQAE\n"
+    "AkFCMA0GCSqGSIb3DQEBCwUAA4IBAQBR9qfkgHxYhlhf1lRDaKv+HgECFy4Kzlzs\n"
+    "cwAgS0vJ+EV/3TCF8SmbnmM8E0YhW+1KiFNnX9D+24swDjC94sNRL2F2Lk+JeNIP\n"
+    "7NiJUf67pnjwm4ajnZpCgUatLVg9Y/nUjFfLr1m4XLS34j98Xm3HZ+HPLBZZCzV4\n"
+    "Vr0lvc9+avniOkaQTh+lBTK1W26djm26hnssCBqDMbfVnjOzp9QCuQpNm6kxElPk\n"
+    "J5TaCMuVlaCgo4O+nfONkkHmA5yoQb6H7cprDR8JI3YNCK7uPKRfR7fkl+bOHW/p\n"
+    "aDxj2bUKdXsI0WlHwm+Bta3c0U53QWX4FB9/Ia2jnCWRS4H0GFPJ\n"
+    "-----END CERTIFICATE-----\n",
+    NULL,
+};
+
 /* Verification time for the path length chains, within all cert lifetimes */
 #define PATHLEN_CHAIN_TIME 1790000000

@@ -869,6 +893,9 @@ static const struct {
     { { "pc1-cert.pem", NULL },
         { { "ee-client.pem", NULL }, { NULL, ca_cert_pathlen_max } }, 1,
         X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY },
+    { { NULL, bad_pc7_cert },
+        { { "ee-client.pem", NULL }, { "ca-cert.pem", NULL } }, 1,
+        X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY },
 };

 static X509 *load_pathlen_cert(const PATHLEN_CERT *pc)