Skip to content

Commit 9f48aef

Browse files
authored
Merge pull request #10638 from rizlik/nc_uri_trailing_dot
NameConstraints fixes
2 parents d733f20 + 44a22af commit 9f48aef

6 files changed

Lines changed: 369 additions & 122 deletions

File tree

src/x509.c

Lines changed: 39 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -5670,95 +5670,12 @@ static int MatchIpName(const char* name, int nameSz, WOLFSSL_GENERAL_NAME* gn)
56705670
constraintData, constraintLen);
56715671
}
56725672

5673-
/* Extract host from URI for name constraint matching.
5674-
* URI format: scheme://[userinfo@]host[:port][/path][?query][#fragment]
5675-
* IPv6 literals are enclosed in brackets: scheme://[ipv6addr]:port/path
5676-
* Returns pointer to host start and sets hostLen, or NULL on failure. */
5677-
static const char* ExtractHostFromUri(const char* uri, int uriLen, int* hostLen)
5678-
{
5679-
const char* hostStart;
5680-
const char* hostEnd;
5681-
const char* p;
5682-
const char* uriEnd;
5683-
5684-
if (uri == NULL || uriLen <= 0 || hostLen == NULL) {
5685-
return NULL;
5686-
}
5687-
5688-
uriEnd = uri + uriLen;
5689-
5690-
/* Find "://" to skip scheme */
5691-
hostStart = NULL;
5692-
for (p = uri; p < uriEnd - 2; p++) {
5693-
if (p[0] == ':' && p[1] == '/' && p[2] == '/') {
5694-
hostStart = p + 3;
5695-
break;
5696-
}
5697-
}
5698-
if (hostStart == NULL || hostStart >= uriEnd) {
5699-
return NULL;
5700-
}
5701-
5702-
/* Skip userinfo if present (look for @ before any /, ?, #)
5703-
* userinfo can contain ':' (ex: user:pass@host), don't stop at ':'
5704-
* For IPv6, also don't stop at '[' in userinfo */
5705-
for (p = hostStart; p < uriEnd; p++) {
5706-
if (*p == '@') {
5707-
hostStart = p + 1;
5708-
break;
5709-
}
5710-
if (*p == '/' || *p == '?' || *p == '#') {
5711-
/* No userinfo found */
5712-
break;
5713-
}
5714-
/* If '[' before '@', found IPv6 literal, not userinfo */
5715-
if (*p == '[') {
5716-
break;
5717-
}
5718-
}
5719-
if (hostStart >= uriEnd) {
5720-
return NULL;
5721-
}
5722-
5723-
/* Check for IPv6 literal */
5724-
if (*hostStart == '[') {
5725-
/* Find closing bracket, skip opening one */
5726-
hostStart++;
5727-
hostEnd = hostStart;
5728-
while (hostEnd < uriEnd && *hostEnd != ']') {
5729-
hostEnd++;
5730-
}
5731-
if (hostEnd >= uriEnd) {
5732-
/* No closing bracket found, malformed */
5733-
return NULL;
5734-
}
5735-
/* hostEnd points to closing bracket, extract content between */
5736-
*hostLen = (int)(hostEnd - hostStart);
5737-
if (*hostLen <= 0) {
5738-
return NULL;
5739-
}
5740-
return hostStart;
5741-
}
5742-
5743-
/* Regular hostname, find end */
5744-
hostEnd = hostStart;
5745-
while (hostEnd < uriEnd && *hostEnd != ':' && *hostEnd != '/' &&
5746-
*hostEnd != '?' && *hostEnd != '#') {
5747-
hostEnd++;
5748-
}
5749-
5750-
*hostLen = (int)(hostEnd - hostStart);
5751-
if (*hostLen <= 0) {
5752-
return NULL;
5753-
}
5754-
5755-
return hostStart;
5756-
}
5757-
57585673
/* Helper to check if name string matches a single GENERAL_NAME constraint.
5674+
* permitted selects subtree semantics for wildcard DNS names: containment
5675+
* for permitted subtrees, intersection for excluded subtrees.
57595676
* Returns 1 if matches, 0 if not. */
57605677
static int MatchNameConstraint(int type, const char* name, int nameSz,
5761-
WOLFSSL_GENERAL_NAME* gn)
5678+
WOLFSSL_GENERAL_NAME* gn, int permitted)
57625679
{
57635680
const char* baseStr;
57645681
int baseLen;
@@ -5788,21 +5705,13 @@ static int MatchNameConstraint(int type, const char* name, int nameSz,
57885705
nameSz, baseStr, baseLen);
57895706
}
57905707
else if (type == WOLFSSL_GEN_URI) {
5791-
const char* host;
5792-
int hostLen;
5793-
5794-
/* For URI, extract host and match against DNS-style */
5795-
host = ExtractHostFromUri(name, nameSz, &hostLen);
5796-
if (host == NULL) {
5797-
return 0;
5798-
}
5799-
return wolfssl_local_MatchBaseName(ASN_DNS_TYPE, host, hostLen,
5708+
return wolfssl_local_MatchUriNameConstraint(name, nameSz,
58005709
baseStr, baseLen);
58015710
}
58025711
else {
58035712
/* WOLFSSL_GEN_DNS uses DNS-style matching */
5804-
return wolfssl_local_MatchBaseName(ASN_DNS_TYPE, name, nameSz,
5805-
baseStr, baseLen);
5713+
return wolfssl_local_MatchDnsNameConstraint(name, nameSz,
5714+
baseStr, baseLen, permitted);
58065715
}
58075716

58085717
default:
@@ -5811,6 +5720,29 @@ static int MatchNameConstraint(int type, const char* name, int nameSz,
58115720
}
58125721
}
58135722

