Skip to content

Commit 2aa5224

Browse files
Fix CN fallback handling in name constraints checking (#3107)
### Description of changes: This change fixes two issues in name constraints enforcement: 1. `cn2dnsid` now recognizes wildcard CNs (leading `*.`) as DNS identifiers by skipping the prefix during DNS syntax validation. The full wildcard string is returned so `nc_dns` can perform suffix matching against permitted/excluded subtrees. 2. `nc_dns` now handles wildcard DNS names against excluded subtrees. An exclusion of `foo.example.com` correctly matches `*.example.com` by comparing parent domains when the DNS name starts with `*`. This is based on upstream commit: google/boringssl@5774eca. ### Call-outs: This change makes name constraints enforcement stricter. Certificates that were previously accepted (due to wildcard CNs being skipped) will now be checked against their CA's name constraints. Only certificates that were already in violation of their CA's constraints will be newly rejected. ### Testing: - Expanded `NameConstraints` in `x509_test.cc` to test both permitted and excluded subtrees. Also taken from upstream commit referenced above. - Added wildcard CN test cases to `CommonNameToDNS` in `x509_compat_test.cc`. - Added `NameConstraintsWildcardCN` in `x509_test.cc` to test the end-to-end CN fallback path with wildcard CNs against both permitted and excluded subtrees. By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license and the ISC license.
1 parent cb3e6f4 commit 2aa5224

File tree

3 files changed

+441
-102
lines changed

3 files changed

+441
-102
lines changed

crypto/x509/v3_ncons.c

Lines changed: 84 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,11 @@ static int do_i2r_name_constraints(const X509V3_EXT_METHOD *method,
2828
static int print_nc_ipadd(BIO *bp, const ASN1_OCTET_STRING *ip);
2929

3030
static int nc_match(GENERAL_NAME *gen, NAME_CONSTRAINTS *nc);
31-
static int nc_match_single(GENERAL_NAME *sub, GENERAL_NAME *gen);
31+
static int nc_match_single(GENERAL_NAME *sub, GENERAL_NAME *gen,
32+
int excluding);
3233
static int nc_dn(X509_NAME *sub, X509_NAME *nm);
33-
static int nc_dns(const ASN1_IA5STRING *sub, const ASN1_IA5STRING *dns);
34+
static int nc_dns(const ASN1_IA5STRING *sub, const ASN1_IA5STRING *dns,
35+
int excluding);
3436
static int nc_email(const ASN1_IA5STRING *sub, const ASN1_IA5STRING *eml);
3537
static int nc_uri(const ASN1_IA5STRING *uri, const ASN1_IA5STRING *base);
3638
static int nc_ip(const ASN1_OCTET_STRING *ip, const ASN1_OCTET_STRING *base);
@@ -295,24 +297,46 @@ int cn2dnsid(ASN1_STRING *cn, unsigned char **dnsid, size_t *idlen) {
295297
}
296298

297299
int isdnsname = 0;
300+
int has_non_dns_char = 0;
301+
302+
// Per RFC 6125 Section 6.4.3, a wildcard DNS-ID uses a leading "*." covering
303+
// the first label. Skip past it for validation, but return the full value so
304+
// |nc_dns| can match it against name constraints.
305+
int check_start = 0;
306+
if (utf8_length > 2 && utf8_value[0] == '*' && utf8_value[1] == '.') {
307+
check_start = 2;
308+
}
298309

299-
// XXX: Deviation from strict DNS name syntax, also check names with '_'
300-
// Check DNS name syntax, any '-' or '.' must be internal,
301-
// and on either side of each '.' we can't have a '-' or '.'.
310+
// Check DNS name syntax. Any '-' or '.' must be internal, and on either side
311+
// of each '.' we can't have a '-' or '.'. Names with '_' are also accepted
312+
// as a deviation from strict DNS syntax.
302313
//
303-
// If the name has just one label, we don't consider it a DNS name. This
314+
// If the name has just one label, we don't consider it a DNS name. This
304315
// means that "CN=sometld" cannot be precluded by DNS name constraints, but
305-
// that is not a problem.
306-
for (int i = 0; i < utf8_length; ++i) {
316+
// that is not a problem. Single-label CNs may contain non-ASCII characters
317+
// (e.g. "CN=Ünternehmen") and are silently skipped.
318+
//
319+
// Multi-label CNs that resemble DNS names must be ASCII-only. Per RFC 6125
320+
// Section 6.4.2, internationalized domain names should appear in A-label
321+
// (punycode) form. A multi-label CN containing non-ASCII bytes or control
322+
// characters is rejected with |X509_V_ERR_UNSUPPORTED_NAME_SYNTAX| to
323+
// prevent it from bypassing name constraints while still being accepted by
324+
// hostname verification.
325+
for (int i = check_start; i < utf8_length; ++i) {
307326
const unsigned char c = utf8_value[i];
308327

309328
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
310329
(c >= '0' && c <= '9') || c == '_') {
311330
continue;
312331
}
313332

333+
if (c >= 0x80 || c <= 0x20 || c == 0x7F) {
334+
has_non_dns_char = 1;
335+
continue;
336+
}
337+
314338
// Dot and hyphen cannot be first or last.
315-
if (i > 0 && i < utf8_length - 1) {
339+
if (i > check_start && i < utf8_length - 1) {
316340
if (c == '-') {
317341
continue;
318342
}
@@ -330,6 +354,13 @@ int cn2dnsid(ASN1_STRING *cn, unsigned char **dnsid, size_t *idlen) {
330354
break;
331355
}
332356

357+
if (isdnsname && has_non_dns_char) {
358+
// Multi-label CN with non-ASCII bytes or control characters. This
359+
// resembles a DNS name but contains characters not permitted in DNS.
360+
OPENSSL_free(utf8_value);
361+
return X509_V_ERR_UNSUPPORTED_NAME_SYNTAX;
362+
}
363+
333364
if (isdnsname) {
334365
*dnsid = utf8_value;
335366
*idlen = (size_t)utf8_length;
@@ -402,7 +433,7 @@ static int nc_match(GENERAL_NAME *gen, NAME_CONSTRAINTS *nc) {
402433
if (match == 0) {
403434
match = 1;
404435
}
405-
r = nc_match_single(gen, sub->base);
436+
r = nc_match_single(gen, sub->base, /*excluding=*/0);
406437
if (r == X509_V_OK) {
407438
match = 2;
408439
} else if (r != X509_V_ERR_PERMITTED_VIOLATION) {
@@ -425,7 +456,7 @@ static int nc_match(GENERAL_NAME *gen, NAME_CONSTRAINTS *nc) {
425456
return X509_V_ERR_SUBTREE_MINMAX;
426457
}
427458

428-
r = nc_match_single(gen, sub->base);
459+
r = nc_match_single(gen, sub->base, /*excluding=*/1);
429460
if (r == X509_V_OK) {
430461
return X509_V_ERR_EXCLUDED_VIOLATION;
431462
} else if (r != X509_V_ERR_PERMITTED_VIOLATION) {
@@ -436,13 +467,14 @@ static int nc_match(GENERAL_NAME *gen, NAME_CONSTRAINTS *nc) {
436467
return X509_V_OK;
437468
}
438469

439-
static int nc_match_single(GENERAL_NAME *gen, GENERAL_NAME *base) {
470+
static int nc_match_single(GENERAL_NAME *gen, GENERAL_NAME *base,
471+
int excluding) {
440472
switch (base->type) {
441473
case GEN_DIRNAME:
442474
return nc_dn(gen->d.directoryName, base->d.directoryName);
443475

444476
case GEN_DNS:
445-
return nc_dns(gen->d.dNSName, base->d.dNSName);
477+
return nc_dns(gen->d.dNSName, base->d.dNSName, excluding);
446478

447479
case GEN_EMAIL:
448480
return nc_email(gen->d.rfc822Name, base->d.rfc822Name);
@@ -484,6 +516,15 @@ static int starts_with(const CBS *cbs, uint8_t c) {
484516
return CBS_len(cbs) > 0 && CBS_data(cbs)[0] == c;
485517
}
486518

519+
static int starts_with_str(const CBS *cbs, const char *str, size_t str_len) {
520+
return CBS_len(cbs) >= str_len &&
521+
!OPENSSL_memcmp(CBS_data(cbs), str, str_len);
522+
}
523+
524+
static int ends_with_byte(const CBS *cbs, uint8_t c) {
525+
return CBS_len(cbs) > 0 && CBS_data(cbs)[CBS_len(cbs) - 1] == c;
526+
}
527+
487528
static int equal_case(const CBS *a, const CBS *b) {
488529
if (CBS_len(a) != CBS_len(b)) {
489530
return 0;
@@ -508,7 +549,8 @@ static int has_suffix_case(const CBS *a, const CBS *b) {
508549
return equal_case(&copy, b);
509550
}
510551

511-
static int nc_dns(const ASN1_IA5STRING *dns, const ASN1_IA5STRING *base) {
552+
static int nc_dns(const ASN1_IA5STRING *dns, const ASN1_IA5STRING *base,
553+
int excluding) {
512554
CBS dns_cbs, base_cbs;
513555
CBS_init(&dns_cbs, dns->data, dns->length);
514556
CBS_init(&base_cbs, base->data, base->length);
@@ -518,6 +560,34 @@ static int nc_dns(const ASN1_IA5STRING *dns, const ASN1_IA5STRING *base) {
518560
return X509_V_OK;
519561
}
520562

563+
// Normalize absolute DNS names by removing the trailing dot, if any.
564+
if (ends_with_byte(&dns_cbs, '.')) {
565+
uint8_t unused;
566+
CBS_get_last_u8(&dns_cbs, &unused);
567+
}
568+
if (ends_with_byte(&base_cbs, '.')) {
569+
uint8_t unused;
570+
CBS_get_last_u8(&base_cbs, &unused);
571+
}
572+
573+
// Wildcard partial-match handling ("*.bar.com" matching name constraint
574+
// "foo.bar.com"). This only handles the case where the dnsname and the
575+
// constraint match after removing the leftmost label, otherwise it is handled
576+
// by falling through to the check of whether the dnsname is fully within or
577+
// fully outside of the constraint.
578+
if (excluding && starts_with_str(&dns_cbs, "*.", 2)) {
579+
CBS unused;
580+
CBS base_parent_cbs = base_cbs;
581+
CBS dns_parent_cbs = dns_cbs;
582+
CBS_skip(&dns_parent_cbs, 2);
583+
if (CBS_get_until_first(&base_parent_cbs, &unused, '.') &&
584+
CBS_skip(&base_parent_cbs, 1)) {
585+
if (equal_case(&dns_parent_cbs, &base_parent_cbs)) {
586+
return X509_V_OK;
587+
}
588+
}
589+
}
590+
521591
// If |base_cbs| begins with a '.', do a simple suffix comparison. This is
522592
// not part of RFC5280, but is part of OpenSSL's original behavior.
523593
if (starts_with(&base_cbs, '.')) {

crypto/x509/x509_compat_test.cc

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2496,6 +2496,39 @@ TEST(X509CompatTest, CommonNameToDNS) {
24962496
{"eXaMpLe", false, ""},
24972497
{"cn with spaces", false, ""},
24982498
{"1 and 2 and 3", false, ""},
2499+
// Wildcard CN test cases: wildcard CNs must be recognized as DNS-IDs
2500+
// so that name constraints are enforced on them.
2501+
{"*.example.com", true, "*.example.com"},
2502+
{"*.sub.example.com", true, "*.sub.example.com"},
2503+
{"*.exa_mple.com", true, "*.exa_mple.com"},
2504+
{"*.foo-bar.example.com", true, "*.foo-bar.example.com"},
2505+
// Wildcard with single remaining label is not a DNS name.
2506+
{"*.com", false, ""},
2507+
// Bare wildcard or incomplete wildcard prefix.
2508+
{"*", false, ""},
2509+
{"*.", false, ""},
2510+
{"*..", false, ""},
2511+
// Star without dot is not a valid wildcard prefix.
2512+
{"*example.com", false, ""},
2513+
// Double star is not valid.
2514+
{"**.example.com", false, ""},
2515+
// Invalid DNS syntax after wildcard prefix.
2516+
{"*.-example.com", false, ""},
2517+
{"*.example-.com", false, ""},
2518+
{"*.example..com", false, ""},
2519+
{"*.example.com.", false, ""},
2520+
{".*.example.com", false, ""},
2521+
// Single-label CNs with non-ASCII are accepted (not DNS names).
2522+
{"r\xc3\xa4ger", false, ""},
2523+
{"\xc3\x9cnternehmen", false, ""},
2524+
// Single-label CNs with control characters are accepted (not DNS names).
2525+
{"foo\x01" "bar", false, ""},
2526+
{std::string("\x7f") + "tld", false, ""},
2527+
// Single-label CN with space is accepted (not a DNS name).
2528+
{"foo bar", false, ""},
2529+
// Punycode equivalents are valid ASCII DNS names.
2530+
{"xn--rger-koa.evil.com", true, "xn--rger-koa.evil.com"},
2531+
{"xn--caf-dma.example.com", true, "xn--caf-dma.example.com"},
24992532
};
25002533

25012534
for (CommonNameToDNSTestParam &param : params) {
@@ -2520,6 +2553,38 @@ TEST(X509CompatTest, CommonNameToDNS) {
25202553
ASSERT_EQ((size_t)0, idlen);
25212554
}
25222555
}
2556+
2557+
// Multi-label CNs with non-ASCII bytes should return
2558+
// X509_V_ERR_UNSUPPORTED_NAME_SYNTAX. Per RFC 6125 Section 6.4.2,
2559+
// internationalized domain names should use A-label (punycode) form.
2560+
const std::string non_ascii_dns[] = {
2561+
"r\xc3\xa4ger.evil.com",
2562+
"caf\xc3\xa9.example.com",
2563+
"*.r\xc3\xa4ger.evil.com",
2564+
// Control characters in multi-label CNs.
2565+
"foo\x01.evil.com",
2566+
std::string("bar\x7f") + ".evil.com",
2567+
"baz\x1f.example.com",
2568+
// Space in multi-label CN.
2569+
"foo .evil.com",
2570+
};
2571+
for (const std::string &name : non_ascii_dns) {
2572+
SCOPED_TRACE(name);
2573+
ASN1_STRING *asn1_str_ptr = NULL;
2574+
std::vector<uint8_t> cn(name.begin(), name.end());
2575+
ASSERT_GE(ASN1_mbstring_copy(&asn1_str_ptr, cn.data(), cn.size(),
2576+
MBSTRING_UTF8, V_ASN1_IA5STRING),
2577+
0);
2578+
bssl::UniquePtr<ASN1_STRING> asn1_cn(asn1_str_ptr);
2579+
ASSERT_TRUE(asn1_cn);
2580+
unsigned char *dnsid_ptr = NULL;
2581+
size_t idlen = 0;
2582+
ASSERT_EQ(X509_V_ERR_UNSUPPORTED_NAME_SYNTAX,
2583+
cn2dnsid(asn1_cn.get(), &dnsid_ptr, &idlen));
2584+
ASSERT_FALSE(dnsid_ptr);
2585+
ASSERT_EQ((size_t)0, idlen);
2586+
}
2587+
25232588
}
25242589

25252590

0 commit comments

Comments
 (0)