From 433d0a0e6254a0b56f1bc9d8acc8b0100b4f7f7d Mon Sep 17 00:00:00 2001 From: Marco Oliverio Date: Fri, 11 Sep 2026 18:34:56 +0200 Subject: [PATCH 1/2] asn: improve URI host extraction --- tests/api/test_asn.c | 46 +++++++++++++++ tests/unit-mcdc/test_asn_ext_whitebox.c | 76 ++++++++++++++----------- wolfcrypt/src/asn.c | 52 +++++++++++++---- 3 files changed, 130 insertions(+), 44 deletions(-) diff --git a/tests/api/test_asn.c b/tests/api/test_asn.c index d6f9924e1ee..b92782c0722 100644 --- a/tests/api/test_asn.c +++ b/tests/api/test_asn.c @@ -1909,6 +1909,52 @@ int test_wolfssl_local_MatchUriNameConstraint(void) ExpectIntEQ(uriNC("https://host.com.evil.com", "host.com"), 0); ExpectIntEQ(uriNC("https://other.com", "host.com"), 0); + /* Only the initial scheme delimiter can introduce an authority. URLs + * embedded in an authority-less URI do not supply its host. */ + ExpectIntEQ(uriNC("urn:example:opaque", "host.com"), 0); + ExpectIntEQ(uriNC("urn:example:https://host.com/x", + "host.com"), 0); + ExpectIntEQ(uriNC("foo:/path/https://host.com/x", + "host.com"), 0); + ExpectIntEQ(uriNC("urn:example?next=https://host.com/x", + "host.com"), 0); + ExpectIntEQ(uriNC("urn:example#https://host.com/x", + "host.com"), 0); + ExpectIntEQ(uriNC("sip:victim.example?next=https://host.com/x", + "host.com"), 0); + ExpectIntEQ(uriNC("urn:example?next=https://a.host.com/x", + ".host.com"), 0); + + /* With a real authority, later URLs must not affect host matching. */ + ExpectIntEQ(uriNC("https://other.com/https://host.com/x", + "host.com"), 0); + ExpectIntEQ(uriNC("https://other.com?next=https://host.com/x", + "host.com"), 0); + ExpectIntEQ(uriNC("https://other.com#https://host.com/x", + "host.com"), 0); + ExpectIntEQ(uriNC("https://host.com?next=https://other.com/x", + "host.com"), 1); + + /* Scheme syntax, not a whitelist: + * ALPHA *(ALPHA / DIGIT / "+" / "-" / "."). */ + ExpectIntEQ(uriNC("a://host.com", "host.com"), 1); + ExpectIntEQ(uriNC("Custom+v2-test.1://host.com", "host.com"), 1); + ExpectIntEQ(uriNC("://host.com", "host.com"), 0); + ExpectIntEQ(uriNC("1https://host.com", "host.com"), 0); + ExpectIntEQ(uriNC("ht/tps://host.com", "host.com"), 0); + ExpectIntEQ(uriNC("ht_tps://host.com", "host.com"), 0); + ExpectIntEQ(uriNC(" https://host.com", "host.com"), 0); + ExpectIntEQ(uriNC("https", "host.com"), 0); + ExpectIntEQ(uriNC("https:", "host.com"), 0); + ExpectIntEQ(uriNC("https:/", "host.com"), 0); + /* Explicit lengths must bound the scheme and separator scans. */ + ExpectIntEQ(wolfssl_local_MatchUriNameConstraint("abc://host.com", 3, + "host.com", 8), 0); + ExpectIntEQ(wolfssl_local_MatchUriNameConstraint("abc://host.com", 4, + "host.com", 8), 0); + ExpectIntEQ(wolfssl_local_MatchUriNameConstraint("abc://host.com", 5, + "host.com", 8), 0); + /* A single trailing dot is the absolute-FQDN marker: "host.com." and * "host.com" denote the same host and must compare equal, matching the * DNS name-constraint path. */ diff --git a/tests/unit-mcdc/test_asn_ext_whitebox.c b/tests/unit-mcdc/test_asn_ext_whitebox.c index c93822ea588..afb700c075d 100644 --- a/tests/unit-mcdc/test_asn_ext_whitebox.c +++ b/tests/unit-mcdc/test_asn_ext_whitebox.c @@ -40,7 +40,8 @@ * 1. wolfssl_local_MatchBaseName() ................ :18555,:18607,:18626 * 2. URI host classification (UriHostIsDecOctet/ * UriHostIsIpv4Address/UriRegNameHasNonEmptyLabels/ - * GetUriHost) ...................................... :18664-:18794 + * GetUriSchemeEnd/GetUriHost), including scheme syntax and the + * four-operand authority guard (older helpers: :18664-:18794) * 3. wolfssl_local_MatchDnsConstraintWildcard() ... :18944,:18954,:18978 * 4. wolfssl_local_MatchIpSubnet() ........................... :19038 * 5. MatchOtherNameConstraint() ............................... :19063 @@ -352,12 +353,13 @@ static void wb_match_dir_attr(void) * UriHostIsDecOctet(): :18664 (NULL/sSz<=0/sSz>3), :18667 (leading zero) * UriHostIsIpv4Address(): :18687 (NULL/hostSz<=0), :18699 (non-digit) * UriRegNameHasNonEmptyLabels(): :18711-:18712 (NULL/leading-dot/trailing-dot) - * GetUriHost(): :18736-:18737 (bad args), :18744 ("://" scan), + * GetUriSchemeEnd(): NULL, bounded scan, colon, ALPHA, initial letter, + * DIGIT and '+'/'-'/'.' decisions + * GetUriHost(): :18736-:18737 (bad args), scheme/authority guard + * (p == NULL || uriEnd - p < 3 || p[1] != '/' || p[2] != '/'), * :18772 (bracket scan), :18794 (trailing-dot re-check) - * Driven indirectly through wolfssl_local_MatchUriNameConstraint() (the - * only external entry point reaching these file-static helpers) since none of - * them are directly link-visible on their own; MatchUriNameConstraint IS - * WOLFSSL_LOCAL/global so it is callable directly here. + * Driven through wolfssl_local_MatchUriNameConstraint(), plus direct calls + * to file-static helpers for scheme decisions and otherwise masked operands. * ------------------------------------------------------------------------- */ #ifndef IGNORE_NAME_CONSTRAINTS static void wb_uri_host_helpers(void) @@ -410,15 +412,40 @@ static void wb_uri_host_helpers(void) == 0, "base==NULL (short-circuits before GetUriHost host/hostSz " "args, but exercises the same bad-args style entry)"); - WB_NOTE("GetUriHost(): \"://\" scheme scan [:18744]"); - /* No "://" anywhere in the (long enough) buffer -> scan runs to - * completion without a match, hostStart stays NULL. */ - WB_CHECK(wolfssl_local_MatchUriNameConstraint("not-a-uri-at-all", 16, - ".host.com", 9) == 0, "no \"://\" present"); - /* "://" present and matched mid-scan (true branch of the 3-byte - * lookahead comparison). */ - WB_CHECK(wolfssl_local_MatchUriNameConstraint("s://a.host.com/x", 16, - ".host.com", 9) == 1, "\"://\" found (baseline true)"); + WB_NOTE("GetUriSchemeEnd(): NULL, length, colon and scheme characters"); + WB_CHECK(GetUriSchemeEnd(NULL, 5) == NULL, "uri==NULL"); + WB_CHECK(GetUriSchemeEnd("a:", 0) == NULL, "empty segment"); + WB_CHECK(GetUriSchemeEnd("a:", 1) == NULL, "colon outside segment"); + WB_CHECK(GetUriSchemeEnd(":", 1) == NULL, "colon cannot start scheme"); + WB_CHECK(GetUriSchemeEnd("1:", 2) == NULL, "first byte must be ALPHA"); + WB_CHECK(GetUriSchemeEnd("A:", 2) != NULL, "uppercase ALPHA"); + WB_CHECK(GetUriSchemeEnd("a:", 2) != NULL, "lowercase ALPHA"); + WB_CHECK(GetUriSchemeEnd("a@:", 3) == NULL, "byte below 'A'"); + WB_CHECK(GetUriSchemeEnd("a[:", 3) == NULL, "byte above 'Z', below 'a'"); + WB_CHECK(GetUriSchemeEnd("a{:", 3) == NULL, "byte above 'z'"); + WB_CHECK(GetUriSchemeEnd("a/:", 3) == NULL, "byte below '0'"); + WB_CHECK(GetUriSchemeEnd("a0:", 3) != NULL, "DIGIT"); + WB_CHECK(GetUriSchemeEnd("a;:", 3) == NULL, "byte above '9'"); + WB_CHECK(GetUriSchemeEnd("a+:", 3) != NULL, "plus allowed after ALPHA"); + WB_CHECK(GetUriSchemeEnd("a-:", 3) != NULL, "minus allowed after ALPHA"); + WB_CHECK(GetUriSchemeEnd("a.:", 3) != NULL, "dot allowed after ALPHA"); + + WB_NOTE("GetUriHost(): scheme + four-operand authority guard"); + /* No colon in the bounded segment -> GetUriSchemeEnd() returns NULL. */ + WB_CHECK(wolfssl_local_MatchUriNameConstraint("not-a-uri-at-all", 15, + "host.com", 8) == 0, "no scheme colon present"); + /* Valid scheme followed by both slashes: all guard operands false. */ + WB_CHECK(wolfssl_local_MatchUriNameConstraint("abc://host.com", 14, + "host.com", 8) == 1, "scheme + authority (baseline)"); + WB_CHECK(wolfssl_local_MatchUriNameConstraint("abc://host.com", 4, + "host.com", 8) == 0, "scheme colon is the last byte"); + WB_CHECK(wolfssl_local_MatchUriNameConstraint("abc://host.com", 5, + "host.com", 8) == 0, "only one byte after scheme colon"); + /* Exact-host constraints ensure subtree rejection cannot mask a bug. */ + WB_CHECK(wolfssl_local_MatchUriNameConstraint("ab:cd://host.com/x", 17, + "host.com", 8) == 0, "scheme colon not followed by '/'"); + WB_CHECK(wolfssl_local_MatchUriNameConstraint("a:/b://host.com/x", 16, + "host.com", 8) == 0, "scheme colon followed by only one '/'"); WB_NOTE("GetUriHost(): IP-literal '[' bracket scan [:18772]"); /* '[' opens an IP-literal host; scan for ']' runs to completion @@ -463,25 +490,6 @@ static void wb_uri_host_helpers(void) WB_CHECK(UriHostIsIpv4Address("1.2.3.4", 7) == 1, ":18753 both operands false (valid dotted quad)"); - /* :18798 -- the three-byte "://" lookahead. Every URI used above has - * its ':' immediately followed by "//", so the 2nd and 3rd operands are - * pinned true. These two URIs each contain an earlier ':' that fails - * the lookahead at a different operand before the real "://" is found. */ - { - /* Compared against the plain "s://host.com/x" form rather than a - * hard-coded 1: what this row has to show is that an earlier ':' - * which fails the lookahead does not change the outcome, and the - * base-matching semantics themselves are asserted elsewhere. */ - int plain = wolfssl_local_MatchUriNameConstraint("s://host.com/x", 14, - ".host.com", 9); - WB_CHECK(wolfssl_local_MatchUriNameConstraint("ab:cd://host.com/x", 18, - ".host.com", 9) == plain, - ":18798 2nd operand false (':' not followed by '/')"); - WB_CHECK(wolfssl_local_MatchUriNameConstraint("a:/b://host.com/x", 17, - ".host.com", 9) == plain, - ":18798 3rd operand false (\":/\" not followed by '/')"); - } - /* :18848 -- after stripping one trailing dot the host is empty. The * "http://../x" row above lands on the 2nd operand (still ends in '.'); * a host of exactly "." lands on the 1st. */ diff --git a/wolfcrypt/src/asn.c b/wolfcrypt/src/asn.c index 8e4b6c6c221..d1e2295e532 100644 --- a/wolfcrypt/src/asn.c +++ b/wolfcrypt/src/asn.c @@ -19400,6 +19400,39 @@ static int UriRegNameHasNonEmptyLabels(const char* host, int hostSz) return 1; } +/* Validate the RFC 3986 scheme and return its terminating colon, or NULL. + * scheme = ALPHA *(ALPHA / DIGIT / "+" / "-" / ".") */ +static const char* GetUriSchemeEnd(const char* uri, int uriSz) +{ + int i; + + if (uri == NULL) { + return NULL; + } + for (i = 0; i < uriSz; i++) { + char c = uri[i]; + + if (i > 0 && c == ':') { + return uri + i; + } + if (('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z')) { + continue; + } + /* The first character must be a letter. */ + if (i == 0) { + return NULL; + } + if ('0' <= c && c <= '9') { + continue; + } + if (c == '+' || c == '-' || c == '.') { + continue; + } + return NULL; + } + return NULL; +} + static int GetUriHost(const char* uri, int uriSz, const char** host, int* hostSz, UriHostType* hostType) { @@ -19408,23 +19441,22 @@ static int GetUriHost(const char* uri, int uriSz, const char** host, const char* p; const char* uriEnd; - /* 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`. */ + /* Early-out for short inputs; the uriEnd - p < 3 guard below bounds + * the authority separator reads after the scheme colon. */ if (uri == NULL || uriSz < 3 || host == NULL || hostSz == NULL || hostType == NULL) { return 0; } uriEnd = uri + uriSz; - hostStart = NULL; - for (p = uri; p < uriEnd - 2; p++) { - if (p[0] == ':' && p[1] == '/' && p[2] == '/') { - hostStart = p + 3; - break; - } + /* Only "//" immediately after the scheme colon introduces an authority; + * a later "://" in a path, query or fragment is not an authority. */ + p = GetUriSchemeEnd(uri, uriSz); + if (p == NULL || uriEnd - p < 3 || p[1] != '/' || p[2] != '/') { + return 0; } - if (hostStart == NULL || hostStart >= uriEnd) { + hostStart = p + 3; + if (hostStart >= uriEnd) { return 0; } From af1e6257661aa6bb73cbeac7847ab9a9c0aaaa23 Mon Sep 17 00:00:00 2001 From: Marco Oliverio Date: Fri, 11 Sep 2026 19:23:46 +0200 Subject: [PATCH 2/2] tests: small fixes --- tests/api/test_asn.c | 4 ++-- tests/unit-mcdc/test_asn_ext_whitebox.c | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/api/test_asn.c b/tests/api/test_asn.c index b92782c0722..76f6e593ce3 100644 --- a/tests/api/test_asn.c +++ b/tests/api/test_asn.c @@ -1909,8 +1909,8 @@ int test_wolfssl_local_MatchUriNameConstraint(void) ExpectIntEQ(uriNC("https://host.com.evil.com", "host.com"), 0); ExpectIntEQ(uriNC("https://other.com", "host.com"), 0); - /* Only the initial scheme delimiter can introduce an authority. URLs - * embedded in an authority-less URI do not supply its host. */ + /* Only the initial scheme delimiter can introduce an authority. A URI + * embedded in an authority-less URI does not supply its host. */ ExpectIntEQ(uriNC("urn:example:opaque", "host.com"), 0); ExpectIntEQ(uriNC("urn:example:https://host.com/x", "host.com"), 0); diff --git a/tests/unit-mcdc/test_asn_ext_whitebox.c b/tests/unit-mcdc/test_asn_ext_whitebox.c index afb700c075d..39f3413f01c 100644 --- a/tests/unit-mcdc/test_asn_ext_whitebox.c +++ b/tests/unit-mcdc/test_asn_ext_whitebox.c @@ -432,7 +432,7 @@ static void wb_uri_host_helpers(void) WB_NOTE("GetUriHost(): scheme + four-operand authority guard"); /* No colon in the bounded segment -> GetUriSchemeEnd() returns NULL. */ - WB_CHECK(wolfssl_local_MatchUriNameConstraint("not-a-uri-at-all", 15, + WB_CHECK(wolfssl_local_MatchUriNameConstraint("not-a-uri-at-all", 16, "host.com", 8) == 0, "no scheme colon present"); /* Valid scheme followed by both slashes: all guard operands false. */ WB_CHECK(wolfssl_local_MatchUriNameConstraint("abc://host.com", 14, @@ -442,9 +442,9 @@ static void wb_uri_host_helpers(void) WB_CHECK(wolfssl_local_MatchUriNameConstraint("abc://host.com", 5, "host.com", 8) == 0, "only one byte after scheme colon"); /* Exact-host constraints ensure subtree rejection cannot mask a bug. */ - WB_CHECK(wolfssl_local_MatchUriNameConstraint("ab:cd://host.com/x", 17, + WB_CHECK(wolfssl_local_MatchUriNameConstraint("ab:cd://host.com/x", 18, "host.com", 8) == 0, "scheme colon not followed by '/'"); - WB_CHECK(wolfssl_local_MatchUriNameConstraint("a:/b://host.com/x", 16, + WB_CHECK(wolfssl_local_MatchUriNameConstraint("a:/b://host.com/x", 17, "host.com", 8) == 0, "scheme colon followed by only one '/'"); WB_NOTE("GetUriHost(): IP-literal '[' bracket scan [:18772]");