5723+
static int NameConstraintsHasType(const WOLFSSL_STACK* sk, int type)
5724+
{
5725+
int i;
5726+
int num;
5727+
5728+
if (sk == NULL) {
5729+
return 0;
5730+
}
5731+
5732+
num = wolfSSL_sk_GENERAL_SUBTREE_num(sk);
5733+
for (i = 0; i < num; i++) {
5734+
WOLFSSL_GENERAL_SUBTREE* subtree;
5735+
5736+
subtree = wolfSSL_sk_GENERAL_SUBTREE_value(sk, i);
5737+
if (subtree != NULL && subtree->base != NULL &&
5738+
subtree->base->type == type) {
5739+
return 1;
5740+
}
5741+
}
5742+
5743+
return 0;
5744+
}
5745+
58145746
/*
58155747
* Check if a name string satisfies given name constraints.
58165748
*
@@ -5841,6 +5773,14 @@ int wolfSSL_NAME_CONSTRAINTS_check_name(WOLFSSL_NAME_CONSTRAINTS* nc,
58415773
return 0;
58425774
}
58435775

5776+
if (type == WOLFSSL_GEN_URI &&
5777+
(NameConstraintsHasType(nc->permittedSubtrees, type) ||
5778+
NameConstraintsHasType(nc->excludedSubtrees, type)) &&
5779+
!wolfssl_local_UriNameHasDnsHost(name, nameSz)) {
5780+
WOLFSSL_MSG("URI name constraint applied to URI without DNS host");
5781+
return 0;
5782+
}
5783+
58445784
/* Check permitted subtrees */
58455785
if (nc->permittedSubtrees != NULL) {
58465786
num = wolfSSL_sk_GENERAL_SUBTREE_num(nc->permittedSubtrees);
@@ -5857,7 +5797,7 @@ int wolfSSL_NAME_CONSTRAINTS_check_name(WOLFSSL_NAME_CONSTRAINTS* nc,
58575797
}
58585798
hasPermittedType = 1;
58595799

5860-
if (MatchNameConstraint(type, name, nameSz, gn)) {
5800+
if (MatchNameConstraint(type, name, nameSz, gn, 1)) {
58615801
matchedPermitted = 1;
58625802
break;
58635803
}
@@ -5884,7 +5824,7 @@ int wolfSSL_NAME_CONSTRAINTS_check_name(WOLFSSL_NAME_CONSTRAINTS* nc,
58845824
continue;
58855825
}
58865826

5887-
if (MatchNameConstraint(type, name, nameSz, gn)) {
5827+
if (MatchNameConstraint(type, name, nameSz, gn, 0)) {
58885828
WOLFSSL_MSG("Name in excluded subtrees");
58895829
return 0;
58905830
}

tests/api.c

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23304,6 +23304,46 @@ static int test_NameConstraints_DnsUriWildcard(void)
2330423304
sanSz = build_simple_san(san, sizeof(san), URI, "https://www.host.com/");
2330523305
ExpectIntGT((int)sanSz, 0);
2330623306
ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz), 0);
23307+
23308+
/* (11) RFC 5280 requires a DNS host when URI constraints are applied.
23309+
* Fail closed even for excluded-only constraints where a boolean
23310+
* non-match would otherwise pass. */
23311+
ncSz = build_simple_nameConstraints(nc, sizeof(nc), 1, URI,
23312+
"blocked.com");
23313+
sanSz = build_simple_san(san, sizeof(san), URI, "https://12.31.2.3/");
23314+
ExpectIntGT((int)ncSz, 0);
23315+
ExpectIntGT((int)sanSz, 0);
23316+
ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz),
23317+
WC_NO_ERR_TRACE(ASN_NAME_INVALID_E));
23318+
23319+
sanSz = build_simple_san(san, sizeof(san), URI, "https://[v1.addr.]/");
23320+
ExpectIntGT((int)sanSz, 0);
23321+
ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz),
23322+
WC_NO_ERR_TRACE(ASN_NAME_INVALID_E));
23323+
23324+
/* An IPv4address host with the absolute-FQDN trailing dot is still not
23325+
* a DNS host. */
23326+
sanSz = build_simple_san(san, sizeof(san), URI, "https://12.31.2.3./");
23327+
ExpectIntGT((int)sanSz, 0);
23328+
ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz),
23329+
WC_NO_ERR_TRACE(ASN_NAME_INVALID_E));
23330+
23331+
/* (12) One trailing dot on the constraint base is the absolute-FQDN
23332+
* marker: DNS:example.com. denotes the same subtree as
23333+
* DNS:example.com, so the permitted form accepts the bare SAN and
23334+
* the excluded form rejects it. */
23335+
ncSz = build_simple_nameConstraints(nc, sizeof(nc), 0, DNS,
23336+
"example.com.");
23337+
sanSz = build_simple_san(san, sizeof(san), DNS, "example.com");
23338+
ExpectIntGT((int)ncSz, 0);
23339+
ExpectIntGT((int)sanSz, 0);
23340+
ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz), 0);
23341+
23342+
ncSz = build_simple_nameConstraints(nc, sizeof(nc), 1, DNS,
23343+
"example.com.");
23344+
ExpectIntGT((int)ncSz, 0);
23345+
ExpectIntEQ(verify_with_otherName_chain(nc, ncSz, 1, san, sanSz),
23346+
WC_NO_ERR_TRACE(ASN_NAME_INVALID_E));
2330723347
#endif
2330823348
return EXPECT_RESULT();
2330923349
}

