Rewrite name constraints matching with CBS.

See also 8393de42498f8be75cf0353f5c9f906a43a748d2 from upstream and
CBS-2021-3712. But rather than do that, I've rewritten it with CBS, so
it's a bit clearer. The previous commit added tests.

Change-Id: Ie52e28f07b9bf805c8730eab7be5d40cb5d558b6
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/49008
Reviewed-by: Adam Langley <agl@google.com>
diff --git a/crypto/bytestring/bytestring_test.cc b/crypto/bytestring/bytestring_test.cc
index 484cd34..0877a2e 100644
--- a/crypto/bytestring/bytestring_test.cc
+++ b/crypto/bytestring/bytestring_test.cc
@@ -115,6 +115,28 @@
   EXPECT_FALSE(CBS_get_u24_length_prefixed(&data, &prefixed));
 }
 
+TEST(CBSTest, GetUntilFirst) {
+  static const uint8_t kData[] = {0, 1, 2, 3, 0, 1, 2, 3};
+  CBS data;
+  CBS_init(&data, kData, sizeof(kData));
+
+  CBS prefix;
+  EXPECT_FALSE(CBS_get_until_first(&data, &prefix, 4));
+  EXPECT_EQ(CBS_data(&data), kData);
+  EXPECT_EQ(CBS_len(&data), sizeof(kData));
+
+  ASSERT_TRUE(CBS_get_until_first(&data, &prefix, 0));
+  EXPECT_EQ(CBS_len(&prefix), 0u);
+  EXPECT_EQ(CBS_data(&data), kData);
+  EXPECT_EQ(CBS_len(&data), sizeof(kData));
+
+  ASSERT_TRUE(CBS_get_until_first(&data, &prefix, 2));
+  EXPECT_EQ(CBS_data(&prefix), kData);
+  EXPECT_EQ(CBS_len(&prefix), 2u);
+  EXPECT_EQ(CBS_data(&data), kData + 2);
+  EXPECT_EQ(CBS_len(&data), sizeof(kData) - 2);
+}
+
 TEST(CBSTest, GetASN1) {
   static const uint8_t kData1[] = {0x30, 2, 1, 2};
   static const uint8_t kData2[] = {0x30, 3, 1, 2};
diff --git a/crypto/bytestring/cbs.c b/crypto/bytestring/cbs.c
index 5590ec88..803c97a 100644
--- a/crypto/bytestring/cbs.c
+++ b/crypto/bytestring/cbs.c
@@ -216,6 +216,14 @@
   return cbs_get_length_prefixed(cbs, out, 3);
 }
 
+int CBS_get_until_first(CBS *cbs, CBS *out, uint8_t c) {
+  const uint8_t *split = OPENSSL_memchr(CBS_data(cbs), c, CBS_len(cbs));
+  if (split == NULL) {
+    return 0;
+  }
+  return CBS_get_bytes(cbs, out, split - CBS_data(cbs));
+}
+
 // parse_base128_integer reads a big-endian base-128 integer from |cbs| and sets
 // |*out| to the result. This is the encoding used in DER for both high tag
 // number form and OID components.
diff --git a/crypto/x509/x509_test.cc b/crypto/x509/x509_test.cc
index d897ff5..728bf7a 100644
--- a/crypto/x509/x509_test.cc
+++ b/crypto/x509/x509_test.cc
@@ -1557,6 +1557,9 @@
        X509_V_ERR_PERMITTED_VIOLATION},
       {GEN_DNS, "foo.example.com", ".unrelated.much.longer.name.example",
        X509_V_ERR_PERMITTED_VIOLATION},
+      // NUL bytes, if not rejected, should not confuse the matching logic.
+      {GEN_DNS, std::string({'a', '\0', 'a'}), std::string({'a', '\0', 'b'}),
+       X509_V_ERR_PERMITTED_VIOLATION},
 
       // Names must be emails.
       {GEN_EMAIL, "not-an-email.example", "not-an-email.example",
diff --git a/crypto/x509v3/v3_ncons.c b/crypto/x509v3/v3_ncons.c
index 593a520..35c826e 100644
--- a/crypto/x509v3/v3_ncons.c
+++ b/crypto/x509v3/v3_ncons.c
@@ -389,25 +389,73 @@
     return X509_V_OK;
 }
 
