Skip to content

Commit c4b4e6c

Browse files
committed
NameConstraints: support wildcard SAN
1 parent 2dd7947 commit c4b4e6c

5 files changed

Lines changed: 623 additions & 8 deletions

File tree

tests/api.c

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24480,6 +24480,172 @@ static int test_NameConstraints_OtherName(void)
2448024480
return EXPECT_RESULT();
2448124481
}
2448224482

24483+
#if defined(WOLFSSL_ASN_TEMPLATE) && \
24484+
defined(WOLFSSL_CERT_REQ) && !defined(NO_ASN_TIME) && \
24485+
defined(WOLFSSL_CERT_GEN) && defined(HAVE_ECC) && \
24486+
defined(WOLFSSL_CERT_EXT) && !defined(NO_CERTS) && \
24487+
defined(WOLFSSL_ALT_NAMES) && defined(WOLFSSL_CUSTOM_OID) && \
24488+
defined(HAVE_OID_ENCODING) && !defined(IGNORE_NAME_CONSTRAINTS)
24489+
/* Build a SubjectAltName extension value with a single GeneralName of the
24490+
* given context tag (0x82 dnsName, 0x86 URI) carrying `val`. */
24491+
static word32 build_simple_san(byte* out, word32 outSz, byte gnTag,
24492+
const char* val)
24493+
{
24494+
word32 vlen = (word32)XSTRLEN(val);
24495+
if (vlen > 0x7F || outSz < vlen + 4)
24496+
return 0;
24497+
out[0] = 0x30; /* SEQUENCE */
24498+
out[1] = (byte)(vlen + 2);
24499+
out[2] = gnTag; /* [tag] GeneralName */
24500+
out[3] = (byte)vlen;
24501+
XMEMCPY(out + 4, val, vlen);
24502+
return vlen + 4;
24503+
}
24504+
24505+
/* Build a NameConstraints extension value with a single subtree ([0]
24506+
* permitted or [1] excluded) carrying a GeneralName of context tag `gnTag`
24507+
* with value `val`. */
24508+
static word32 build_simple_nameConstraints(byte* out, word32 outSz,
24509+
int excluded, byte gnTag, const char* val)
24510+
{
24511+
word32 vlen = (word32)XSTRLEN(val);
24512+
word32 n3 = vlen + 2; /* GeneralSubtree content: the base GN */
24513+
word32 n2 = n3 + 2; /* subtrees list content: one GeneralSubtree */
24514+
word32 n1 = n2 + 2; /* SEQUENCE content: the subtrees list */
24515+
if (vlen > 0x7F || outSz < n1 + 2)
24516+
return 0;
24517+
out[0] = 0x30; /* SEQUENCE */
24518+
out[1] = (byte)n1;
24519+
out[2] = excluded ? 0xA1 : 0xA0; /* [1] excluded / [0] permitted */
24520+
out[3] = (byte)n2;
24521+
out[4] = 0x30; /* GeneralSubtree */
24522+
out[5] = (byte)n3;
24523+
out[6] = gnTag; /* base GeneralName */
24524+
out[7] = (byte)vlen;
24525+
XMEMCPY(out + 8, val, vlen);
24526+
return n1 + 2;
24527+
}
24528+
#endif
24529+
24530+
/* End-to-end enforcement of DNS and URI nameConstraints against wildcard and
24531+
* sub-host leaf SANs, exercised through the real chain-verification path.
24532+
*
24533+
* DNS subtree semantics (RFC 5280 4.2.1.10) plus wildcard reasoning:
24534+
* - excluded DNS:foo.example.com must reject a leaf SAN *.example.com,
24535+
* because the wildcard can expand to the excluded host (security gap).
24536+
* - permitted DNS:example.com must accept *.example.com (contained), while
24537+
* permitted DNS:foo.example.com must reject it (the wildcard can escape).
24538+
* URI semantics: a constraint without a leading dot is an EXACT host match,
24539+
* not a subtree, so permitted URI:host.com must reject https://www.host.com/.
24540+
*/
24541+
static int test_NameConstraints_DnsUriWildcard(void)
24542+
{
24543+
EXPECT_DECLS;
24544+
#if defined(WOLFSSL_ASN_TEMPLATE) && \
24545+
defined(WOLFSSL_CERT_REQ) && !defined(NO_ASN_TIME) && \
24546+
defined(WOLFSSL_CERT_GEN) && defined(HAVE_ECC) && \
24547+
defined(WOLFSSL_CERT_EXT) && !defined(NO_CERTS) && \
24548+
defined(WOLFSSL_ALT_NAMES) && defined(WOLFSSL_CUSTOM_OID) && \
24549+
defined(HAVE_OID_ENCODING) && !defined(IGNORE_NAME_CONSTRAINTS)
24550+
byte nc[64];
24551+
byte san[64];
24552+
word32 ncSz, sanSz;
24553+
const byte DNS = 0x82; /* [2] dnsName */
24554+
const byte URI = 0x86; /* [6] uniformResourceIdentifier */
24555+
24556+
/* --- DNS, excluded subtree --- */
24557+
24558+
/* (1) Excluded foo.example.com vs wildcard SAN that can reach it: the
24559+
* pre-fix byte-length guard let this through. Must reject. */
24560+
ncSz = build_simple_nameConstraints(nc, sizeof(nc), 1, DNS,
24561+
"foo.example.com");
24562+
sanSz = build_simple_san(san, sizeof(san), DNS, "*.example.com");
24563+
ExpectIntGT((int)ncSz, 0);
24564+
ExpectIntGT((int)sanSz, 0);
24565+
ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz),
24566+
WC_NO_ERR_TRACE(ASN_NAME_INVALID_E));
24567+
24568+
/* (2) Positive control: same exclusion, wildcard in a different domain
24569+
* cannot reach foo.example.com -> accept. */
24570+
sanSz = build_simple_san(san, sizeof(san), DNS, "*.other.com");
24571+
ExpectIntGT((int)sanSz, 0);
24572+
ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz), 0);
24573+
24574+
/* (3) Regression: literal SAN inside the excluded subtree still rejects. */
24575+
sanSz = build_simple_san(san, sizeof(san), DNS, "foo.example.com");
24576+
ExpectIntGT((int)sanSz, 0);
24577+
ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz),
24578+
WC_NO_ERR_TRACE(ASN_NAME_INVALID_E));
24579+
24580+
/* (4) Regression: literal SAN outside the excluded subtree accepts. */
24581+
sanSz = build_simple_san(san, sizeof(san), DNS, "bar.other.com");
24582+
ExpectIntGT((int)sanSz, 0);
24583+
ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz), 0);
24584+
24585+
/* --- DNS, permitted subtree --- */
24586+
24587+
/* (5) Permitted example.com accepts *.example.com (fully contained). */
24588+
ncSz = build_simple_nameConstraints(nc, sizeof(nc), 0, DNS, "example.com");
24589+
sanSz = build_simple_san(san, sizeof(san), DNS, "*.example.com");
24590+
ExpectIntGT((int)ncSz, 0);
24591+
ExpectIntGT((int)sanSz, 0);
24592+
ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz), 0);
24593+
24594+
/* (6) Permitted foo.example.com rejects *.example.com (the wildcard can
24595+
* expand outside the permitted subtree). */
24596+
ncSz = build_simple_nameConstraints(nc, sizeof(nc), 0, DNS,
24597+
"foo.example.com");
24598+
sanSz = build_simple_san(san, sizeof(san), DNS, "*.example.com");
24599+
ExpectIntGT((int)ncSz, 0);
24600+
ExpectIntGT((int)sanSz, 0);
24601+
ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz),
24602+
WC_NO_ERR_TRACE(ASN_NAME_INVALID_E));
24603+
24604+
/* --- URI, exact-host vs subtree semantics --- */
24605+
24606+
/* (7) Permitted URI host.com (no leading dot) is an EXACT host match:
24607+
* a sub-host URI is NOT contained -> reject. Pre-fix this wrongly
24608+
* subtree-matched and accepted. */
24609+
ncSz = build_simple_nameConstraints(nc, sizeof(nc), 0, URI, "host.com");
24610+
sanSz = build_simple_san(san, sizeof(san), URI, "https://www.host.com/");
24611+
ExpectIntGT((int)ncSz, 0);
24612+
ExpectIntGT((int)sanSz, 0);
24613+
ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz),
24614+
WC_NO_ERR_TRACE(ASN_NAME_INVALID_E));
24615+
24616+
/* (8) Permitted URI host.com accepts the exact host. */
24617+
sanSz = build_simple_san(san, sizeof(san), URI, "https://host.com/path");
24618+
ExpectIntGT((int)sanSz, 0);
24619+
ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz), 0);
24620+
24621+
/* (9) Permitted URI .host.com (leading dot) is a subtree: the sub-host is
24622+
* accepted but the bare host is not. */
24623+
ncSz = build_simple_nameConstraints(nc, sizeof(nc), 0, URI, ".host.com");
24624+
sanSz = build_simple_san(san, sizeof(san), URI, "https://www.host.com/");
24625+
ExpectIntGT((int)ncSz, 0);
24626+
ExpectIntGT((int)sanSz, 0);
24627+
ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz), 0);
24628+
24629+
sanSz = build_simple_san(san, sizeof(san), URI, "https://host.com/");
24630+
ExpectIntGT((int)sanSz, 0);
24631+
ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz),
24632+
WC_NO_ERR_TRACE(ASN_NAME_INVALID_E));
24633+
24634+
/* (10) Excluded URI host.com (exact): bare host excluded, sub-host not. */
24635+
ncSz = build_simple_nameConstraints(nc, sizeof(nc), 1, URI, "host.com");
24636+
sanSz = build_simple_san(san, sizeof(san), URI, "https://host.com/");
24637+
ExpectIntGT((int)ncSz, 0);
24638+
ExpectIntGT((int)sanSz, 0);
24639+
ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz),
24640+
WC_NO_ERR_TRACE(ASN_NAME_INVALID_E));
24641+
24642+
sanSz = build_simple_san(san, sizeof(san), URI, "https://www.host.com/");
24643+
ExpectIntGT((int)sanSz, 0);
24644+
ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz), 0);
24645+
#endif
24646+
return EXPECT_RESULT();
24647+
}
24648+
2448324649
static int test_MakeCertWithCaFalse(void)
2448424650
{
2448524651
EXPECT_DECLS;
@@ -40240,6 +40406,7 @@ TEST_CASE testCases[] = {
4024040406
TEST_DECL(test_PathLenSelfIssuedAllowed),
4024140407
TEST_DECL(test_PathLenNoKeyUsage),
4024240408
TEST_DECL(test_NameConstraints_OtherName),
40409+
TEST_DECL(test_NameConstraints_DnsUriWildcard),
4024340410
TEST_DECL(test_ParseSerial0FixtureMatrix),
4024440411
TEST_DECL(test_MakeCertWithCaFalse),
4024540412
#ifdef WOLFSSL_CERT_SIGN_CB

0 commit comments

Comments
 (0)