tests/api/test_asn.c

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1153,6 +1153,21 @@ int test_wolfssl_local_MatchUriNameConstraint(void)
11531153
ExpectIntEQ(uriNC("https://host.com.evil.com", "host.com"), 0);
11541154
ExpectIntEQ(uriNC("https://other.com", "host.com"), 0);
11551155

1156+
/* A single trailing dot is the absolute-FQDN marker: "host.com." and
1157+
* "host.com" denote the same host and must compare equal, matching the
1158+
* DNS name-constraint path. */
1159+
ExpectIntEQ(uriNC("https://host.com./", "host.com"), 1);
1160+
ExpectIntEQ(uriNC("https://host.com.:8443/x", "host.com"), 1);
1161+
ExpectIntEQ(uriNC("https://host.com", "host.com."), 1);
1162+
ExpectIntEQ(uriNC("https://host.com./", "host.com."), 1);
1163+
ExpectIntEQ(uriNC("https://v1.addr./", "v1.addr"), 1);
1164+
ExpectIntEQ(uriNC("https://v1.addr/", "v1.addr."), 1);
1165+
/* Only ONE trailing dot is the marker; an empty last label is not. */
1166+
ExpectIntEQ(uriNC("https://host.com../", "host.com"), 0);
1167+
ExpectIntEQ(uriNC("https://a.host.com../", ".host.com"), 0);
1168+
/* Empty interior labels are not valid DNS host labels. */
1169+
ExpectIntEQ(uriNC("https://a..host.com/", ".host.com"), 0);
1170+
11561171
/*
11571172
* Leading-dot constraint: proper subtree of hosts (apex excluded).
11581173
*/
@@ -1164,10 +1179,18 @@ int test_wolfssl_local_MatchUriNameConstraint(void)
11641179
ExpectIntEQ(uriNC("https://evilhost.com", ".host.com"), 0);
11651180