+static int starts_with(const CBS *cbs, uint8_t c)
+{
+    return CBS_len(cbs) > 0 && CBS_data(cbs)[0] == c;
+}
+
+static int equal_case(const CBS *a, const CBS *b)
+{
+    if (CBS_len(a) != CBS_len(b)) {
+        return 0;
+    }
+    /* Note we cannot use |OPENSSL_strncasecmp| because that would stop
+     * iterating at NUL. */
+    const uint8_t *a_data = CBS_data(a), *b_data = CBS_data(b);
+    for (size_t i = 0; i < CBS_len(a); i++) {
+        if (OPENSSL_tolower(a_data[i]) != OPENSSL_tolower(b_data[i])) {
+            return 0;
+        }
+    }
+    return 1;
+}
+
+static int has_suffix_case(const CBS *a, const CBS *b)
+{
+    if (CBS_len(a) < CBS_len(b)) {
+        return 0;
+    }
+    CBS copy = *a;
+    CBS_skip(&copy, CBS_len(a) - CBS_len(b));
+    return equal_case(&copy, b);
+}
+
 static int nc_dns(ASN1_IA5STRING *dns, ASN1_IA5STRING *base)
 {
-    char *baseptr = (char *)base->data;
-    char *dnsptr = (char *)dns->data;
+    CBS dns_cbs, base_cbs;
+    CBS_init(&dns_cbs, dns->data, dns->length);
+    CBS_init(&base_cbs, base->data, base->length);
+
     /* Empty matches everything */
-    if (!*baseptr)
+    if (CBS_len(&base_cbs) == 0) {
         return X509_V_OK;
+    }
+
+    /* If |base_cbs| begins with a '.', do a simple suffix comparison. This is
+     * not part of RFC5280, but is part of OpenSSL's original behavior. */
+    if (starts_with(&base_cbs, '.')) {
+        if (has_suffix_case(&dns_cbs, &base_cbs)) {
+            return X509_V_OK;
+        }
+        return X509_V_ERR_PERMITTED_VIOLATION;
+    }
+
     /*
      * Otherwise can add zero or more components on the left so compare RHS
      * and if dns is longer and expect '.' as preceding character.
      */
-    if (dns->length > base->length) {
-        dnsptr += dns->length - base->length;
-        if (*baseptr != '.' && dnsptr[-1] != '.')
+    if (CBS_len(&dns_cbs) > CBS_len(&base_cbs)) {
+        uint8_t dot;
+        if (!CBS_skip(&dns_cbs, CBS_len(&dns_cbs) - CBS_len(&base_cbs) - 1) ||
+            !CBS_get_u8(&dns_cbs, &dot) ||
+            dot != '.') {
             return X509_V_ERR_PERMITTED_VIOLATION;
+        }
     }
 
-    if (OPENSSL_strcasecmp(baseptr, dnsptr))
+    if (!equal_case(&dns_cbs, &base_cbs)) {
         return X509_V_ERR_PERMITTED_VIOLATION;
+    }
 
     return X509_V_OK;
 
@@ -415,86 +463,94 @@
 
 static int nc_email(ASN1_IA5STRING *eml, ASN1_IA5STRING *base)
 {
-    const char *baseptr = (char *)base->data;
-    const char *emlptr = (char *)eml->data;
+    CBS eml_cbs, base_cbs;
+    CBS_init(&eml_cbs, eml->data, eml->length);
+    CBS_init(&base_cbs, base->data, base->length);
 
-    const char *baseat = strchr(baseptr, '@');
-    const char *emlat = strchr(emlptr, '@');
-    if (!emlat)
+    /* TODO(davidben): In OpenSSL 1.1.1, this switched from the first '@' to the
+     * last one. Match them here, or perhaps do an actual parse. Looks like
+     * multiple '@'s may be allowed in quoted strings. */
+    CBS eml_local, base_local;
+    if (!CBS_get_until_first(&eml_cbs, &eml_local, '@')) {
         return X509_V_ERR_UNSUPPORTED_NAME_SYNTAX;
+    }
+    int base_has_at = CBS_get_until_first(&base_cbs, &base_local, '@');
+
     /* Special case: inital '.' is RHS match */
-    if (!baseat && (*baseptr == '.')) {
-        if (eml->length > base->length) {
-            emlptr += eml->length - base->length;
-            if (!OPENSSL_strcasecmp(baseptr, emlptr))
-                return X509_V_OK;
+    if (!base_has_at && starts_with(&base_cbs, '.')) {
+        if (has_suffix_case(&eml_cbs, &base_cbs)) {
+            return X509_V_OK;
         }
         return X509_V_ERR_PERMITTED_VIOLATION;
     }
 
     /* If we have anything before '@' match local part */
-
-    if (baseat) {
-        if (baseat != baseptr) {
-            if ((baseat - baseptr) != (emlat - emlptr))
-                return X509_V_ERR_PERMITTED_VIOLATION;
+    if (base_has_at) {
+        /* TODO(davidben): This interprets a constraint of "@example.com" as
+         * "example.com", which is not part of RFC5280. */
+        if (CBS_len(&base_local) > 0) {
             /* Case sensitive match of local part */
-            if (strncmp(baseptr, emlptr, emlat - emlptr))
+            if (!CBS_mem_equal(&base_local, CBS_data(&eml_local),
+                               CBS_len(&eml_local))) {
                 return X509_V_ERR_PERMITTED_VIOLATION;
+            }
         }
         /* Position base after '@' */
-        baseptr = baseat + 1;
+        assert(starts_with(&base_cbs, '@'));
+        CBS_skip(&base_cbs, 1);
     }
-    emlptr = emlat + 1;
+
     /* Just have hostname left to match: case insensitive */
-    if (OPENSSL_strcasecmp(baseptr, emlptr))
+    assert(starts_with(&eml_cbs, '@'));
+    CBS_skip(&eml_cbs, 1);
+    if (!equal_case(&base_cbs, &eml_cbs)) {
         return X509_V_ERR_PERMITTED_VIOLATION;
+    }
 
     return X509_V_OK;
-
 }
 
 static int nc_uri(ASN1_IA5STRING *uri, ASN1_IA5STRING *base)
 {
-    const char *baseptr = (char *)base->data;
-    const char *hostptr = (char *)uri->data;
-    const char *p = strchr(hostptr, ':');
-    int hostlen;
+    CBS uri_cbs, base_cbs;
+    CBS_init(&uri_cbs, uri->data, uri->length);
+    CBS_init(&base_cbs, base->data, base->length);
+
     /* Check for foo:// and skip past it */
-    if (!p || (p[1] != '/') || (p[2] != '/'))
+    CBS scheme;
+    uint8_t byte;
+    if (!CBS_get_until_first(&uri_cbs, &scheme, ':') ||
+        !CBS_skip(&uri_cbs, 1) ||  // Skip the colon
+        !CBS_get_u8(&uri_cbs, &byte) || byte != '/' ||
+        !CBS_get_u8(&uri_cbs, &byte) || byte != '/') {
         return X509_V_ERR_UNSUPPORTED_NAME_SYNTAX;
-    hostptr = p + 3;
+    }
 
-    /* Determine length of hostname part of URI */
+    /* Look for a port indicator as end of hostname first. Otherwise look for
+     * trailing slash, or the end of the string.
+     * TODO(davidben): This is not a correct URI parser and mishandles IPv6
+     * literals. */
+    CBS host;
+    if (!CBS_get_until_first(&uri_cbs, &host, ':') &&
+        !CBS_get_until_first(&uri_cbs, &host, '/')) {
+        host = uri_cbs;
+    }
 
-    /* Look for a port indicator as end of hostname first */
-
-    p = strchr(hostptr, ':');
-    /* Otherwise look for trailing slash */
-    if (!p)
-        p = strchr(hostptr, '/');
-
-    if (!p)
-        hostlen = strlen(hostptr);
-    else
-        hostlen = p - hostptr;
-
-    if (hostlen == 0)
+    if (CBS_len(&host) == 0) {
         return X509_V_ERR_UNSUPPORTED_NAME_SYNTAX;
+    }
 
     /* Special case: inital '.' is RHS match */
-    if (*baseptr == '.') {
-        if (hostlen > base->length) {
-            p = hostptr + hostlen - base->length;
-            if (!OPENSSL_strncasecmp(p, baseptr, base->length))
-                return X509_V_OK;
+    if (starts_with(&base_cbs, '.')) {
+        if (has_suffix_case(&host, &base_cbs)) {
+            return X509_V_OK;
         }
         return X509_V_ERR_PERMITTED_VIOLATION;
     }
 
-    if ((base->length != (int)hostlen)
-        || OPENSSL_strncasecmp(hostptr, baseptr, hostlen))
+    if (!equal_case(&base_cbs, &host)) {
         return X509_V_ERR_PERMITTED_VIOLATION;
+    }
 
     return X509_V_OK;
 
diff --git a/include/openssl/bytestring.h b/include/openssl/bytestring.h
index 180326d..5ef3742 100644
--- a/include/openssl/bytestring.h
+++ b/include/openssl/bytestring.h
@@ -154,6 +154,11 @@
 // returns one on success and zero on error.
 OPENSSL_EXPORT int CBS_get_u24_length_prefixed(CBS *cbs, CBS *out);
 
+// CBS_get_until_first finds the first instance of |c| in |cbs|. If found, it
+// sets |*out| to the text before the match, advances |cbs| over it, and returns
+// one. Otherwise, it returns zero and leaves |cbs| unmodified.
+OPENSSL_EXPORT int CBS_get_until_first(CBS *cbs, CBS *out, uint8_t c);
+
 
 // Parsing ASN.1
 //