From 2124a1075f3b4f3f97efe46add8c145f06ca5a8f Mon Sep 17 00:00:00 2001 From: aidan garske Date: Mon, 29 Jun 2026 16:10:15 -0700 Subject: [PATCH] F-6558 - Reject name constraint subtree with non-zero minimum or maximum --- tests/api.c | 96 +++++++++++++++++++++++++++++++++++++++++++++ wolfcrypt/src/asn.c | 15 ++++++- 2 files changed, 110 insertions(+), 1 deletion(-) diff --git a/tests/api.c b/tests/api.c index 32b90b9a079..362c7dcb485 100644 --- a/tests/api.c +++ b/tests/api.c @@ -23208,6 +23208,47 @@ static word32 build_simple_nameConstraints(byte* out, word32 outSz, XMEMCPY(out + 8, val, vlen); return n1 + 2; } + +/* Build a NameConstraints extension value with a single subtree ([0] + * permitted or [1] excluded) whose GeneralSubtree carries a base GeneralName + * of context tag `gnTag` plus optional minimum ([0]) and maximum ([1]) + * BaseDistance fields. Used to confirm that a non-zero minimum or any + * maximum is rejected (RFC 5280 4.2.1.10 requires minimum 0, maximum absent + * within this profile). */ +static word32 build_minmax_nameConstraints(byte* out, word32 outSz, + int excluded, byte gnTag, const char* val, int minPresent, byte minVal, + int maxPresent, byte maxVal) +{ + word32 vlen = (word32)XSTRLEN(val); + word32 extra = (word32)((minPresent ? 3 : 0) + (maxPresent ? 3 : 0)); + word32 n3 = vlen + 2 + extra; /* GeneralSubtree content: base GN + min/max */ + word32 n2 = n3 + 2; /* subtrees list content: one GeneralSubtree */ + word32 n1 = n2 + 2; /* SEQUENCE content: the subtrees list */ + word32 idx; + if (vlen > 0x7F || n3 > 0x7F || outSz < n1 + 2) + return 0; + out[0] = 0x30; /* SEQUENCE */ + out[1] = (byte)n1; + out[2] = excluded ? 0xA1 : 0xA0; /* [1] excluded / [0] permitted */ + out[3] = (byte)n2; + out[4] = 0x30; /* GeneralSubtree */ + out[5] = (byte)n3; + out[6] = gnTag; /* base GeneralName */ + out[7] = (byte)vlen; + XMEMCPY(out + 8, val, vlen); + idx = 8 + vlen; + if (minPresent) { + out[idx++] = 0x80; /* [0] minimum BaseDistance */ + out[idx++] = 0x01; + out[idx++] = minVal; + } + if (maxPresent) { + out[idx++] = 0x81; /* [1] maximum BaseDistance */ + out[idx++] = 0x01; + out[idx++] = maxVal; + } + return n1 + 2; +} #endif /* End-to-end enforcement of DNS and URI nameConstraints against wildcard and @@ -23329,6 +23370,60 @@ static int test_NameConstraints_DnsUriWildcard(void) return EXPECT_RESULT(); } +/* A GeneralSubtree's minimum/maximum BaseDistance fields were parsed but + * never stored or checked, so a subtree carrying a non-zero minimum or any + * maximum was silently accepted and then enforced as if it were minimum 0 / + * maximum absent. RFC 5280 4.2.1.10 requires minimum 0 and maximum absent + * within this profile, so such a constraint must be rejected. The bad + * constraint rides on the issuing CA, so a rejection surfaces as a failure + * to build the chain rather than a specific leaf error code. */ +static int test_NameConstraints_SubtreeMinMax(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_ASN_TEMPLATE) && \ + defined(WOLFSSL_CERT_REQ) && !defined(NO_ASN_TIME) && \ + defined(WOLFSSL_CERT_GEN) && defined(HAVE_ECC) && \ + defined(WOLFSSL_CERT_EXT) && !defined(NO_CERTS) && \ + defined(WOLFSSL_ALT_NAMES) && defined(WOLFSSL_CUSTOM_OID) && \ + defined(HAVE_OID_ENCODING) && !defined(IGNORE_NAME_CONSTRAINTS) + byte nc[64]; + word32 ncSz; + const byte DNS = 0x82; /* [2] dnsName */ + + /* (1) Control: minimum and maximum both absent -> conformant, accepts. */ + ncSz = build_minmax_nameConstraints(nc, sizeof(nc), 1, DNS, "example.com", + 0, 0, 0, 0); + ExpectIntGT((int)ncSz, 0); + ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, NULL, 0), 0); + + /* (2) Control: explicit minimum == 0 is conformant -> accepts. Pins the + * rejection below to a non-zero minimum, not to any minimum field. */ + ncSz = build_minmax_nameConstraints(nc, sizeof(nc), 1, DNS, "example.com", + 1, 0, 0, 0); + ExpectIntGT((int)ncSz, 0); + ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, NULL, 0), 0); + + /* (3) minimum == 1 must be rejected. */ + ncSz = build_minmax_nameConstraints(nc, sizeof(nc), 1, DNS, "example.com", + 1, 1, 0, 0); + ExpectIntGT((int)ncSz, 0); + ExpectIntNE(verify_with_otherName_chain(nc, ncSz, 1, NULL, 0), 0); + + /* (4) maximum present (== 0) must be rejected. */ + ncSz = build_minmax_nameConstraints(nc, sizeof(nc), 1, DNS, "example.com", + 0, 0, 1, 0); + ExpectIntGT((int)ncSz, 0); + ExpectIntNE(verify_with_otherName_chain(nc, ncSz, 1, NULL, 0), 0); + + /* (5) maximum present (== 5) must be rejected. */ + ncSz = build_minmax_nameConstraints(nc, sizeof(nc), 0, DNS, "example.com", + 0, 0, 1, 5); + ExpectIntGT((int)ncSz, 0); + ExpectIntNE(verify_with_otherName_chain(nc, ncSz, 1, NULL, 0), 0); +#endif + return EXPECT_RESULT(); +} + static int test_MakeCertWithCaFalse(void) { EXPECT_DECLS; @@ -34948,6 +35043,7 @@ TEST_CASE testCases[] = { TEST_DECL(test_PathLenNoKeyUsage), TEST_DECL(test_NameConstraints_OtherName), TEST_DECL(test_NameConstraints_DnsUriWildcard), + TEST_DECL(test_NameConstraints_SubtreeMinMax), TEST_DECL(test_ParseSerial0FixtureMatrix), TEST_DECL(test_MakeCertWithCaFalse), #ifdef WOLFSSL_CERT_SIGN_CB diff --git a/wolfcrypt/src/asn.c b/wolfcrypt/src/asn.c index 7e855986f67..8d5675413f6 100644 --- a/wolfcrypt/src/asn.c +++ b/wolfcrypt/src/asn.c @@ -20362,7 +20362,20 @@ static int DecodeSubtree(const byte* input, word32 sz, Base_entry** head, ret = GetASN_Items(subTreeASN, dataASN, subTreeASN_Length, 0, input, &idx, sz); if (ret == 0) { - byte t = dataASN[SUBTREEASN_IDX_BASE].tag; + byte t; + + /* RFC 5280 Sec. 4.2.1.10: within this profile minimum must be 0 + * and maximum must be absent. Reject a subtree that carries a + * non-zero minimum or any maximum rather than enforcing it as if + * those fields were the defaults. */ + if ((minVal != 0) || + (dataASN[SUBTREEASN_IDX_MAX].length > 0)) { + WOLFSSL_MSG("unsupported name constraint minimum/maximum"); + ret = ASN_NAME_INVALID_E; + break; + } + + t = dataASN[SUBTREEASN_IDX_BASE].tag; /* Check GeneralName tag is one of the types we can handle. * registeredID is included so that ConfirmNameConstraints can