11661181
/*
1167-
* IPv6 literal host extraction ([..]) then exact match.
1182+
* RFC 5280 URI constraints require a DNS host. IP-literals / IPvFuture
1183+
* hosts in brackets and IPv4address hosts are not DNS reg-names.
11681184
*/
1169-
ExpectIntEQ(uriNC("https://[2001:db8::1]:443/x", "2001:db8::1"), 1);
1185+
ExpectIntEQ(uriNC("https://[2001:db8::1]:443/x", "2001:db8::1"), 0);
11701186
ExpectIntEQ(uriNC("https://[2001:db8::1]", "2001:db8::2"), 0);
1187+
ExpectIntEQ(uriNC("https://[v1.addr.]/", "v1.addr"), 0);
1188+
ExpectIntEQ(uriNC("https://[v1.addr.]/", "v1.addr."), 0);
1189+
ExpectIntEQ(uriNC("https://12.31.2.3/", "12.31.2.3"), 0);
1190+
/* An IPv4address host is still not a DNS reg-name when written with the
1191+
* absolute-FQDN trailing dot. */
1192+
ExpectIntEQ(uriNC("https://12.31.2.3./", "12.31.2.3"), 0);
1193+
ExpectIntEQ(uriNC("https://12.31.2.3./", "12.31.2.3."), 0);
11711194

