Skip to content

Support client-side hostname checks with leading . #2403

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: x509
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions crypto/test/x509_util.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@

#include <openssl/x509.h>

int Verify(X509 *leaf, const std::vector<X509 *> &roots,
const std::vector<X509 *> &intermediates,
const std::vector<X509_CRL *> &crls, unsigned long flags = 0,
std::function<void(X509_STORE_CTX *)> configure_callback = nullptr);
int Verify(
X509 *leaf, const std::vector<X509 *> &roots,
const std::vector<X509 *> &intermediates,
const std::vector<X509_CRL *> &crls, unsigned long flags = 0,
std::function<void(X509_STORE_CTX *)> configure_callback = nullptr);

// CRLsToStack converts a vector of |X509_CRL*| to an OpenSSL
// STACK_OF(X509_CRL), bumping the reference counts for each CRL in question.
Expand Down
5 changes: 5 additions & 0 deletions crypto/x509/internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,11 @@ OPENSSL_EXPORT int validate_cidr_mask(CBS *cidr_mask);

OPENSSL_EXPORT int cn2dnsid(ASN1_STRING *cn, unsigned char **dnsid, size_t *idlen);

// Match reference identifiers starting with "." to any sub-domain.
// This is a non-public flag, turned on implicitly when the subject
// reference identity is a DNS name.
#define _X509_CHECK_FLAG_DOT_SUBDOMAINS 0x8000
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although this one needs to be in an internal header file, it would be nice if all of the relevant bit-flags were documented here with it:

// External supported flags:
// * X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT 0x0001
// * X509_CHECK_FLAG_NO_WILDCARDS         0x0002
// * X509_CHECK_FLAG_NEVER_CHECK_SUBJECT  0x0020
// * X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS 0x000?

// Internal flags:
// * _X509_CHECK_FLAG_DOT_SUBDOMAINS      0x8000

// External unsupported flags:
// * X509_CHECK_FLAG_MULTI_LABEL_WILDCARDS
// * X509_CHECK_FLAG_SINGLE_LABEL_SUBDOMAINS


#if defined(__cplusplus)
} // extern C
#endif
Expand Down
39 changes: 39 additions & 0 deletions crypto/x509/v3_utl.c
Original file line number Diff line number Diff line change
Expand Up @@ -686,10 +686,40 @@ typedef int (*equal_fn)(const unsigned char *pattern, size_t pattern_len,
const unsigned char *subject, size_t subject_len,
unsigned int flags);

// Skip pattern prefix to match "wildcard" subject
static void skip_prefix(const unsigned char **p, size_t *plen,
size_t subject_len, unsigned int flags) {
const unsigned char *pattern = *p;
size_t pattern_len = *plen;

// If subject starts with a leading '.' followed by more octets, and
// pattern is longer, compare just an equal-length suffix with the
// full subject (starting at the '.'), provided the prefix contains
// no NULs.
if ((flags & _X509_CHECK_FLAG_DOT_SUBDOMAINS) == 0) {
return;
}

while (pattern_len > subject_len && *pattern) {
if ((flags & X509_CHECK_FLAG_SINGLE_LABEL_SUBDOMAINS) && *pattern == '.') {
break;
}
++pattern;
--pattern_len;
}

// Skip if entire prefix acceptable
if (pattern_len == subject_len) {
*p = pattern;
*plen = pattern_len;
}
}

