Skip to content

Commit a97b5f9

Browse files
committed
quic: use OpenSSL built-in cert and hostname validation
PR-URL: #34533 Reviewed-By: Anna Henningsen <anna@addaleax.net>
1 parent 1e47051 commit a97b5f9

6 files changed

+21
-608
lines changed

node.gyp

+1-2
Original file line numberDiff line numberDiff line change
@@ -1235,8 +1235,7 @@
12351235
],
12361236
'sources': [
12371237
'test/cctest/test_quic_buffer.cc',
1238-
'test/cctest/test_quic_cid.cc',
1239-
'test/cctest/test_quic_verifyhostnameidentity.cc'
1238+
'test/cctest/test_quic_cid.cc'
12401239
]
12411240
}],
12421241
['v8_enable_inspector==1', {

src/quic/node_quic_crypto.cc

+8-224
Original file line numberDiff line numberDiff line change
@@ -331,229 +331,6 @@ bool InvalidRetryToken(
331331
return false;
332332
}
333333

334-
namespace {
335-
336-
bool SplitHostname(
337-
const char* hostname,
338-
std::vector<std::string>* parts,
339-
const char delim = '.') {
340-
static std::string check_str =
341-
"\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2A\x2B\x2C\x2D\x2E\x2F\x30"
342-
"\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3A\x3B\x3C\x3D\x3E\x3F\x40"
343-
"\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4A\x4B\x4C\x4D\x4E\x4F\x50"
344-
"\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5A\x5B\x5C\x5D\x5E\x5F\x60"
345-
"\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6A\x6B\x6C\x6D\x6E\x6F\x70"
346-
"\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7A\x7B\x7C\x7D\x7E\x7F";
347-
348-
std::stringstream str(hostname);
349-
std::string part;
350-
while (getline(str, part, delim)) {
351-
// if (part.length() == 0 ||
352-
// part.find_first_not_of(check_str) != std::string::npos) {
353-
// return false;
354-
// }
355-
for (size_t n = 0; n < part.length(); n++) {
356-
if (part[n] >= 'A' && part[n] <= 'Z')
357-
part[n] = (part[n] | 0x20); // Lower case the letter
358-
if (check_str.find(part[n]) == std::string::npos)
359-
return false;
360-
}
361-
parts->push_back(part);
362-
}
363-
return true;
364-
}
365-
366-
bool CheckCertNames(
367-
const std::vector<std::string>& host_parts,
368-
const std::string& name,
369-
bool use_wildcard = true) {
370-
371-
if (name.length() == 0)
372-
return false;
373-
374-
std::vector<std::string> name_parts;
375-
if (!SplitHostname(name.c_str(), &name_parts))
376-
return false;
377-
378-
if (name_parts.size() != host_parts.size())
379-
return false;
380-
381-
for (size_t n = host_parts.size() - 1; n > 0; --n) {
382-
if (host_parts[n] != name_parts[n])
383-
return false;
384-
}
385-
386-
if (name_parts[0].find('*') == std::string::npos ||
387-
name_parts[0].find("xn--") != std::string::npos) {
388-
return host_parts[0] == name_parts[0];
389-
}
390-
391-
if (!use_wildcard)
392-
return false;
393-
394-
std::vector<std::string> sub_parts;
395-
SplitHostname(name_parts[0].c_str(), &sub_parts, '*');
396-
397-
if (sub_parts.size() > 2)
398-
return false;
399-
400-
if (name_parts.size() <= 2)
401-
return false;
402-
403-
std::string prefix;
404-
std::string suffix;
405-
if (sub_parts.size() == 2) {
406-
prefix = sub_parts[0];
407-
suffix = sub_parts[1];
408-
} else {
409-
prefix = "";
410-
suffix = sub_parts[0];
411-
}
412-
413-
if (prefix.length() + suffix.length() > host_parts[0].length())
414-
return false;
415-
416-
if (host_parts[0].compare(0, prefix.length(), prefix))
417-
return false;
418-
419-
if (host_parts[0].compare(
420-
host_parts[0].length() - suffix.length(),
421-
suffix.length(), suffix)) {
422-
return false;
423-
}
424-
425-
return true;
426-
}
427-
428-
} // namespace
429-
430-
int VerifyHostnameIdentity(
431-
const crypto::SSLPointer& ssl,
432-
const char* hostname) {
433-
int err = X509_V_ERR_HOSTNAME_MISMATCH;
434-
crypto::X509Pointer cert(SSL_get_peer_certificate(ssl.get()));
435-
if (!cert)
436-
return err;
437-
438-
// There are several pieces of information we need from the cert at this point
439-
// 1. The Subject (if it exists)
440-
// 2. The collection of Alt Names (if it exists)
441-
//
442-
// The certificate may have many Alt Names. We only care about the ones that
443-
// are prefixed with 'DNS:', 'URI:', or 'IP Address:'. We might check
444-
// additional ones later but we'll start with these.
445-
//
446-
// Ideally, we'd be able to *just* use OpenSSL's built in name checking for
447-
// this (SSL_set1_host and X509_check_host) but it does not appear to do
448-
// checking on URI or IP Address Alt names, which is unfortunate. We need
449-
// both of those to retain compatibility with the peer identity verification
450-
// Node.js already does elsewhere. At the very least, we'll use
451-
// X509_check_host here first as a first step. If it is successful, awesome,
452-
// there's nothing else for us to do. Return and be happy!
453-
if (X509_check_host(
454-
cert.get(),
455-
hostname,
456-
strlen(hostname),
457-
X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT |
458-
X509_CHECK_FLAG_MULTI_LABEL_WILDCARDS |
459-
X509_CHECK_FLAG_SINGLE_LABEL_SUBDOMAINS,
460-
nullptr) > 0) {
461-
return 0;
462-
}
463-
464-
if (X509_check_ip_asc(
465-
cert.get(),
466-
hostname,
467-
X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT) > 0) {
468-
return 0;
469-
}
470-
471-
// If we've made it this far, then we have to perform a more check
472-
return VerifyHostnameIdentity(
473-
hostname,
474-
crypto::GetCertificateCN(cert.get()),
475-
crypto::GetCertificateAltNames(cert.get()));
476-
}
477-
478-
int VerifyHostnameIdentity(
479-
const char* hostname,
480-
const std::string& cert_cn,
481-
const std::unordered_multimap<std::string, std::string>& altnames) {
482-
483-
int err = X509_V_ERR_HOSTNAME_MISMATCH;
484-
485-
// 1. If the hostname is an IP address (v4 or v6), the certificate is valid
486-
// if and only if there is an 'IP Address:' alt name specifying the same
487-
// IP address. The IP address must be canonicalized to ensure a proper
488-
// check. It's possible that the X509_check_ip_asc covers this. If so,
489-
// we can remove this check.
490-
491-
if (SocketAddress::is_numeric_host(hostname)) {
492-
auto ips = altnames.equal_range("ip");
493-
for (auto ip = ips.first; ip != ips.second; ++ip) {
494-
if (ip->second.compare(hostname) == 0) {
495-
// Success!
496-
return 0;
497-
}
498-
}
499-
// No match, and since the hostname is an IP address, skip any
500-
// further checks
501-
return err;
502-
}
503-
504-
auto dns_names = altnames.equal_range("dns");
505-
auto uri_names = altnames.equal_range("uri");
506-
507-
size_t dns_count = std::distance(dns_names.first, dns_names.second);
508-
size_t uri_count = std::distance(uri_names.first, uri_names.second);
509-
510-
std::vector<std::string> host_parts;
511-
SplitHostname(hostname, &host_parts);
512-
513-
// 2. If there no 'DNS:' or 'URI:' Alt names, if the certificate has a
514-
// Subject, then we need to extract the CN field from the Subject. and
515-
// check that the hostname matches the CN, taking into consideration
516-
// the possibility of a wildcard in the CN. If there is a match, congrats,
517-
// we have a valid certificate. Return and be happy.
518-
519-
if (dns_count == 0 && uri_count == 0) {
520-
if (cert_cn.length() > 0 && CheckCertNames(host_parts, cert_cn))
521-
return 0;
522-
// No match, and since there are no dns or uri entries, return
523-
return err;
524-
}
525-
526-
// 3. If, however, there are 'DNS:' and 'URI:' Alt names, things become more
527-
// complicated. Essentially, we need to iterate through each 'DNS:' and
528-
// 'URI:' Alt name to find one that matches. The 'DNS:' Alt names are
529-
// relatively simple but may include wildcards. The 'URI:' Alt names
530-
// require the name to be parsed as a URL, then extract the hostname from
531-
// the URL, which is then checked against the hostname. If you find a
532-
// match, yay! Return and be happy. (Note, it's possible that the 'DNS:'
533-
// check in this step is redundant to the X509_check_host check. If so,
534-
// we can simplify by removing those checks here.)
535-
536-
// First, let's check dns names
537-
for (auto name = dns_names.first; name != dns_names.second; ++name) {
538-
if (name->first.length() > 0 &&
539-
CheckCertNames(host_parts, name->second)) {
540-
return 0;
541-
}
542-
}
543-
544-
// Then, check uri names
545-
for (auto name = uri_names.first; name != uri_names.second; ++name) {
546-
if (name->first.length() > 0 &&
547-
CheckCertNames(host_parts, name->second, false)) {
548-
return 0;
549-
}
550-
}
551-
552-
// 4. Failing all of the previous checks, we assume the certificate is
553-
// invalid for an unspecified reason.
554-
return err;
555-
}
556-
557334
// Get the ALPN protocol identifier that was negotiated for the session
558335
Local<Value> GetALPNProtocol(const QuicSession& session) {
559336
QuicCryptoContext* ctx = session.crypto_context();
@@ -747,10 +524,17 @@ SSL_QUIC_METHOD quic_method = SSL_QUIC_METHOD{
747524
void SetHostname(const crypto::SSLPointer& ssl, const std::string& hostname) {
748525
// If the hostname is an IP address, use an empty string
749526
// as the hostname instead.
750-
if (SocketAddress::is_numeric_host(hostname.c_str())) {
527+
X509_VERIFY_PARAM* param = SSL_get0_param(ssl.get());
528+
X509_VERIFY_PARAM_set_hostflags(param, 0);
529+
530+
if (UNLIKELY(SocketAddress::is_numeric_host(hostname.c_str()))) {
751531
SSL_set_tlsext_host_name(ssl.get(), "");
532+
CHECK_EQ(X509_VERIFY_PARAM_set1_host(param, "", 0), 1);
752533
} else {
753534
SSL_set_tlsext_host_name(ssl.get(), hostname.c_str());
535+
CHECK_EQ(
536+
X509_VERIFY_PARAM_set1_host(param, hostname.c_str(), hostname.length()),
537+
1);
754538
}
755539
}
756540

src/quic/node_quic_crypto.h

-6
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,6 @@ bool InvalidRetryToken(
100100
const uint8_t* token_secret,
101101
uint64_t verification_expiration);
102102

103-
int VerifyHostnameIdentity(const crypto::SSLPointer& ssl, const char* hostname);
104-
int VerifyHostnameIdentity(
105-
const char* hostname,
106-
const std::string& cert_cn,
107-
const std::unordered_multimap<std::string, std::string>& altnames);
108-
109103
// Get the ALPN protocol identifier that was negotiated for the session
110104
v8::Local<v8::Value> GetALPNProtocol(const QuicSession& session);
111105

src/quic/node_quic_session.cc

+11-26
Original file line numberDiff line numberDiff line change
@@ -570,19 +570,22 @@ void JSQuicSessionListener::OnHandshakeCompleted() {
570570
String::NewFromUtf8(env->isolate(), hostname).ToLocalChecked();
571571
}
572572

573-
int err = ctx->VerifyPeerIdentity(
574-
hostname != nullptr ?
575-
hostname :
576-
session()->hostname().c_str());
573+
Local<Value> validationErrorReason = v8::Null(env->isolate());
574+
Local<Value> validationErrorCode = v8::Null(env->isolate());
575+
int err = ctx->VerifyPeerIdentity();
576+
if (err != X509_V_OK) {
577+
crypto::GetValidationErrorReason(env, err).ToLocal(&validationErrorReason);
578+
crypto::GetValidationErrorCode(env, err).ToLocal(&validationErrorCode);
579+
}
577580

578581
Local<Value> argv[] = {
579582
servername,
580583
GetALPNProtocol(*session()),
581584
ctx->cipher_name().ToLocalChecked(),
582585
ctx->cipher_version().ToLocalChecked(),
583586
Integer::New(env->isolate(), session()->max_pktlen_),
584-
crypto::GetValidationErrorReason(env, err).ToLocalChecked(),
585-
crypto::GetValidationErrorCode(env, err).ToLocalChecked(),
587+
validationErrorReason,
588+
validationErrorCode,
586589
session()->crypto_context()->early_data() ?
587590
v8::True(env->isolate()) :
588591
v8::False(env->isolate())
@@ -1144,26 +1147,8 @@ bool QuicCryptoContext::InitiateKeyUpdate() {
11441147
uv_hrtime()) == 0;
11451148
}
11461149

1147-
int QuicCryptoContext::VerifyPeerIdentity(const char* hostname) {
1148-
int err = crypto::VerifyPeerCertificate(ssl_);
1149-
if (err)
1150-
return err;
1151-
1152-
// QUIC clients are required to verify the peer identity, servers are not.
1153-
switch (side_) {
1154-
case NGTCP2_CRYPTO_SIDE_CLIENT:
1155-
if (LIKELY(is_option_set(
1156-
QUICCLIENTSESSION_OPTION_VERIFY_HOSTNAME_IDENTITY))) {
1157-
return VerifyHostnameIdentity(ssl_, hostname);
1158-
}
1159-
break;
1160-
case NGTCP2_CRYPTO_SIDE_SERVER:
1161-
// TODO(@jasnell): In the future, we may want to implement this but
1162-
// for now we keep things simple and skip peer identity verification.
1163-
break;
1164-
}
1165-
1166-
return 0;
1150+
int QuicCryptoContext::VerifyPeerIdentity() {
1151+
return crypto::VerifyPeerCertificate(ssl_);
11671152
}
11681153

11691154
// Write outbound TLS handshake data into the ngtcp2 connection

src/quic/node_quic_session.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -477,7 +477,7 @@ class QuicCryptoContext final : public MemoryRetainer {
477477

478478
bool InitiateKeyUpdate();
479479

480-
int VerifyPeerIdentity(const char* hostname);
480+
int VerifyPeerIdentity();
481481

482482
QuicSession* session() const { return session_.get(); }
483483

0 commit comments

Comments
 (0)