11721195
/*
11731196
* Malformed / degenerate URIs and inputs (reject).

tests/api/test_ossl_x509_ext.c

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2039,8 +2039,8 @@ int test_wolfSSL_NAME_CONSTRAINTS_uri(void)
20392039
ExpectIntEQ(wolfSSL_NAME_CONSTRAINTS_check_name(nc, GEN_URI,
20402040
"https://user:pass@www.wolfssl.com/path", 38), 1);
20412041

2042-
/* IPv6 literal URIs, host extracted without brackets.
2043-
* These don't match .wolfssl.com constraint (different host type) */
2042+
/* URI constraints require a DNS reg-name host, so IP-literals do not
2043+
* match the .wolfssl.com constraint. */
20442044
ExpectIntEQ(wolfSSL_NAME_CONSTRAINTS_check_name(nc, GEN_URI,
20452045
"https://[::1]:8080/path", 23), 0);
20462046
ExpectIntEQ(wolfSSL_NAME_CONSTRAINTS_check_name(nc, GEN_URI,
@@ -2243,6 +2243,71 @@ int test_wolfSSL_NAME_CONSTRAINTS_check_name(void)
22432243
x509 = NULL;
22442244
nc = NULL;
22452245

2246+
/* Wildcard names against an excluded DNS subtree. Build the constraints
2247+
* programmatically: excluded;DNS:foo.example.com */
2248+
ExpectNotNull(nc = wolfSSL_NAME_CONSTRAINTS_new());
2249+
if (EXPECT_SUCCESS()) {
2250+
GENERAL_SUBTREE* subtree = NULL;
2251+
2252+
ExpectNotNull(nc->excludedSubtrees = wolfSSL_sk_new_null());
2253+
if (EXPECT_SUCCESS()) {
2254+
nc->excludedSubtrees->type = STACK_TYPE_GENERAL_SUBTREE;
2255+
}
2256+
ExpectNotNull(subtree = wolfSSL_GENERAL_SUBTREE_new());
2257+
if (EXPECT_SUCCESS()) {
2258+
ExpectNotNull(subtree->base = wolfSSL_GENERAL_NAME_new());
2259+
}
2260+
if (EXPECT_SUCCESS()) {
2261+
subtree->base->type = GEN_DNS;
2262+
ExpectIntEQ(wolfSSL_ASN1_STRING_set(subtree->base->d.ia5,
2263+
"foo.example.com", 15), WOLFSSL_SUCCESS);
2264+
}
2265+
if (EXPECT_SUCCESS()) {
2266+
ExpectIntGT(wolfSSL_sk_push(nc->excludedSubtrees, subtree), 0);
2267+
}
2268+
if (EXPECT_FAIL()) {
2269+
wolfSSL_GENERAL_SUBTREE_free(subtree);
2270+
}
2271+
}
2272+
2273+
if (EXPECT_SUCCESS()) {
2274+
/* Literal names inside the excluded subtree are rejected. */
2275+
ExpectIntEQ(wolfSSL_NAME_CONSTRAINTS_check_name(nc, GEN_DNS,
2276+
"foo.example.com", 15), 0);
2277+
ExpectIntEQ(wolfSSL_NAME_CONSTRAINTS_check_name(nc, GEN_DNS,
2278+
"a.foo.example.com", 17), 0);
2279+
/* Names outside the subtree pass; there are no permitted subtrees. */
2280+
ExpectIntEQ(wolfSSL_NAME_CONSTRAINTS_check_name(nc, GEN_DNS,
2281+
"bar.example.com", 15), 1);
2282+
ExpectIntEQ(wolfSSL_NAME_CONSTRAINTS_check_name(nc, GEN_DNS,
2283+
"*.other.com", 11), 1);
2284+
/* A wildcard's '*' can expand to "foo", so "*.example.com" covers
2285+
* the excluded "foo.example.com" and must be rejected. */
2286+
ExpectIntEQ(wolfSSL_NAME_CONSTRAINTS_check_name(nc, GEN_DNS,
2287+
"*.example.com", 13), 0);
2288+
}
2289+
2290+
if (EXPECT_SUCCESS()) {
2291+
/* One trailing dot on the base is the absolute-FQDN marker:
2292+
* excluded;DNS:foo.example.com. covers the same subtree, so the
2293+
* bare name is still rejected. */
2294+
GENERAL_SUBTREE* subtree = NULL;
2295+
2296+
ExpectNotNull(subtree =
2297+
wolfSSL_sk_GENERAL_SUBTREE_value(nc->excludedSubtrees, 0));
2298+
if (EXPECT_SUCCESS()) {
2299+
ExpectIntEQ(wolfSSL_ASN1_STRING_set(subtree->base->d.ia5,
2300+
"foo.example.com.", 16), WOLFSSL_SUCCESS);
2301+
}
2302+
ExpectIntEQ(wolfSSL_NAME_CONSTRAINTS_check_name(nc, GEN_DNS,
2303+
"foo.example.com", 15), 0);
2304+
ExpectIntEQ(wolfSSL_NAME_CONSTRAINTS_check_name(nc, GEN_DNS,
2305+
"bar.example.com", 15), 1);
2306+
}
2307+
2308+
NAME_CONSTRAINTS_free(nc);
2309+
nc = NULL;
2310+
22462311
/* Test IP address constraint checking with cert-ext-ncip.pem
22472312
* This cert has permitted IP 192.168.1.0/255.255.255.0 */
22482313
if ((f = XFOPEN("./certs/test/cert-ext-ncip.pem", "rb")) == XBADFILE) {

0 commit comments

Comments
 (0)