mirror of
https://github.com/wolfSSL/wolfssl.git
synced 2026-07-05 15:50:51 +02:00
NameConstraints: support wildcard SAN
This commit is contained in:
+167
@@ -24480,6 +24480,172 @@ static int test_NameConstraints_OtherName(void)
|
||||
return EXPECT_RESULT();
|
||||
}
|
||||
|
||||
#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)
|
||||
/* Build a SubjectAltName extension value with a single GeneralName of the
|
||||
* given context tag (0x82 dnsName, 0x86 URI) carrying `val`. */
|
||||
static word32 build_simple_san(byte* out, word32 outSz, byte gnTag,
|
||||
const char* val)
|
||||
{
|
||||
word32 vlen = (word32)XSTRLEN(val);
|
||||
if (vlen > 0x7F || outSz < vlen + 4)
|
||||
return 0;
|
||||
out[0] = 0x30; /* SEQUENCE */
|
||||
out[1] = (byte)(vlen + 2);
|
||||
out[2] = gnTag; /* [tag] GeneralName */
|
||||
out[3] = (byte)vlen;
|
||||
XMEMCPY(out + 4, val, vlen);
|
||||
return vlen + 4;
|
||||
}
|
||||
|
||||
/* Build a NameConstraints extension value with a single subtree ([0]
|
||||
* permitted or [1] excluded) carrying a GeneralName of context tag `gnTag`
|
||||
* with value `val`. */
|
||||
static word32 build_simple_nameConstraints(byte* out, word32 outSz,
|
||||
int excluded, byte gnTag, const char* val)
|
||||
{
|
||||
word32 vlen = (word32)XSTRLEN(val);
|
||||
word32 n3 = vlen + 2; /* GeneralSubtree content: the base GN */
|
||||
word32 n2 = n3 + 2; /* subtrees list content: one GeneralSubtree */
|
||||
word32 n1 = n2 + 2; /* SEQUENCE content: the subtrees list */
|
||||
if (vlen > 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);
|
||||
return n1 + 2;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* End-to-end enforcement of DNS and URI nameConstraints against wildcard and
|
||||
* sub-host leaf SANs, exercised through the real chain-verification path.
|
||||
*
|
||||
* DNS subtree semantics (RFC 5280 4.2.1.10) plus wildcard reasoning:
|
||||
* - excluded DNS:foo.example.com must reject a leaf SAN *.example.com,
|
||||
* because the wildcard can expand to the excluded host (security gap).
|
||||
* - permitted DNS:example.com must accept *.example.com (contained), while
|
||||
* permitted DNS:foo.example.com must reject it (the wildcard can escape).
|
||||
* URI semantics: a constraint without a leading dot is an EXACT host match,
|
||||
* not a subtree, so permitted URI:host.com must reject https://www.host.com/.
|
||||
*/
|
||||
static int test_NameConstraints_DnsUriWildcard(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];
|
||||
byte san[64];
|
||||
word32 ncSz, sanSz;
|
||||
const byte DNS = 0x82; /* [2] dnsName */
|
||||
const byte URI = 0x86; /* [6] uniformResourceIdentifier */
|
||||
|
||||
/* --- DNS, excluded subtree --- */
|
||||
|
||||
/* (1) Excluded foo.example.com vs wildcard SAN that can reach it: the
|
||||
* pre-fix byte-length guard let this through. Must reject. */
|
||||
ncSz = build_simple_nameConstraints(nc, sizeof(nc), 1, DNS,
|
||||
"foo.example.com");
|
||||
sanSz = build_simple_san(san, sizeof(san), DNS, "*.example.com");
|
||||
ExpectIntGT((int)ncSz, 0);
|
||||
ExpectIntGT((int)sanSz, 0);
|
||||
ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz),
|
||||
WC_NO_ERR_TRACE(ASN_NAME_INVALID_E));
|
||||
|
||||
/* (2) Positive control: same exclusion, wildcard in a different domain
|
||||
* cannot reach foo.example.com -> accept. */
|
||||
sanSz = build_simple_san(san, sizeof(san), DNS, "*.other.com");
|
||||
ExpectIntGT((int)sanSz, 0);
|
||||
ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz), 0);
|
||||
|
||||
/* (3) Regression: literal SAN inside the excluded subtree still rejects. */
|
||||
sanSz = build_simple_san(san, sizeof(san), DNS, "foo.example.com");
|
||||
ExpectIntGT((int)sanSz, 0);
|
||||
ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz),
|
||||
WC_NO_ERR_TRACE(ASN_NAME_INVALID_E));
|
||||
|
||||
/* (4) Regression: literal SAN outside the excluded subtree accepts. */
|
||||
sanSz = build_simple_san(san, sizeof(san), DNS, "bar.other.com");
|
||||
ExpectIntGT((int)sanSz, 0);
|
||||
ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz), 0);
|
||||
|
||||
/* --- DNS, permitted subtree --- */
|
||||
|
||||
/* (5) Permitted example.com accepts *.example.com (fully contained). */
|
||||
ncSz = build_simple_nameConstraints(nc, sizeof(nc), 0, DNS, "example.com");
|
||||
sanSz = build_simple_san(san, sizeof(san), DNS, "*.example.com");
|
||||
ExpectIntGT((int)ncSz, 0);
|
||||
ExpectIntGT((int)sanSz, 0);
|
||||
ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz), 0);
|
||||
|
||||
/* (6) Permitted foo.example.com rejects *.example.com (the wildcard can
|
||||
* expand outside the permitted subtree). */
|
||||
ncSz = build_simple_nameConstraints(nc, sizeof(nc), 0, DNS,
|
||||
"foo.example.com");
|
||||
sanSz = build_simple_san(san, sizeof(san), DNS, "*.example.com");
|
||||
ExpectIntGT((int)ncSz, 0);
|
||||
ExpectIntGT((int)sanSz, 0);
|
||||
ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz),
|
||||
WC_NO_ERR_TRACE(ASN_NAME_INVALID_E));
|
||||
|
||||
/* --- URI, exact-host vs subtree semantics --- */
|
||||
|
||||
/* (7) Permitted URI host.com (no leading dot) is an EXACT host match:
|
||||
* a sub-host URI is NOT contained -> reject. Pre-fix this wrongly
|
||||
* subtree-matched and accepted. */
|
||||
ncSz = build_simple_nameConstraints(nc, sizeof(nc), 0, URI, "host.com");
|
||||
sanSz = build_simple_san(san, sizeof(san), URI, "https://www.host.com/");
|
||||
ExpectIntGT((int)ncSz, 0);
|
||||
ExpectIntGT((int)sanSz, 0);
|
||||
ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz),
|
||||
WC_NO_ERR_TRACE(ASN_NAME_INVALID_E));
|
||||
|
||||
/* (8) Permitted URI host.com accepts the exact host. */
|
||||
sanSz = build_simple_san(san, sizeof(san), URI, "https://host.com/path");
|
||||
ExpectIntGT((int)sanSz, 0);
|
||||
ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz), 0);
|
||||
|
||||
/* (9) Permitted URI .host.com (leading dot) is a subtree: the sub-host is
|
||||
* accepted but the bare host is not. */
|
||||
ncSz = build_simple_nameConstraints(nc, sizeof(nc), 0, URI, ".host.com");
|
||||
sanSz = build_simple_san(san, sizeof(san), URI, "https://www.host.com/");
|
||||
ExpectIntGT((int)ncSz, 0);
|
||||
ExpectIntGT((int)sanSz, 0);
|
||||
ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz), 0);
|
||||
|
||||
sanSz = build_simple_san(san, sizeof(san), URI, "https://host.com/");
|
||||
ExpectIntGT((int)sanSz, 0);
|
||||
ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz),
|
||||
WC_NO_ERR_TRACE(ASN_NAME_INVALID_E));
|
||||
|
||||
/* (10) Excluded URI host.com (exact): bare host excluded, sub-host not. */
|
||||
ncSz = build_simple_nameConstraints(nc, sizeof(nc), 1, URI, "host.com");
|
||||
sanSz = build_simple_san(san, sizeof(san), URI, "https://host.com/");
|
||||
ExpectIntGT((int)ncSz, 0);
|
||||
ExpectIntGT((int)sanSz, 0);
|
||||
ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz),
|
||||
WC_NO_ERR_TRACE(ASN_NAME_INVALID_E));
|
||||
|
||||
sanSz = build_simple_san(san, sizeof(san), URI, "https://www.host.com/");
|
||||
ExpectIntGT((int)sanSz, 0);
|
||||
ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz), 0);
|
||||
#endif
|
||||
return EXPECT_RESULT();
|
||||
}
|
||||
|
||||
static int test_MakeCertWithCaFalse(void)
|
||||
{
|
||||
EXPECT_DECLS;
|
||||
@@ -40240,6 +40406,7 @@ TEST_CASE testCases[] = {
|
||||
TEST_DECL(test_PathLenSelfIssuedAllowed),
|
||||
TEST_DECL(test_PathLenNoKeyUsage),
|
||||
TEST_DECL(test_NameConstraints_OtherName),
|
||||
TEST_DECL(test_NameConstraints_DnsUriWildcard),
|
||||
TEST_DECL(test_ParseSerial0FixtureMatrix),
|
||||
TEST_DECL(test_MakeCertWithCaFalse),
|
||||
#ifdef WOLFSSL_CERT_SIGN_CB
|
||||
|
||||
@@ -849,6 +849,19 @@ int test_wolfssl_local_MatchBaseName(void)
|
||||
"sub.domain.com", 14, ".domain.com", 11), 1);
|
||||
ExpectIntEQ(wolfssl_local_MatchBaseName(ASN_DNS_TYPE,
|
||||
"a.b.domain.com", 14, ".domain.com", 11), 1);
|
||||
/* Trailing-dot normalization: absolute DNS form is equivalent. */
|
||||
ExpectIntEQ(wolfssl_local_MatchBaseName(ASN_DNS_TYPE,
|
||||
"domain.com.", (int)XSTRLEN("domain.com."),
|
||||
"domain.com", (int)XSTRLEN("domain.com")), 1);
|
||||
ExpectIntEQ(wolfssl_local_MatchBaseName(ASN_DNS_TYPE,
|
||||
"domain.com", (int)XSTRLEN("domain.com"),
|
||||
"domain.com.", (int)XSTRLEN("domain.com.")), 1);
|
||||
ExpectIntEQ(wolfssl_local_MatchBaseName(ASN_DNS_TYPE,
|
||||
"domain.com.", (int)XSTRLEN("domain.com."),
|
||||
"domain.com.", (int)XSTRLEN("domain.com.")), 1);
|
||||
ExpectIntEQ(wolfssl_local_MatchBaseName(ASN_DNS_TYPE,
|
||||
"sub.domain.com.", (int)XSTRLEN("sub.domain.com."),
|
||||
".domain.com.", (int)XSTRLEN(".domain.com.")), 1);
|
||||
|
||||
/* Negative tests - should NOT match */
|
||||
/* Bug #3: fakedomain.com should NOT match domain.com (no dot boundary) */
|
||||
@@ -870,6 +883,10 @@ int test_wolfssl_local_MatchBaseName(void)
|
||||
/* Name starting with dot */
|
||||
ExpectIntEQ(wolfssl_local_MatchBaseName(ASN_DNS_TYPE,
|
||||
".domain.com", 11, "domain.com", 10), 0);
|
||||
/* More than one trailing dot leaves an empty label after normalization. */
|
||||
ExpectIntEQ(wolfssl_local_MatchBaseName(ASN_DNS_TYPE,
|
||||
"domain.com..", (int)XSTRLEN("domain.com.."),
|
||||
"domain.com", (int)XSTRLEN("domain.com")), 0);
|
||||
|
||||
/*
|
||||
* Tests for email type (ASN_RFC822_TYPE = 0x01)
|
||||
@@ -969,6 +986,210 @@ int test_wolfssl_local_MatchBaseName(void)
|
||||
return EXPECT_RESULT();
|
||||
}
|
||||
|
||||
#if !defined(NO_CERTS) && !defined(NO_ASN) && !defined(IGNORE_NAME_CONSTRAINTS)
|
||||
/* Convenience wrappers so the cases below read as (name, base) pairs and the
|
||||
* string lengths can't drift out of sync with the literals. */
|
||||
static int dnsWildPermitted(const char* name, const char* base)
|
||||
{
|
||||
return wolfssl_local_MatchDnsConstraintWildcard(name, (int)XSTRLEN(name),
|
||||
base, (int)XSTRLEN(base), 1);
|
||||
}
|
||||
static int dnsWildExcluded(const char* name, const char* base)
|
||||
{
|
||||
return wolfssl_local_MatchDnsConstraintWildcard(name, (int)XSTRLEN(name),
|
||||
base, (int)XSTRLEN(base), 0);
|
||||
}
|
||||
static int uriNC(const char* uri, const char* base)
|
||||
{
|
||||
return wolfssl_local_MatchUriNameConstraint(uri, (int)XSTRLEN(uri), base,
|
||||
(int)XSTRLEN(base));
|
||||
}
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Tests label-aware matching of a wildcard DNS SAN against a name-constraint
|
||||
* subtree. The permitted variant must prove containment (every expansion of
|
||||
* the wildcard stays inside the subtree); the excluded variant must detect
|
||||
* intersection (some expansion falls inside the subtree). A '*' never crosses
|
||||
* a label boundary, so the comparison is by label from the right.
|
||||
*/
|
||||
int test_wolfssl_local_MatchDnsConstraintWildcard(void)
|
||||
{
|
||||
EXPECT_DECLS;
|
||||
|
||||
#if !defined(NO_CERTS) && !defined(NO_ASN) && !defined(IGNORE_NAME_CONSTRAINTS)
|
||||
/*
|
||||
* PERMITTED subtree -- containment. Accept only when EVERY expansion of
|
||||
* the wildcard is inside the base subtree.
|
||||
*/
|
||||
|
||||
/* Wildcard is an extra label to the left of the base: always contained. */
|
||||
ExpectIntEQ(dnsWildPermitted("*.example.com", "example.com"), 1);
|
||||
ExpectIntEQ(dnsWildPermitted("*.sub.example.com", "example.com"), 1);
|
||||
ExpectIntEQ(dnsWildPermitted("foo*.example.com", "example.com"), 1);
|
||||
ExpectIntEQ(dnsWildPermitted("a*b.example.com", "example.com"), 1);
|
||||
/* Case-insensitive on the literal tail labels. */
|
||||
ExpectIntEQ(dnsWildPermitted("*.EXAMPLE.CoM", "example.com"), 1);
|
||||
/* Single-label base; the matched tail "com" is literal. */
|
||||
ExpectIntEQ(dnsWildPermitted("*.example.com", "com"), 1);
|
||||
/* Leading-dot base requires at least one label before it -- the wildcard
|
||||
* label satisfies that. */
|
||||
ExpectIntEQ(dnsWildPermitted("*.example.com", ".example.com"), 1);
|
||||
ExpectIntEQ(dnsWildPermitted("*.sub.example.com", ".example.com"), 1);
|
||||
/* Trailing-dot normalization: absolute DNS form is equivalent. */
|
||||
ExpectIntEQ(dnsWildPermitted("*.example.com.", "example.com"), 1);
|
||||
ExpectIntEQ(dnsWildPermitted("*.example.com", "example.com."), 1);
|
||||
ExpectIntEQ(dnsWildPermitted("*.example.com.", "example.com."), 1);
|
||||
ExpectIntEQ(dnsWildPermitted("*.example.com.", ".example.com."), 1);
|
||||
|
||||
/* Wildcard lands on a label that must equal the base: NOT provably
|
||||
* contained, because the label can expand to something else. */
|
||||
ExpectIntEQ(dnsWildPermitted("*.example.com", "foo.example.com"), 0);
|
||||
ExpectIntEQ(dnsWildPermitted("*.example.com.", "foo.example.com"), 0);
|
||||
ExpectIntEQ(dnsWildPermitted("*.example.com", "foo.example.com."), 0);
|
||||
ExpectIntEQ(dnsWildPermitted("ex*.com", "example.com"), 0);
|
||||
ExpectIntEQ(dnsWildPermitted("foo.exa*ple.com", "example.com"), 0);
|
||||
/* Tail labels do not match the base at all. */
|
||||
ExpectIntEQ(dnsWildPermitted("*.example.com", "example.org"), 0);
|
||||
ExpectIntEQ(dnsWildPermitted("*.evil.com", "example.com"), 0);
|
||||
/* Leading-dot base, but wildcard would have to equal an interior base
|
||||
* label. */
|
||||
ExpectIntEQ(dnsWildPermitted("*.example.com", ".sub.example.com"), 0);
|
||||
/* A bare '*' cannot be proven inside any multi-label-or-single subtree. */
|
||||
ExpectIntEQ(dnsWildPermitted("*", "com"), 0);
|
||||
|
||||
/*
|
||||
* EXCLUDED subtree -- intersection. Reject when SOME expansion of the
|
||||
* wildcard falls inside the base subtree. A wildcard label is
|
||||
* conservatively treated as able to match any single base label.
|
||||
*/
|
||||
|
||||
ExpectIntEQ(dnsWildExcluded("*.example.com", "foo.example.com"), 1);
|
||||
ExpectIntEQ(dnsWildExcluded("*.example.com.", "foo.example.com"), 1);
|
||||
ExpectIntEQ(dnsWildExcluded("*.example.com", "foo.example.com."), 1);
|
||||
ExpectIntEQ(dnsWildExcluded("*.example.com.", "foo.example.com."), 1);
|
||||
/* Wildcard adds a label on top of the excluded subtree. */
|
||||
ExpectIntEQ(dnsWildExcluded("*.example.com", "example.com"), 1);
|
||||
ExpectIntEQ(dnsWildExcluded("*.example.com", "com"), 1);
|
||||
ExpectIntEQ(dnsWildExcluded("*.example.com", ".example.com"), 1);
|
||||
/* Wildcard in a non-left label still intersects. */
|
||||
ExpectIntEQ(dnsWildExcluded("foo.*.example.com", "bar.example.com"), 1);
|
||||
/* Partial-label wildcard: conservatively excluded even though "ex*"
|
||||
* cannot actually expand to "foo" (over-rejection, safe). */
|
||||
ExpectIntEQ(dnsWildExcluded("ex*.example.com", "foo.example.com"), 1);
|
||||
/* A bare '*' can expand to the apex label of a single-label subtree. */
|
||||
ExpectIntEQ(dnsWildExcluded("*", "com"), 1);
|
||||
|
||||
/* No intersection: literal tail labels differ from the base. */
|
||||
ExpectIntEQ(dnsWildExcluded("*.example.com", "foo.other.com"), 0);
|
||||
ExpectIntEQ(dnsWildExcluded("*.other.com", "example.com"), 0);
|
||||
ExpectIntEQ(dnsWildExcluded("*.example.com", "example.org"), 0);
|
||||
/* Leading-dot excluded base needs a label before it; the wildcard SAN has
|
||||
* no room for one, so no expansion reaches the proper subtree. */
|
||||
ExpectIntEQ(dnsWildExcluded("*.example.com", ".foo.example.com"), 0);
|
||||
/* Same arity: '*' can expand to the apex label of the base, so the
|
||||
* wildcard intersects (*.com can be example.com, which is excluded). */
|
||||
ExpectIntEQ(dnsWildExcluded("*.com", "example.com"), 1);
|
||||
/* But a base with MORE labels than the name cannot be reached. */
|
||||
ExpectIntEQ(dnsWildExcluded("*.com", "a.example.com"), 0);
|
||||
|
||||
/*
|
||||
* Error / degenerate inputs (both flags reject).
|
||||
*/
|
||||
ExpectIntEQ(wolfssl_local_MatchDnsConstraintWildcard(NULL, 5,
|
||||
"com", 3, 1), 0);
|
||||
ExpectIntEQ(wolfssl_local_MatchDnsConstraintWildcard("*.com", 5,
|
||||
NULL, 3, 1), 0);
|
||||
ExpectIntEQ(wolfssl_local_MatchDnsConstraintWildcard("*.com", 0,
|
||||
"com", 3, 1), 0);
|
||||
ExpectIntEQ(wolfssl_local_MatchDnsConstraintWildcard("*.com", 5,
|
||||
"com", 0, 1), 0);
|
||||
/* Name beginning with a dot is invalid. */
|
||||
ExpectIntEQ(dnsWildPermitted(".x.com", "com"), 0);
|
||||
ExpectIntEQ(dnsWildExcluded(".x.com", "com"), 0);
|
||||
/* Base that is only dots collapses to nothing. */
|
||||
ExpectIntEQ(dnsWildExcluded("*.example.com", "."), 0);
|
||||
ExpectIntEQ(dnsWildExcluded("*.example.com", ".."), 0);
|
||||
/* SAN has an empty interior label ("*..com"), but only the right-most
|
||||
* "com" label overlaps the base "com" -- the empty label sits outside the
|
||||
* compared suffix, and '*' can expand to any label, so the matcher
|
||||
* conservatively reports intersection. */
|
||||
ExpectIntEQ(dnsWildExcluded("*..com", "com"), 1);
|
||||
|
||||
#endif /* !NO_CERTS && !NO_ASN && !IGNORE_NAME_CONSTRAINTS */
|
||||
|
||||
return EXPECT_RESULT();
|
||||
}
|
||||
|
||||
/*
|
||||
* Tests URI name-constraint matching (RFC 5280 4.2.1.10): the constraint
|
||||
* applies to the host portion of the URI. A constraint that does NOT begin
|
||||
* with a dot is an exact host match; one that begins with a dot matches any
|
||||
* host with one or more additional leading labels (the bare host is excluded).
|
||||
*/
|
||||
int test_wolfssl_local_MatchUriNameConstraint(void)
|
||||
{
|
||||
EXPECT_DECLS;
|
||||
|
||||
#if !defined(NO_CERTS) && !defined(NO_ASN) && !defined(IGNORE_NAME_CONSTRAINTS)
|
||||
/*
|
||||
* Exact host match (no leading dot in the constraint).
|
||||
*/
|
||||
ExpectIntEQ(uriNC("https://host.com/path", "host.com"), 1);
|
||||
ExpectIntEQ(uriNC("https://host.com", "host.com"), 1);
|
||||
ExpectIntEQ(uriNC("https://host.com:8443/x", "host.com"), 1);
|
||||
ExpectIntEQ(uriNC("ftp://user@host.com/x", "host.com"), 1);
|
||||
ExpectIntEQ(uriNC("https://HOST.COM", "host.com"), 1);
|
||||
ExpectIntEQ(uriNC("https://host.com?q=1", "host.com"), 1);
|
||||
ExpectIntEQ(uriNC("https://host.com#frag", "host.com"), 1);
|
||||
|
||||
/* The bug this fix closes: an exact-host constraint must NOT subtree-match
|
||||
* a sub-host. */
|
||||
ExpectIntEQ(uriNC("https://www.host.com/", "host.com"), 0);
|
||||
ExpectIntEQ(uriNC("https://a.b.host.com", "host.com"), 0);
|
||||
/* Suffix that does not respect a label boundary. */
|
||||
ExpectIntEQ(uriNC("https://xhost.com", "host.com"), 0);
|
||||
/* host.com is a prefix of the URI host but not the whole host. */
|
||||
ExpectIntEQ(uriNC("https://host.com.evil.com", "host.com"), 0);
|
||||
ExpectIntEQ(uriNC("https://other.com", "host.com"), 0);
|
||||
|
||||
/*
|
||||
* Leading-dot constraint: proper subtree of hosts (apex excluded).
|
||||
*/
|
||||
ExpectIntEQ(uriNC("https://www.host.com/", ".host.com"), 1);
|
||||
ExpectIntEQ(uriNC("https://a.b.host.com", ".host.com"), 1);
|
||||
ExpectIntEQ(uriNC("https://www.host.com:443", ".host.com"), 1);
|
||||
/* The bare host is NOT in the leading-dot subtree. */
|
||||
ExpectIntEQ(uriNC("https://host.com", ".host.com"), 0);
|
||||
ExpectIntEQ(uriNC("https://evilhost.com", ".host.com"), 0);
|
||||
|
||||
/*
|
||||
* IPv6 literal host extraction ([..]) then exact match.
|
||||
*/
|
||||
ExpectIntEQ(uriNC("https://[2001:db8::1]:443/x", "2001:db8::1"), 1);
|
||||
ExpectIntEQ(uriNC("https://[2001:db8::1]", "2001:db8::2"), 0);
|
||||
|
||||
/*
|
||||
* Malformed / degenerate URIs and inputs (reject).
|
||||
*/
|
||||
ExpectIntEQ(uriNC("no-scheme-host.com", "host.com"), 0);
|
||||
ExpectIntEQ(uriNC("https://", "host.com"), 0);
|
||||
/* double literal to abide source-check thinking it's a c++ comment */
|
||||
ExpectIntEQ(uriNC("https://" "/path", "host.com"), 0);
|
||||
ExpectIntEQ(wolfssl_local_MatchUriNameConstraint(NULL, 10,
|
||||
"host.com", 8), 0);
|
||||
ExpectIntEQ(wolfssl_local_MatchUriNameConstraint("https://host.com", 16,
|
||||
NULL, 8), 0);
|
||||
ExpectIntEQ(wolfssl_local_MatchUriNameConstraint("https://host.com", 0,
|
||||
"host.com", 8), 0);
|
||||
ExpectIntEQ(wolfssl_local_MatchUriNameConstraint("https://host.com", 16,
|
||||
"host.com", 0), 0);
|
||||
|
||||
#endif /* !NO_CERTS && !NO_ASN && !IGNORE_NAME_CONSTRAINTS */
|
||||
|
||||
return EXPECT_RESULT();
|
||||
}
|
||||
|
||||
/*
|
||||
* Testing wc_DecodeRsaPssParams with known DER byte arrays.
|
||||
* Exercises both WOLFSSL_ASN_TEMPLATE and non-template paths.
|
||||
|
||||
@@ -30,6 +30,8 @@ int test_DecodeAsymKey_negative(void);
|
||||
int test_GetSetShortInt(void);
|
||||
int test_wc_IndexSequenceOf(void);
|
||||
int test_wolfssl_local_MatchBaseName(void);
|
||||
int test_wolfssl_local_MatchDnsConstraintWildcard(void);
|
||||
int test_wolfssl_local_MatchUriNameConstraint(void);
|
||||
int test_wc_DecodeRsaPssParams(void);
|
||||
int test_SerialNumber0_RootCA(void);
|
||||
int test_DecodeAltNames_length_underflow(void);
|
||||
@@ -46,6 +48,8 @@ int test_ToTraditional_ex_mldsa_bad_params(void);
|
||||
TEST_DECL_GROUP("asn", test_GetSetShortInt), \
|
||||
TEST_DECL_GROUP("asn", test_wc_IndexSequenceOf), \
|
||||
TEST_DECL_GROUP("asn", test_wolfssl_local_MatchBaseName), \
|
||||
TEST_DECL_GROUP("asn", test_wolfssl_local_MatchDnsConstraintWildcard), \
|
||||
TEST_DECL_GROUP("asn", test_wolfssl_local_MatchUriNameConstraint), \
|
||||
TEST_DECL_GROUP("asn", test_wc_DecodeRsaPssParams), \
|
||||
TEST_DECL_GROUP("asn", test_SerialNumber0_RootCA), \
|
||||
TEST_DECL_GROUP("asn", test_DecodeAltNames_length_underflow), \
|
||||
|
||||
+224
-8
@@ -17898,12 +17898,31 @@ int wolfssl_local_MatchBaseName(int type, const char* name, int nameSz,
|
||||
const char* base, int baseSz)
|
||||
{
|
||||
if (base == NULL || baseSz <= 0 || name == NULL || nameSz <= 0 ||
|
||||
name[0] == '.' || nameSz < baseSz ||
|
||||
name[0] == '.' ||
|
||||
(type != ASN_RFC822_TYPE && type != ASN_DNS_TYPE &&
|
||||
type != ASN_DIR_TYPE)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (type == ASN_DNS_TYPE) {
|
||||
/* MatchDomainName treats one trailing dot as the absolute-FQDN marker.
|
||||
* Apply the same normalization before enforcing DNS name constraints.
|
||||
*/
|
||||
if (name[nameSz - 1] == '.') {
|
||||
nameSz--;
|
||||
}
|
||||
if (base[baseSz - 1] == '.') {
|
||||
baseSz--;
|
||||
}
|
||||
if (nameSz <= 0 || baseSz <= 0) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (nameSz < baseSz) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (type == ASN_DIR_TYPE)
|
||||
return XMEMCMP(name, base, (size_t)baseSz) == 0;
|
||||
|
||||
@@ -17992,8 +18011,8 @@ int wolfssl_local_MatchBaseName(int type, const char* name, int nameSz,
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int MatchUriNameConstraint(const char* uri, int uriSz, const char* base,
|
||||
int baseSz)
|
||||
int wolfssl_local_MatchUriNameConstraint(const char* uri, int uriSz,
|
||||
const char* base, int baseSz)
|
||||
{
|
||||
const char* hostStart;
|
||||
const char* hostEnd;
|
||||
@@ -18001,7 +18020,10 @@ static int MatchUriNameConstraint(const char* uri, int uriSz, const char* base,
|
||||
const char* uriEnd;
|
||||
int hostSz;
|
||||
|
||||
if (uri == NULL || uriSz <= 0 || base == NULL || baseSz <= 0) {
|
||||
/* Need at least 3 bytes for the "://" scheme separator; rejecting short
|
||||
* inputs early also keeps the loop bound (uriEnd - 2) from forming a
|
||||
* pointer before `uri`. */
|
||||
if (uri == NULL || uriSz < 3 || base == NULL || baseSz <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -18057,8 +18079,168 @@ static int MatchUriNameConstraint(const char* uri, int uriSz, const char* base,
|
||||
return 0;
|
||||
}
|
||||
|
||||
return wolfssl_local_MatchBaseName(ASN_DNS_TYPE, hostStart, hostSz, base,
|
||||
baseSz);
|
||||
/* RFC 5280 4.2.1.10: for URIs the constraint applies to the host part.
|
||||
* A constraint that begins with a '.' matches any host with one or more
|
||||
* additional leading labels (the bare host is excluded) - this is the
|
||||
* DNS subtree behaviour. A constraint without a leading '.' specifies a
|
||||
* single host and must match it exactly. */
|
||||
if (base[0] == '.') {
|
||||
return wolfssl_local_MatchBaseName(ASN_DNS_TYPE, hostStart, hostSz,
|
||||
base, baseSz);
|
||||
}
|
||||
else {
|
||||
int i;
|
||||
if (hostSz != baseSz) {
|
||||
return 0;
|
||||
}
|
||||
for (i = 0; i < baseSz; i++) {
|
||||
if (XTOLOWER((unsigned char)hostStart[i]) !=
|
||||
XTOLOWER((unsigned char)base[i])) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Locate the right-most label of `s` that ends (exclusive) at index `end`.
|
||||
* Sets *outStart to its starting index (the index of its first byte). If
|
||||
* outHasWild is non-NULL, sets *outHasWild to 1 iff the label contains '*'.
|
||||
* The '.' immediately before *outStart (if any) is the label separator and is
|
||||
* not part of either the current or the preceding label. */
|
||||
static void PrevDnsLabel(const char* s, int end, int* outStart,
|
||||
int* outHasWild)
|
||||
{
|
||||
int start = end;
|
||||
int hasWild = 0;
|
||||
while (start > 0 && s[start - 1] != '.') {
|
||||
if (s[start - 1] == '*') {
|
||||
hasWild = 1;
|
||||
}
|
||||
start--;
|
||||
}
|
||||
*outStart = start;
|
||||
if (outHasWild != NULL) {
|
||||
*outHasWild = hasWild;
|
||||
}
|
||||
}
|
||||
|
||||
/* Match a wildcard DNS SAN against a DNS name-constraint subtree.
|
||||
*
|
||||
* A wildcard SAN denotes the set of names its '*'(s) can expand to. Because a
|
||||
* '*' never crosses a label boundary (see MatchDomainName), every expansion
|
||||
* has the same number of labels and only the content of '*'-bearing labels
|
||||
* varies. Matching is therefore done label-by-label from the right against the
|
||||
* constraint base.
|
||||
*
|
||||
* permitted != 0: containment. Accept only if EVERY expansion stays inside the
|
||||
* subtree, i.e. each of the right-most base-length labels of the name is
|
||||
* wildcard-free and equal to the corresponding base label. Extra labels to
|
||||
* the left may be anything (adding labels on the left stays in-subtree).
|
||||
*
|
||||
* permitted == 0: intersection (for excluded subtrees). Reject if SOME
|
||||
* expansion falls inside the subtree. A label containing a '*' is
|
||||
* conservatively treated as able to match any single base label; a literal
|
||||
* label must equal the base label.
|
||||
*
|
||||
* A leading '.' on the base denotes a proper subtree (the apex is excluded),
|
||||
* which requires at least one extra label on the left of the name.
|
||||
*
|
||||
* Returns 1 on match (contained / intersecting), 0 otherwise.
|
||||
*/
|
||||
int wolfssl_local_MatchDnsConstraintWildcard(const char* name, int nameSz,
|
||||
const char* base, int baseSz, int permitted)
|
||||
{
|
||||
int baseLead;
|
||||
int ni, bi;
|
||||
|
||||
if (name == NULL || base == NULL || nameSz <= 0 || baseSz <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* MatchDomainName treats one trailing dot as the absolute-FQDN marker.
|
||||
* Apply the same normalization before label-wise constraint matching.
|
||||
*/
|
||||
if (name[nameSz - 1] == '.') {
|
||||
nameSz--;
|
||||
}
|
||||
if (base[baseSz - 1] == '.') {
|
||||
baseSz--;
|
||||
}
|
||||
if (nameSz <= 0 || baseSz <= 0 || name[0] == '.') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
baseLead = (base[0] == '.');
|
||||
if (baseLead) {
|
||||
base++;
|
||||
baseSz--;
|
||||
}
|
||||
/* A base of only dots (".", "..") has no labels to match. */
|
||||
if (baseSz <= 0 || base[0] == '.') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ni = nameSz; /* exclusive end of the unconsumed name */
|
||||
bi = baseSz; /* exclusive end of the unconsumed base */
|
||||
|
||||
/* Compare each base label (right to left) with the aligned name label. */
|
||||
while (bi > 0) {
|
||||
int nStart, bStart, nLen, bLen, k;
|
||||
int hasWild = 0;
|
||||
|
||||
/* Base labels remain but the name has none left -> name is shorter in
|
||||
* labels and cannot contain the base. */
|
||||
if (ni <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
PrevDnsLabel(name, ni, &nStart, &hasWild);
|
||||
PrevDnsLabel(base, bi, &bStart, NULL);
|
||||
nLen = ni - nStart;
|
||||
bLen = bi - bStart;
|
||||
|
||||
/* Empty label (e.g. "a..b" or an extra trailing dot) is invalid. */
|
||||
if (nLen == 0 || bLen == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (hasWild) {
|
||||
/* permitted: a wildcard label cannot prove containment.
|
||||
* excluded: a wildcard label is conservatively treated as
|
||||
* compatible, so nothing more to check. */
|
||||
if (permitted) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
else {
|
||||
/* Literal label: both modes require exact case-insensitive
|
||||
* equality with the base label. */
|
||||
if (nLen != bLen) {
|
||||
return 0;
|
||||
}
|
||||
for (k = 0; k < bLen; k++) {
|
||||
if (XTOLOWER((unsigned char)name[nStart + k]) !=
|
||||
XTOLOWER((unsigned char)base[bStart + k])) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Consume both labels and the '.' that precedes them (if any). */
|
||||
ni = nStart - 1;
|
||||
bi = bStart - 1;
|
||||
}
|
||||
|
||||
/* All base labels matched. ni >= 0 means name[ni] == '.' and at least one
|
||||
* extra label remains on the left; ni < 0 means the name had exactly the
|
||||
* base's label count (an apex match). A leading-dot base is a proper
|
||||
* subtree and requires at least one extra left label. */
|
||||
if (baseLead && ni < 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Check if IP address matches a name constraint.
|
||||
@@ -18111,6 +18293,18 @@ static int MatchOtherNameConstraint(DNS_entry* name, Base_entry* current)
|
||||
return XMEMCMP(name->name, current->name, (size_t)current->nameSz) == 0;
|
||||
}
|
||||
|
||||
/* Return 1 if the name contains a wildcard '*' character. */
|
||||
static int DnsNameHasWildcard(const char* name, int nameSz)
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < nameSz; i++) {
|
||||
if (name[i] == '*') {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Search through the list to find if the name is permitted.
|
||||
* name The DNS name to search for
|
||||
* dnsList The list to search through
|
||||
@@ -18138,7 +18332,7 @@ static int PermittedListOk(DNS_entry* name, Base_entry* dnsList, byte nameType)
|
||||
}
|
||||
}
|
||||
else if (nameType == ASN_URI_TYPE) {
|
||||
if (MatchUriNameConstraint(name->name, name->len,
|
||||
if (wolfssl_local_MatchUriNameConstraint(name->name, name->len,
|
||||
current->name, current->nameSz)) {
|
||||
match = 1;
|
||||
break;
|
||||
@@ -18161,6 +18355,17 @@ static int PermittedListOk(DNS_entry* name, Base_entry* dnsList, byte nameType)
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (nameType == ASN_DNS_TYPE &&
|
||||
DnsNameHasWildcard(name->name, name->len)) {
|
||||
/* Wildcard DNS SAN: a '*' can expand to a longer label, so the
|
||||
* byte-length guard used for literal names below is invalid.
|
||||
* Permit only if every expansion stays inside the subtree. */
|
||||
if (wolfssl_local_MatchDnsConstraintWildcard(name->name,
|
||||
name->len, current->name, current->nameSz, 1)) {
|
||||
match = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (name->len >= current->nameSz &&
|
||||
wolfssl_local_MatchBaseName(nameType, name->name, name->len,
|
||||
current->name, current->nameSz)) {
|
||||
@@ -18202,7 +18407,7 @@ static int IsInExcludedList(DNS_entry* name, Base_entry* dnsList, byte nameType)
|
||||
}
|
||||
}
|
||||
else if (nameType == ASN_URI_TYPE) {
|
||||
if (MatchUriNameConstraint(name->name, name->len,
|
||||
if (wolfssl_local_MatchUriNameConstraint(name->name, name->len,
|
||||
current->name, current->nameSz)) {
|
||||
ret = 1;
|
||||
break;
|
||||
@@ -18224,6 +18429,17 @@ static int IsInExcludedList(DNS_entry* name, Base_entry* dnsList, byte nameType)
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (nameType == ASN_DNS_TYPE &&
|
||||
DnsNameHasWildcard(name->name, name->len)) {
|
||||
/* Wildcard DNS SAN: a '*' can expand to a longer label, so the
|
||||
* byte-length guard used for literal names below is invalid.
|
||||
* Exclude if any expansion can fall inside the subtree. */
|
||||
if (wolfssl_local_MatchDnsConstraintWildcard(name->name,
|
||||
name->len, current->name, current->nameSz, 0)) {
|
||||
ret = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (name->len >= current->nameSz &&
|
||||
wolfssl_local_MatchBaseName(nameType, name->name, name->len,
|
||||
current->name, current->nameSz)) {
|
||||
|
||||
@@ -3213,6 +3213,13 @@ WOLFSSL_TEST_VIS int wolfssl_local_MatchBaseName(int type, const char* name,
|
||||
WOLFSSL_TEST_VIS int wolfssl_local_MatchIpSubnet(const byte* ip, int ipSz,
|
||||
const byte* constraint,
|
||||
int constraintSz);
|
||||
WOLFSSL_TEST_VIS int wolfssl_local_MatchUriNameConstraint(const char* uri,
|
||||
int uriSz, const char* base,
|
||||
int baseSz);
|
||||
WOLFSSL_TEST_VIS int wolfssl_local_MatchDnsConstraintWildcard(
|
||||
const char* name, int nameSz,
|
||||
const char* base, int baseSz,
|
||||
int permitted);
|
||||
#endif
|
||||
|
||||
#if ((defined(HAVE_ED25519) && defined(HAVE_ED25519_KEY_IMPORT)) \
|
||||
|
||||
Reference in New Issue
Block a user