// Compare while ASCII ignoring case.
static int equal_nocase(const unsigned char *pattern, size_t pattern_len,
const unsigned char *subject, size_t subject_len,
unsigned int flags) {
skip_prefix(&pattern, &pattern_len, subject_len, flags);
if (pattern_len != subject_len) {
return 0;
}
Expand Down Expand Up @@ -719,6 +749,7 @@ static int equal_nocase(const unsigned char *pattern, size_t pattern_len,
static int equal_case(const unsigned char *pattern, size_t pattern_len,
const unsigned char *subject, size_t subject_len,
unsigned int flags) {
skip_prefix(&pattern, &pattern_len, subject_len, flags);
if (pattern_len != subject_len) {
return 0;
}
Expand Down Expand Up @@ -932,12 +963,20 @@ static int do_x509_check(const X509 *x, const char *chk, size_t chklen,
int alt_type;
int rv = 0;
equal_fn equal;

// This flag is internal-only and is never expected to be set by caller.
flags &= ~_X509_CHECK_FLAG_DOT_SUBDOMAINS;

if (check_type == GEN_EMAIL) {
cnid = NID_pkcs9_emailAddress;
alt_type = V_ASN1_IA5STRING;
equal = equal_email;
} else if (check_type == GEN_DNS) {
cnid = NID_commonName;
// Implicit client-side DNS sub-domain pattern
if (chklen > 1 && chk[0] == '.') {
flags |= _X509_CHECK_FLAG_DOT_SUBDOMAINS;
}
alt_type = V_ASN1_IA5STRING;
if (flags & X509_CHECK_FLAG_NO_WILDCARDS) {
equal = equal_nocase;
Expand Down
181 changes: 181 additions & 0 deletions crypto/x509/x509_compat_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1869,6 +1869,69 @@ Lw6dQq2WGG6gApL/Cc0QonvzksvY5Ewf2qIpu2Si
-----END CERTIFICATE-----
)";

/*
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
13:c1:63:e7:03:6f:a3:26:ac:31:71:a9:fe:ad:a6:34:20:94:bf:3a
Signature Algorithm: ecdsa-with-SHA256
Issuer: C=US, ST=Washington, O=AWS Libcrypto, OU=Good CA, CN=Root CA 1
Validity
Not Before: Jan 1 00:00:00 2015 GMT
Not After : Jan 1 00:00:00 2100 GMT
Subject: C=US, ST=Washington, O=AWS Libcrypto, OU=Good Endpoint, SN=Wildcard Testing, CN=*.sub2.example.com
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
pub:
04:b2:b7:bd:35:f2:eb:da:86:d5:dc:40:44:c7:23:
14:f9:d0:a5:40:17:30:85:b6:c6:11:38:c2:db:2c:
c5:bc:0c:19:11:d8:68:61:d6:a3:92:6b:8a:18:52:
2c:dc:86:a7:ad:29:ad:91:ac:7e:df:87:24:3b:f3:
b4:71:2b:4e:58
ASN1 OID: prime256v1
NIST CURVE: P-256
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Extended Key Usage:
TLS Web Server Authentication, TLS Web Client Authentication
X509v3 Subject Alternative Name:
DNS:*.sub1.example.com, DNS:*.example.org, DNS:host.example.com
X509v3 Subject Key Identifier:
C8:78:64:E9:F7:9C:0F:56:E2:1D:CE:EE:ED:24:E0:9F:1D:4B:A3:BF
X509v3 Authority Key Identifier:
19:19:E1:8C:09:E2:5D:5C:16:04:E1:9C:74:66:19:FD:B8:52:5B:DF
Signature Algorithm: ecdsa-with-SHA256
Signature Value:
30:45:02:20:13:bc:6c:9c:3b:8e:c7:95:e7:9f:31:08:dd:7f:
6c:ea:97:4e:29:01:72:b5:9c:45:f1:29:bc:d7:ce:39:5a:21:
02:21:00:f5:77:2c:9d:23:6d:71:69:4d:93:eb:7e:fd:a5:17:
24:37:ee:97:01:4f:1c:54:09:cd:3d:87:e9:1d:da:5f:7e
*/
static char kFoo[] = R"(
-----BEGIN CERTIFICATE-----
MIICsDCCAlagAwIBAgIUE8Fj5wNvoyasMXGp/q2mNCCUvzowCgYIKoZIzj0EAwIw
YDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldhc2hpbmd0b24xFjAUBgNVBAoMDUFX
UyBMaWJjcnlwdG8xEDAOBgNVBAsMB0dvb2QgQ0ExEjAQBgNVBAMMCVJvb3QgQ0Eg
MTAgFw0xNTAxMDEwMDAwMDBaGA8yMTAwMDEwMTAwMDAwMFowgYoxCzAJBgNVBAYT
AlVTMRMwEQYDVQQIDApXYXNoaW5ndG9uMRYwFAYDVQQKDA1BV1MgTGliY3J5cHRv
MRYwFAYDVQQLDA1Hb29kIEVuZHBvaW50MRkwFwYDVQQEDBBXaWxkY2FyZCBUZXN0
aW5nMRswGQYDVQQDDBIqLnN1YjIuZXhhbXBsZS5jb20wWTATBgcqhkjOPQIBBggq
hkjOPQMBBwNCAASyt7018uvahtXcQETHIxT50KVAFzCFtsYROMLbLMW8DBkR2Ghh
1qOSa4oYUizchqetKa2RrH7fhyQ787RxK05Yo4HAMIG9MA4GA1UdDwEB/wQEAwIF
oDAMBgNVHRMBAf8EAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjA+
BgNVHREENzA1ghIqLnN1YjEuZXhhbXBsZS5jb22CDSouZXhhbXBsZS5vcmeCEGhv
c3QuZXhhbXBsZS5jb20wHQYDVR0OBBYEFMh4ZOn3nA9W4h3O7u0k4J8dS6O/MB8G
A1UdIwQYMBaAFBkZ4YwJ4l1cFgThnHRmGf24UlvfMAoGCCqGSM49BAMCA0gAMEUC
IBO8bJw7jseV558xCN1/bOqXTikBcrWcRfEpvNfOOVohAiEA9XcsnSNtcWlNk+t+
/aUXJDfulwFPHFQJzT2H6R3aX34=
-----END CERTIFICATE-----
)";

// EE certificate should not verify if signed by invalid root CA
TEST(X509CompatTest, CertificatesFromTrustStoreValidated) {
bssl::UniquePtr<X509> root = CertFromPEM(kRootBadBasicConstraints);
Expand Down Expand Up @@ -2367,3 +2430,121 @@ TEST(X509CompatTest, CommonNameToDNS) {
}
}


TEST(X509CompatTest, WildcardNameBehaviors) {
bssl::UniquePtr<X509> root = CertFromPEM(kValidRootCA1);
ASSERT_TRUE(root);
bssl::UniquePtr<X509> leaf = CertFromPEM(kFoo);
ASSERT_TRUE(leaf);

EXPECT_EQ(X509_V_ERR_HOSTNAME_MISMATCH,
Verify(
leaf.get(), /*roots=*/{root.get()},
/*intermediates=*/{},
/*crls=*/{}, /*flags=*/0,
[](X509_STORE_CTX *ctx) {
X509_VERIFY_PARAM *param = X509_STORE_CTX_get0_param(ctx);
char host[] = "example.com";
X509_VERIFY_PARAM_set1_host(param, host, sizeof(host) - 1);
}));
EXPECT_EQ(X509_V_OK,
Verify(leaf.get(), /*roots=*/{root.get()},
/*intermediates=*/{},
/*crls=*/{}, /*flags=*/0, [](X509_STORE_CTX *ctx) {
X509_VERIFY_PARAM *param = X509_STORE_CTX_get0_param(ctx);
char host[] = "host.example.com";
X509_VERIFY_PARAM_set1_host(param, host, sizeof(host) - 1);
}));
EXPECT_EQ(X509_V_OK,
Verify(leaf.get(), /*roots=*/{root.get()},
/*intermediates=*/{},
/*crls=*/{}, /*flags=*/0, [](X509_STORE_CTX *ctx) {
X509_VERIFY_PARAM *param = X509_STORE_CTX_get0_param(ctx);
char host[] = "foo.sub1.example.com";
X509_VERIFY_PARAM_set1_host(param, host, sizeof(host) - 1);
}));
// *.sub2.example.com is in the commonName so not checked by default if DNS
// SAN is present
EXPECT_EQ(X509_V_ERR_HOSTNAME_MISMATCH,
Verify(leaf.get(), /*roots=*/{root.get()},
/*intermediates=*/{},
/*crls=*/{}, /*flags=*/0, [](X509_STORE_CTX *ctx) {
X509_VERIFY_PARAM *param = X509_STORE_CTX_get0_param(ctx);
char host[] = "bar.sub2.example.com";
X509_VERIFY_PARAM_set1_host(param, host, sizeof(host) - 1);
}));
EXPECT_EQ(X509_V_OK,
Verify(leaf.get(), /*roots=*/{root.get()},
/*intermediates=*/{},
/*crls=*/{}, /*flags=*/0, [](X509_STORE_CTX *ctx) {
X509_VERIFY_PARAM *param = X509_STORE_CTX_get0_param(ctx);
X509_VERIFY_PARAM_set_hostflags(
param, X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT);
char host[] = "bar.sub2.example.com";
X509_VERIFY_PARAM_set1_host(param, host, sizeof(host) - 1);
}));
EXPECT_EQ(X509_V_OK,
Verify(leaf.get(), /*roots=*/{root.get()},
/*intermediates=*/{},
/*crls=*/{}, /*flags=*/0, [](X509_STORE_CTX *ctx) {
X509_VERIFY_PARAM *param = X509_STORE_CTX_get0_param(ctx);
char host[] = "baz.example.org";
X509_VERIFY_PARAM_set1_host(param, host, sizeof(host) - 1);
}));

// client-side host name verification behavior with leading '.'
EXPECT_EQ(X509_V_OK,
Verify(leaf.get(), /*roots=*/{root.get()},
/*intermediates=*/{},
/*crls=*/{}, /*flags=*/0, [](X509_STORE_CTX *ctx) {
X509_VERIFY_PARAM *param = X509_STORE_CTX_get0_param(ctx);
char host[] = ".example.com";
X509_VERIFY_PARAM_set1_host(param, host, sizeof(host) - 1);
}));
EXPECT_EQ(X509_V_OK,
Verify(leaf.get(), /*roots=*/{root.get()},
/*intermediates=*/{},
/*crls=*/{}, /*flags=*/0, [](X509_STORE_CTX *ctx) {
X509_VERIFY_PARAM *param = X509_STORE_CTX_get0_param(ctx);
char host[] = ".sub1.example.com";
X509_VERIFY_PARAM_set1_host(param, host, sizeof(host) - 1);
}));
EXPECT_EQ(X509_V_ERR_HOSTNAME_MISMATCH,
Verify(leaf.get(), /*roots=*/{root.get()},
/*intermediates=*/{},
/*crls=*/{}, /*flags=*/0, [](X509_STORE_CTX *ctx) {
X509_VERIFY_PARAM *param = X509_STORE_CTX_get0_param(ctx);
char host[] = ".sub2.example.com";
X509_VERIFY_PARAM_set1_host(param, host, sizeof(host) - 1);
}));
EXPECT_EQ(X509_V_OK,
Verify(leaf.get(), /*roots=*/{root.get()},
/*intermediates=*/{},
/*crls=*/{}, /*flags=*/0, [](X509_STORE_CTX *ctx) {
X509_VERIFY_PARAM *param = X509_STORE_CTX_get0_param(ctx);
X509_VERIFY_PARAM_set_hostflags(
param, X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT);
char host[] = ".sub2.example.com";
X509_VERIFY_PARAM_set1_host(param, host, sizeof(host) - 1);
}));
EXPECT_EQ(X509_V_OK,
Verify(leaf.get(), /*roots=*/{root.get()},
/*intermediates=*/{},
/*crls=*/{}, /*flags=*/0, [](X509_STORE_CTX *ctx) {
X509_VERIFY_PARAM *param = X509_STORE_CTX_get0_param(ctx);
X509_VERIFY_PARAM_set_hostflags(
param, X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT);
char host[] = ".example.org";
X509_VERIFY_PARAM_set1_host(param, host, sizeof(host) - 1);
}));
EXPECT_EQ(X509_V_ERR_HOSTNAME_MISMATCH,
Verify(leaf.get(), /*roots=*/{root.get()},
/*intermediates=*/{},
/*crls=*/{}, /*flags=*/0, [](X509_STORE_CTX *ctx) {
X509_VERIFY_PARAM *param = X509_STORE_CTX_get0_param(ctx);
X509_VERIFY_PARAM_set_hostflags(
param, X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT);
char host[] = ".baz.example.org";
X509_VERIFY_PARAM_set1_host(param, host, sizeof(host) - 1);
}));
}
18 changes: 14 additions & 4 deletions crypto/x509/x509_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5134,19 +5134,29 @@ TEST(X509Test, Names) {
},

// OpenSSL has some non-standard wildcard syntax for input DNS names. We
// do not support this.
// support this for compatibility.
{
/*cert_subject=*/{},
/*cert_dns_names=*/{"www.a.example", "*.b.test"},
/*cert_emails=*/{},
/*valid_dns_names=*/{},
/*valid_dns_names=*/{".a.example", ".b.test", ".example", ".test"},
/*invalid_dns_names=*/
{".www.a.example", ".www.b.test", ".a.example", ".b.test", ".example",
".test"},
{".www.a.example", ".www.b.test"},
/*valid_emails=*/{},
/*invalid_emails=*/{},
/*flags=*/0,
},
{
/*cert_subject=*/{},
/*cert_dns_names=*/{"www.a.example", "*.b.test"},
/*cert_emails=*/{},
/*valid_dns_names=*/{".a.example", ".b.test"},
/*invalid_dns_names=*/
{".www.a.example", ".www.b.test", ".example", ".test"},
/*valid_emails=*/{},
/*invalid_emails=*/{},
/*flags=*/X509_CHECK_FLAG_SINGLE_LABEL_SUBDOMAINS,
},

// Emails match case-sensitively before the '@' and case-insensitively
// after. They do not match DNS names.
Expand Down
9 changes: 8 additions & 1 deletion include/openssl/x509.h
Original file line number Diff line number Diff line change
Expand Up @@ -3086,6 +3086,14 @@ OPENSSL_EXPORT int X509_VERIFY_PARAM_add1_host(X509_VERIFY_PARAM *param,
// X509_CHECK_FLAG_NO_WILDCARDS disables wildcard matching for DNS names.
#define X509_CHECK_FLAG_NO_WILDCARDS 0x2

// X509_CHECK_FLAG_NEVER_CHECK_SUBJECT constrains host name patterns passed to |X509_check_host|
// starting with '.' to only match a single label / subdomain.
//
// For example, by default the host name '.example.com' would match a certificate DNS name like
// 'www.example.com' and 'www.foo.example.com'. Setting this flag would result in the same host name
// only matching 'www.example.com' but not 'www.foo.example.com'.
#define X509_CHECK_FLAG_SINGLE_LABEL_SUBDOMAINS 0x10

// X509_CHECK_FLAG_NEVER_CHECK_SUBJECT disables the subject fallback, normally
// enabled when subjectAltNames is missing.
#define X509_CHECK_FLAG_NEVER_CHECK_SUBJECT 0x20
Expand Down Expand Up @@ -4997,7 +5005,6 @@ OPENSSL_EXPORT void X509_STORE_CTX_set0_untrusted(X509_STORE_CTX *ctx,
// The following flags do nothing. The corresponding non-standard options have
// been removed.
#define X509_CHECK_FLAG_MULTI_LABEL_WILDCARDS 0
#define X509_CHECK_FLAG_SINGLE_LABEL_SUBDOMAINS 0

// X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS does nothing, but is necessary in
// OpenSSL to enable standard wildcard matching. In AWS-LC, this behavior is
Expand Down
1 change: 1 addition & 0 deletions ssl/ssl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2146,6 +2146,7 @@ static void ExpectDefaultVersion(uint16_t min_version, uint16_t max_version,

TEST(SSLTest, DefaultVersion) {
ExpectDefaultVersion(TLS1_VERSION, TLS1_3_VERSION, &TLS_method);
ExpectDefaultVersion(TLS1_VERSION, TLS1_3_VERSION, &TLS_with_buffers_method);
ExpectDefaultVersion(TLS1_VERSION, TLS1_VERSION, &TLSv1_method);
ExpectDefaultVersion(TLS1_1_VERSION, TLS1_1_VERSION, &TLSv1_1_method);
ExpectDefaultVersion(TLS1_2_VERSION, TLS1_2_VERSION, &TLSv1_2_method);
Expand Down
Loading