Repository: trafficserver Updated Branches: refs/heads/master 939014865 -> 1649abc30
TS-3608: Add client side hostname validation for SSL connection Project: http://git-wip-us.apache.org/repos/asf/trafficserver/repo Commit: http://git-wip-us.apache.org/repos/asf/trafficserver/commit/1649abc3 Tree: http://git-wip-us.apache.org/repos/asf/trafficserver/tree/1649abc3 Diff: http://git-wip-us.apache.org/repos/asf/trafficserver/diff/1649abc3 Branch: refs/heads/master Commit: 1649abc30e932bef203d991bbb693a52ba64c248 Parents: 9390148 Author: Uri Shachar <ushac...@apache.org> Authored: Sat May 16 17:17:06 2015 +0300 Committer: Uri Shachar <ushac...@apache.org> Committed: Sat May 16 17:17:06 2015 +0300 ---------------------------------------------------------------------- CHANGES | 2 + iocore/net/Makefile.am | 3 + iocore/net/P_SSLClientUtils.h | 39 +++++ iocore/net/P_SSLUtils.h | 4 +- iocore/net/SSLClientUtils.cc | 186 ++++++++++++++++++++ iocore/net/SSLNetVConnection.cc | 4 + iocore/net/SSLUtils.cc | 87 ---------- lib/ts/Makefile.am | 10 +- lib/ts/X509HostnameValidator.cc | 270 ++++++++++++++++++++++++++++++ lib/ts/X509HostnameValidator.h | 39 +++++ lib/ts/libts.h | 1 + lib/ts/test_X509HostnameValidator.cc | 185 ++++++++++++++++++++ 12 files changed, 738 insertions(+), 92 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/trafficserver/blob/1649abc3/CHANGES ---------------------------------------------------------------------- diff --git a/CHANGES b/CHANGES index b631182..69be670 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,8 @@ -*- coding: utf-8 -*- Changes with Apache Traffic Server 6.0.0 + *) [TS-3608] Client side SSL does not validate upstream hostname + *) [TS-3604] Transparent Mode does not work when accept_threads set to 0. *) [TS-3597] TLS can fail accecpt / handshake when accept thread is turned off. http://git-wip-us.apache.org/repos/asf/trafficserver/blob/1649abc3/iocore/net/Makefile.am ---------------------------------------------------------------------- diff --git a/iocore/net/Makefile.am b/iocore/net/Makefile.am index 2e8405c..a807311 100644 --- a/iocore/net/Makefile.am +++ b/iocore/net/Makefile.am @@ -46,6 +46,7 @@ test_certlookup_SOURCES = \ SSLCertLookup.cc test_certlookup_LDADD = \ + @OPENSSL_LIBS@ \ $(top_builddir)/lib/ts/libtsutil.la \ $(top_builddir)/iocore/eventsystem/libinkevent.a @@ -78,6 +79,7 @@ libinknet_a_SOURCES = \ P_SSLNextProtocolAccept.h \ P_SSLNextProtocolSet.h \ P_SSLUtils.h \ + P_SSLClientUtils.h \ P_OCSPStapling.h \ P_Socks.h \ P_UDPConnection.h \ @@ -102,6 +104,7 @@ libinknet_a_SOURCES = \ SSLNextProtocolAccept.cc \ SSLNextProtocolSet.cc \ SSLUtils.cc \ + SSLClientUtils.cc \ OCSPStapling.cc \ Socks.cc \ UDPIOEvent.cc \ http://git-wip-us.apache.org/repos/asf/trafficserver/blob/1649abc3/iocore/net/P_SSLClientUtils.h ---------------------------------------------------------------------- diff --git a/iocore/net/P_SSLClientUtils.h b/iocore/net/P_SSLClientUtils.h new file mode 100644 index 0000000..b0a1404 --- /dev/null +++ b/iocore/net/P_SSLClientUtils.h @@ -0,0 +1,39 @@ +/** @file + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#ifndef IOCORE_NET_P_SSLCLIENTUTILS_H_ +#define IOCORE_NET_P_SSLCLIENTUTILS_H_ + +#include "ink_config.h" +#include "Diags.h" +#include "P_SSLUtils.h" +#include "P_SSLConfig.h" + +#include <openssl/opensslconf.h> +#include <openssl/ssl.h> + +// Create and initialize a SSL client context. +SSL_CTX *SSLInitClientContext(const struct SSLConfigParams *param); + +// Returns the index used to store our data on the SSL +int get_ssl_client_data_index(); + +#endif /* IOCORE_NET_P_SSLCLIENTUTILS_H_ */ http://git-wip-us.apache.org/repos/asf/trafficserver/blob/1649abc3/iocore/net/P_SSLUtils.h ---------------------------------------------------------------------- diff --git a/iocore/net/P_SSLUtils.h b/iocore/net/P_SSLUtils.h index bd7aa89..efab041 100644 --- a/iocore/net/P_SSLUtils.h +++ b/iocore/net/P_SSLUtils.h @@ -24,6 +24,7 @@ #include "ink_config.h" #include "Diags.h" +#include "P_SSLClientUtils.h" #define OPENSSL_THREAD_DEFINES #include <openssl/opensslconf.h> @@ -112,9 +113,6 @@ extern RecRawStatBlock *ssl_rsb; // Create a default SSL server context. SSL_CTX *SSLDefaultServerContext(); -// Create and initialize a SSL client context. -SSL_CTX *SSLInitClientContext(const SSLConfigParams *param); - // Initialize the SSL library. void SSLInitializeLibrary(); http://git-wip-us.apache.org/repos/asf/trafficserver/blob/1649abc3/iocore/net/SSLClientUtils.cc ---------------------------------------------------------------------- diff --git a/iocore/net/SSLClientUtils.cc b/iocore/net/SSLClientUtils.cc new file mode 100644 index 0000000..0ab741c --- /dev/null +++ b/iocore/net/SSLClientUtils.cc @@ -0,0 +1,186 @@ +/** @file + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#include "ink_config.h" +#include "records/I_RecHttp.h" +#include "libts.h" +#include "P_Net.h" +#include "P_SSLClientUtils.h" + +#include <openssl/err.h> +#include <openssl/pem.h> +#include <openssl/x509.h> + +#if (OPENSSL_VERSION_NUMBER >= 0x10000000L) // openssl returns a const SSL_METHOD +typedef const SSL_METHOD *ink_ssl_method_t; +#else +typedef SSL_METHOD *ink_ssl_method_t; +#endif + +static int ssl_client_data_index = 0; + +int +get_ssl_client_data_index() +{ + return ssl_client_data_index; +} + +int +verify_callback(int preverify_ok, X509_STORE_CTX *ctx) +{ + X509 *cert; + int depth; + int err; + SSL *ssl; + + SSLDebug("Entered verify cb"); + depth = X509_STORE_CTX_get_error_depth(ctx); + cert = X509_STORE_CTX_get_current_cert(ctx); + err = X509_STORE_CTX_get_error(ctx); + + if (!preverify_ok) { + // Don't bother to check the hostname if we failed openssl's verification + SSLDebug("verify error:num=%d:%s:depth=%d", err, X509_verify_cert_error_string(err), depth); + return preverify_ok; + } + if (depth != 0) { + // Not server cert.... + return preverify_ok; + } + /* + * Retrieve the pointer to the SSL of the connection currently treated + * and the application specific data stored into the SSL object. + */ + ssl = static_cast<SSL *>(X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx())); + SSLNetVConnection *netvc = static_cast<SSLNetVConnection *>(SSL_get_ex_data(ssl, ssl_client_data_index)); + if (netvc != nullptr) { + // Match SNI if present + if (netvc->options.sni_servername) { + char *matched_name = nullptr; + if (validate_hostname(cert, reinterpret_cast<unsigned char *>(netvc->options.sni_servername.get()), false, &matched_name)) { + SSLDebug("Hostname %s verified OK, matched %s", netvc->options.sni_servername.get(), matched_name); + ats_free(matched_name); + return preverify_ok; + } + SSLDebug("Hostname verification failed for (%s)", netvc->options.sni_servername.get()); + } + // Otherwise match by IP + else { + char buff[INET6_ADDRSTRLEN]; + ats_ip_ntop(netvc->server_addr, buff, INET6_ADDRSTRLEN); + if (validate_hostname(cert, reinterpret_cast<unsigned char *>(buff), true, nullptr)) { + SSLDebug("IP %s verified OK", buff); + return preverify_ok; + } + SSLDebug("IP verification failed for (%s)", buff); + } + return 0; + } + return preverify_ok; +} + +SSL_CTX * +SSLInitClientContext(const SSLConfigParams *params) +{ + ink_ssl_method_t meth = NULL; + SSL_CTX *client_ctx = NULL; + char *clientKeyPtr = NULL; + + // Note that we do not call RAND_seed() explicitly here, we depend on OpenSSL + // to do the seeding of the PRNG for us. This is the case for all platforms that + // has /dev/urandom for example. + + meth = SSLv23_client_method(); + client_ctx = SSL_CTX_new(meth); + + // disable selected protocols + SSL_CTX_set_options(client_ctx, params->ssl_ctx_options); + if (!client_ctx) { + SSLError("cannot create new client context"); + _exit(1); + } + + if (params->ssl_client_ctx_protocols) { + SSL_CTX_set_options(client_ctx, params->ssl_client_ctx_protocols); + } + if (params->client_cipherSuite != NULL) { + if (!SSL_CTX_set_cipher_list(client_ctx, params->client_cipherSuite)) { + SSLError("invalid client cipher suite in records.config"); + goto fail; + } + } + + // if no path is given for the client private key, + // assume it is contained in the client certificate file. + clientKeyPtr = params->clientKeyPath; + if (clientKeyPtr == NULL) { + clientKeyPtr = params->clientCertPath; + } + + if (params->clientCertPath != 0) { + if (!SSL_CTX_use_certificate_chain_file(client_ctx, params->clientCertPath)) { + SSLError("failed to load client certificate from %s", params->clientCertPath); + goto fail; + } + + if (!SSL_CTX_use_PrivateKey_file(client_ctx, clientKeyPtr, SSL_FILETYPE_PEM)) { + SSLError("failed to load client private key file from %s", clientKeyPtr); + goto fail; + } + + if (!SSL_CTX_check_private_key(client_ctx)) { + SSLError("client private key (%s) does not match the certificate public key (%s)", clientKeyPtr, params->clientCertPath); + goto fail; + } + } + + if (params->clientVerify) { + SSL_CTX_set_verify(client_ctx, SSL_VERIFY_PEER, verify_callback); + SSL_CTX_set_verify_depth(client_ctx, params->client_verify_depth); + + if (params->clientCACertFilename != NULL || params->clientCACertPath != NULL) { + if (!SSL_CTX_load_verify_locations(client_ctx, params->clientCACertFilename, params->clientCACertPath)) { + SSLError("invalid client CA Certificate file (%s) or CA Certificate path (%s)", params->clientCACertFilename, + params->clientCACertPath); + goto fail; + } + } + + if (!SSL_CTX_set_default_verify_paths(client_ctx)) { + SSLError("failed to set the default verify paths"); + goto fail; + } + } + + // Reserve an application data index for SSL verify callback. Since it's always called within the NetVC + // context there's no need for allocating - we can simply save a ptr to the NetVC + ssl_client_data_index = SSL_get_ex_new_index(0, (void *)"NetVC index", nullptr, nullptr, nullptr); + + if (SSLConfigParams::init_ssl_ctx_cb) { + SSLConfigParams::init_ssl_ctx_cb(client_ctx, false); + } + + return client_ctx; + +fail: + SSL_CTX_free(client_ctx); + _exit(1); +} http://git-wip-us.apache.org/repos/asf/trafficserver/blob/1649abc3/iocore/net/SSLNetVConnection.cc ---------------------------------------------------------------------- diff --git a/iocore/net/SSLNetVConnection.cc b/iocore/net/SSLNetVConnection.cc index 5884ed6..1be57d5 100644 --- a/iocore/net/SSLNetVConnection.cc +++ b/iocore/net/SSLNetVConnection.cc @@ -1095,8 +1095,10 @@ SSLNetVConnection::sslClientHandShakeEvent(int &err) SSL_INCREMENT_DYN_STAT(ssl_sni_name_set_failure); } } + #endif + SSL_set_ex_data(ssl, get_ssl_client_data_index(), this); ssl_error_t ssl_error = SSLConnect(ssl); switch (ssl_error) { case SSL_ERROR_NONE: @@ -1155,6 +1157,8 @@ SSLNetVConnection::sslClientHandShakeEvent(int &err) case SSL_ERROR_SSL: default: err = errno; + // FIXME -- This triggers a retry on cases of cert validation errors.... + Debug("ssl", "SSLNetVConnection::sslClientHandShakeEvent, SSL_ERROR_SSL"); SSL_CLR_ERR_INCR_DYN_STAT(this, ssl_error_ssl, "SSLNetVConnection::sslClientHandShakeEvent, SSL_ERROR_SSL errno=%d", errno); return EVENT_ERROR; break; http://git-wip-us.apache.org/repos/asf/trafficserver/blob/1649abc3/iocore/net/SSLUtils.cc ---------------------------------------------------------------------- diff --git a/iocore/net/SSLUtils.cc b/iocore/net/SSLUtils.cc index 1d61a8a..d81bd02 100644 --- a/iocore/net/SSLUtils.cc +++ b/iocore/net/SSLUtils.cc @@ -1468,93 +1468,6 @@ fail: return NULL; } -SSL_CTX * -SSLInitClientContext(const SSLConfigParams *params) -{ - ink_ssl_method_t meth = NULL; - SSL_CTX *client_ctx = NULL; - char *clientKeyPtr = NULL; - - // Note that we do not call RAND_seed() explicitly here, we depend on OpenSSL - // to do the seeding of the PRNG for us. This is the case for all platforms that - // has /dev/urandom for example. - - meth = SSLv23_client_method(); - client_ctx = SSL_CTX_new(meth); - - // disable selected protocols - SSL_CTX_set_options(client_ctx, params->ssl_ctx_options); - if (!client_ctx) { - SSLError("cannot create new client context"); - _exit(1); - } - - if (params->ssl_client_ctx_protocols) { - SSL_CTX_set_options(client_ctx, params->ssl_client_ctx_protocols); - } - if (params->client_cipherSuite != NULL) { - if (!SSL_CTX_set_cipher_list(client_ctx, params->client_cipherSuite)) { - SSLError("invalid client cipher suite in records.config"); - goto fail; - } - } - - // if no path is given for the client private key, - // assume it is contained in the client certificate file. - clientKeyPtr = params->clientKeyPath; - if (clientKeyPtr == NULL) { - clientKeyPtr = params->clientCertPath; - } - - if (params->clientCertPath != 0) { - if (!SSL_CTX_use_certificate_chain_file(client_ctx, params->clientCertPath)) { - SSLError("failed to load client certificate from %s", params->clientCertPath); - goto fail; - } - - if (!SSL_CTX_use_PrivateKey_file(client_ctx, clientKeyPtr, SSL_FILETYPE_PEM)) { - SSLError("failed to load client private key file from %s", clientKeyPtr); - goto fail; - } - - if (!SSL_CTX_check_private_key(client_ctx)) { - SSLError("client private key (%s) does not match the certificate public key (%s)", clientKeyPtr, params->clientCertPath); - goto fail; - } - } - - if (params->clientVerify) { - int client_verify_server; - - client_verify_server = params->clientVerify ? SSL_VERIFY_PEER : SSL_VERIFY_NONE; - SSL_CTX_set_verify(client_ctx, client_verify_server, NULL); - SSL_CTX_set_verify_depth(client_ctx, params->client_verify_depth); - - if (params->clientCACertFilename != NULL && params->clientCACertPath != NULL) { - if (!SSL_CTX_load_verify_locations(client_ctx, params->clientCACertFilename, params->clientCACertPath)) { - SSLError("invalid client CA Certificate file (%s) or CA Certificate path (%s)", params->clientCACertFilename, - params->clientCACertPath); - goto fail; - } - } - - if (!SSL_CTX_set_default_verify_paths(client_ctx)) { - SSLError("failed to set the default verify paths"); - goto fail; - } - } - - if (SSLConfigParams::init_ssl_ctx_cb) { - SSLConfigParams::init_ssl_ctx_cb(client_ctx, false); - } - - return client_ctx; - -fail: - SSL_CTX_free(client_ctx); - _exit(1); -} - static char * asn1_strdup(ASN1_STRING *s) { http://git-wip-us.apache.org/repos/asf/trafficserver/blob/1649abc3/lib/ts/Makefile.am ---------------------------------------------------------------------- diff --git a/lib/ts/Makefile.am b/lib/ts/Makefile.am index d5ca4ac..7e04222 100644 --- a/lib/ts/Makefile.am +++ b/lib/ts/Makefile.am @@ -21,7 +21,7 @@ library_includedir=$(includedir)/ts library_include_HEADERS = apidefs.h noinst_PROGRAMS = mkdfa CompileParseRules -check_PROGRAMS = test_arena test_atomic test_freelist test_geometry test_List test_Map test_Regex test_Vec +check_PROGRAMS = test_arena test_atomic test_freelist test_geometry test_List test_Map test_Regex test_Vec test_X509HostnameValidator TESTS = $(check_PROGRAMS) AM_CPPFLAGS = -I$(top_srcdir)/lib @@ -180,7 +180,9 @@ libtsutil_la_SOURCES = \ llqueue.cc \ lockfile.cc \ signals.cc \ - signals.h + signals.h \ + X509HostnameValidator.cc \ + X509HostnameValidator.h #test_UNUSED_SOURCES = \ # load_http_hdr.cc \ @@ -227,6 +229,10 @@ test_geometry_SOURCES = test_geometry.cc test_geometry_LDADD = libtsutil.la @LIBTCL@ @LIBPCRE@ test_geometry_LDFLAGS = @EXTRA_CXX_LDFLAGS@ @LIBTOOL_LINK_FLAGS@ +test_X509HostnameValidator_SOURCES = test_X509HostnameValidator.cc +test_X509HostnameValidator_LDADD = libtsutil.la @LIBTCL@ @LIBPCRE@ @OPENSSL_LIBS@ +test_X509HostnameValidator_LDFLAGS = @EXTRA_CXX_LDFLAGS@ @LIBTOOL_LINK_FLAGS@ + CompileParseRules_SOURCES = CompileParseRules.cc test:: $(TESTS) http://git-wip-us.apache.org/repos/asf/trafficserver/blob/1649abc3/lib/ts/X509HostnameValidator.cc ---------------------------------------------------------------------- diff --git a/lib/ts/X509HostnameValidator.cc b/lib/ts/X509HostnameValidator.cc new file mode 100644 index 0000000..71e51d8 --- /dev/null +++ b/lib/ts/X509HostnameValidator.cc @@ -0,0 +1,270 @@ +/** @file + + Validate hostname matches certificate according to RFC6125 + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#include <memory.h> +#include <strings.h> +#include <openssl/x509.h> +#include <openssl/x509v3.h> + +#include <ink_memory.h> + +typedef bool (*equal_fn)(const unsigned char *prefix, size_t prefix_len, const unsigned char *suffix, size_t suffix_len); + +/* Return a ptr to a valid wildcard or NULL if not found + * + * Using OpenSSL default flags: + * X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS = False + * X509_CHECK_FLAG_MULTI_LABEL_WILDCARDS = False + * At most one wildcard per pattern. + * No wildcards inside IDNA labels (a full label match is ok: + * *.a.b matches xn--something-or-other.a.b .) + * No wildcards after the first label. + */ + +static const unsigned char * +find_wildcard_in_hostname(const unsigned char *p, size_t len, bool idna_subject) +{ + size_t i = 0; + // Minimum wildcard length *.a.b + if (len < 5) { + return nullptr; + } + + int wildcard_pos = -1; + // Find last dot (can't be last) -- memrchr is GNU extension.... + size_t final_dot_pos = 0; + for (i = len - 2; i > 1; i--) { + if (p[i] == '.') { + final_dot_pos = i; + break; + } + } + // Final dot minimal pos is a.b.xxxxxx + if (final_dot_pos < 3) + return nullptr; + + for (i = 0; i < final_dot_pos; i++) { + /* + * Make sure there are at least two '.' in the string + */ + if (p[i] == '*') { + if (wildcard_pos != -1) { + // Multiple wildcards in first label + break; + } else if (i == 0 || // First char is wildcard + ((i < final_dot_pos - 1) && (p[i + 1] == '.'))) { // Found a trailing wildcard in the first label + + // IDNA hostnames must match a full label + if (idna_subject && (i != 0 || p[i + 1] != '.')) + break; + + wildcard_pos = i; + } else { + // Either mid-label wildcard or not enough dots + break; + } + } + // String contains at least two dots. + if (p[i] == '.') { + if (wildcard_pos != -1) + return &p[wildcard_pos]; + // Only valid wildcard is in the first label + break; + } + } + return nullptr; +} + + +/* + * Comparison functions + * @param pattern is the value from the certificate + * @param subject is the value from the client request + */ + +/* Compare while ASCII ignoring case. */ +static bool +equal_nocase(const unsigned char *pattern, size_t pattern_len, const unsigned char *subject, size_t subject_len) +{ + if (pattern_len != subject_len) + return 0; + return (strncasecmp((char *)pattern, (char *)subject, pattern_len) == 0); +} + +/* Compare using memcmp. */ +static bool +equal_case(const unsigned char *pattern, size_t pattern_len, const unsigned char *subject, size_t subject_len) +{ + if (pattern_len != subject_len) + return 0; + return (memcmp(pattern, subject, pattern_len) == 0); +} + +/* + * Compare the prefix and suffix with the subject, and check that the + * characters in-between are valid. + */ +static bool +wildcard_match(const unsigned char *prefix, size_t prefix_len, const unsigned char *suffix, size_t suffix_len, + const unsigned char *subject, size_t subject_len) +{ + const unsigned char *wildcard_start; + const unsigned char *wildcard_end; + const unsigned char *p; + + if (subject_len < prefix_len + suffix_len) + return false; + if (!equal_nocase(prefix, prefix_len, subject, prefix_len)) + return false; + wildcard_start = subject + prefix_len; + wildcard_end = subject + (subject_len - suffix_len); + if (!equal_nocase(wildcard_end, suffix_len, suffix, suffix_len)) + return false; + /* + * If the wildcard makes up the entire first label, it must match at + * least one character. + */ + if (prefix_len == 0 && *suffix == '.') { + if (wildcard_start == wildcard_end) + return false; + } + /* The wildcard may match a literal '*' */ + if (wildcard_end == wildcard_start + 1 && *wildcard_start == '*') + return true; + /* + * Check that the part matched by the wildcard contains only + * permitted characters and only matches a single label + */ + for (p = wildcard_start; p != wildcard_end; ++p) + if (!(('0' <= *p && *p <= '9') || ('A' <= *p && *p <= 'Z') || ('a' <= *p && *p <= 'z') || *p == '-')) + return false; + return true; +} + + +/* Compare using wildcards. */ +static bool +equal_wildcard(const unsigned char *pattern, size_t pattern_len, const unsigned char *subject, size_t subject_len) +{ + const unsigned char *wildcard = NULL; + + bool is_idna = (subject_len > 4 && strncasecmp((const char *)(subject), "xn--", 4) == 0); + /* + * Subject names starting with '.' can only match a wildcard pattern + * via a subject sub-domain pattern suffix match (that we don't allow). + */ + if (subject_len > 5 && subject[0] != '.') + wildcard = find_wildcard_in_hostname(pattern, pattern_len, is_idna); + + if (wildcard == nullptr) + return equal_nocase(pattern, pattern_len, subject, subject_len); + return wildcard_match(pattern, wildcard - pattern, wildcard + 1, (pattern + pattern_len) - wildcard - 1, subject, subject_len); +} + + +/* + * Compare an ASN1_STRING to a supplied string. only compare if string matches the specified type + * + * Returns true if the strings match, false otherwise + */ + +static bool +do_check_string(ASN1_STRING *a, int cmp_type, equal_fn equal, const unsigned char *b, size_t blen, char **peername) +{ + bool retval = false; + + if (!a->data || !a->length || cmp_type != a->type) + return false; + retval = equal(a->data, a->length, b, blen); + if (retval && peername) + *peername = ats_strndup((char *)a->data, a->length); + return retval; +} + + +bool +validate_hostname(X509 *x, const unsigned char *hostname, bool is_ip, char **peername) +{ + GENERAL_NAMES *gens = NULL; + X509_NAME *name = NULL; + int i; + int alt_type; + bool retval = false; + ; + equal_fn equal; + size_t hostname_len = strlen((char *)hostname); + + if (!is_ip) { + alt_type = V_ASN1_IA5STRING; + equal = equal_wildcard; + } else { + alt_type = V_ASN1_OCTET_STRING; + equal = equal_case; + } + + // Check SANs for a match. + gens = (GENERAL_NAMES *)X509_get_ext_d2i(x, NID_subject_alt_name, NULL, NULL); + if (gens) { + for (i = 0; i < sk_GENERAL_NAME_num(gens); i++) { + GENERAL_NAME *gen; + ASN1_STRING *cstr; + gen = sk_GENERAL_NAME_value(gens, i); + + if (is_ip && gen->type == GEN_IPADD) { + cstr = gen->d.iPAddress; + } else if (!is_ip && gen->type == GEN_DNS) { + cstr = gen->d.dNSName; + } else { + continue; + } + + if ((retval = do_check_string(cstr, alt_type, equal, hostname, hostname_len, peername)) == true) + // We got a match + break; + } + GENERAL_NAMES_free(gens); + if (retval) + return retval; + } + // No SAN match -- check the subject + i = -1; + name = X509_get_subject_name(x); + + while ((i = X509_NAME_get_index_by_NID(name, NID_commonName, i)) >= 0) { + ASN1_STRING *str; + int astrlen; + unsigned char *astr; + str = X509_NAME_ENTRY_get_data(X509_NAME_get_entry(name, i)); + // Convert to UTF-8 + astrlen = ASN1_STRING_to_UTF8(&astr, str); + + if (astrlen < 0) + return -1; + retval = equal(astr, astrlen, hostname, hostname_len); + if (retval && peername) + *peername = ats_strndup((char *)astr, astrlen); + OPENSSL_free(astr); + return retval; + } + return false; +} http://git-wip-us.apache.org/repos/asf/trafficserver/blob/1649abc3/lib/ts/X509HostnameValidator.h ---------------------------------------------------------------------- diff --git a/lib/ts/X509HostnameValidator.h b/lib/ts/X509HostnameValidator.h new file mode 100644 index 0000000..5d9542d --- /dev/null +++ b/lib/ts/X509HostnameValidator.h @@ -0,0 +1,39 @@ +/** @file + + A partial implementation of RFC6125 for verifying that an X509 certificate matches a specific hostname. + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#ifndef LIB_TS_X509HOSTNAMEVALIDATOR_H_ +#define LIB_TS_X509HOSTNAMEVALIDATOR_H_ + +/* + * Validate that the certificate is for the specified hostname/IP address + * @param cert The X509 certificate we match against + * @param hostname Null terminated string that we want to match + * @param is_ip Is the specified hostname an IP string + * @param peername If not NULL, the matching value from the certificate will allocated and the ptr adjusted. + * In this case caller must free afterwards with ats_free + */ + +bool validate_hostname(X509 *cert, const unsigned char *hostname, bool is_ip, char **peername); + + +#endif /* LIB_TS_X509HOSTNAMEVALIDATOR_H_ */ http://git-wip-us.apache.org/repos/asf/trafficserver/blob/1649abc3/lib/ts/libts.h ---------------------------------------------------------------------- diff --git a/lib/ts/libts.h b/lib/ts/libts.h index d244158..f136d74 100644 --- a/lib/ts/libts.h +++ b/lib/ts/libts.h @@ -105,5 +105,6 @@ #include "HostLookup.h" #include "InkErrno.h" #include "Vec.h" +#include "X509HostnameValidator.h" #endif /*_inktomiplus_h_*/ http://git-wip-us.apache.org/repos/asf/trafficserver/blob/1649abc3/lib/ts/test_X509HostnameValidator.cc ---------------------------------------------------------------------- diff --git a/lib/ts/test_X509HostnameValidator.cc b/lib/ts/test_X509HostnameValidator.cc new file mode 100644 index 0000000..7cf94fc --- /dev/null +++ b/lib/ts/test_X509HostnameValidator.cc @@ -0,0 +1,185 @@ +/** @file + + Unit test for the X509 Hostname validation functionality + + @section license License + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#include <openssl/pem.h> +#include <openssl/x509.h> +#include <openssl/ssl.h> +#include <openssl/bio.h> + +#include "ts/X509HostnameValidator.h" +#include "ts/TestBox.h" + +// A simple certificate for CN=test.sslheaders.trafficserver.apache.org. +static const char *test_certificate_cn_name = "test.sslheaders.trafficserver.apache.org"; +static const char *test_certificate_cn = "-----BEGIN CERTIFICATE-----\n" + "MIICGzCCAYSgAwIBAgIJAN/JvtOlj/5HMA0GCSqGSIb3DQEBBQUAMDMxMTAvBgNV\n" + "BAMMKHRlc3Quc3NsaGVhZGVycy50cmFmZmljc2VydmVyLmFwYWNoZS5vcmcwHhcN\n" + "MTQwNzIzMTc1MTA4WhcNMTcwNTEyMTc1MTA4WjAzMTEwLwYDVQQDDCh0ZXN0LnNz\n" + "bGhlYWRlcnMudHJhZmZpY3NlcnZlci5hcGFjaGUub3JnMIGfMA0GCSqGSIb3DQEB\n" + "AQUAA4GNADCBiQKBgQDNuincV56iMA1E7Ss9BlNvRmUdV3An5S6vXHP/hXSVTSj+\n" + "3o0I7es/2noBM7UmXWTBGNjcQYzBed/QIvqM9p5Q4B7kKFTb1xBOl4EU3LHl9fzz\n" + "hxbZMAc2MHW5X8+eCR6K6IBu5sRuLTPvIZhg63/ffhNJTImyW2+eH8guVGd38QID\n" + "AQABozcwNTAzBgNVHREELDAqgih0ZXN0LnNzbGhlYWRlcnMudHJhZmZpY3NlcnZl\n" + "ci5hcGFjaGUub3JnMA0GCSqGSIb3DQEBBQUAA4GBACayHRw5e0iejNkigLARk9aR\n" + "Wiy0WFkUdffhywjnOKxEGvfZGkNQPFN+0SHk7rAm8SlztOIElSvx/y9DByn4IeSw\n" + "2aU6zZiZUSPi9Stg8/tWv9MvOSU/J7CHaHkWuYbfBTBNDokfqFtqY3UJ7Pn+6ybS\n" + "2RZzwmSjinT8GglE30JR\n" + "-----END CERTIFICATE-----\n"; + +// A completely wildcard certificate with invalid wildcard format SANs- shouldn't match anything +static const char *test_certificate_bad_sans = "-----BEGIN CERTIFICATE-----\n" + "MIIB7jCCAZigAwIBAgIJAIECheWAKHNWMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNV\n" + "BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX\n" + "aWRnaXRzIFB0eSBMdGQxEDAOBgNVBAMMByouKi4qLiowHhcNMTUwMzA4MTcxOTIy\n" + "WhcNMjUwMzA1MTcxOTIyWjBXMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1T\n" + "dGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRAwDgYDVQQD\n" + "DAcqLiouKi4qMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAMeiIvB0e2s7gXc4uxmD\n" + "FeUPjVhjGaGejdkgNoAV/z1sV36G06VGj3JBGkw63fhixVoSfk4MJ/tvuMlu/9E4\n" + "wL0CAwEAAaNHMEUwCQYDVR0TBAIwADALBgNVHQ8EBAMCBeAwKwYDVR0RBCQwIoIB\n" + "KoIFKi5jb22CCioubG9uZ2xvbmeHBMCoAQGHBMCoRQ4wDQYJKoZIhvcNAQELBQAD\n" + "QQC+zaPBEbJhL/Euaf2slgTMTKhnI3DUo/H5WXj54BKpefv0dtzjPD9rpEPqilhO\n" + "w0LiMuz7rapF/2++9BVPPmBh\n" + "-----END CERTIFICATE-----\n"; + +/* Multiple wildcard SANs: + * DNS:*.something.or.other, DNS:*.trafficserver.org, DNS:foo*.trafficserver.com, DNS:*bar.trafficserver.net + * CN: test.sslheaders.trafficserver.apache.org + */ +static const char *test_certificate_cn_and_SANs = "-----BEGIN CERTIFICATE-----\n" + "MIICajCCAhSgAwIBAgIJAK5xL+HYV+IuMA0GCSqGSIb3DQEBCwUAMHgxCzAJBgNV\n" + "BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX\n" + "aWRnaXRzIFB0eSBMdGQxMTAvBgNVBAMMKHRlc3Quc3NsaGVhZGVycy50cmFmZmlj\n" + "c2VydmVyLmFwYWNoZS5vcmcwHhcNMTUwMzI0MTUyNTEwWhcNMjUwMzIxMTUyNTEw\n" + "WjB4MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwY\n" + "SW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMTEwLwYDVQQDDCh0ZXN0LnNzbGhlYWRl\n" + "cnMudHJhZmZpY3NlcnZlci5hcGFjaGUub3JnMFwwDQYJKoZIhvcNAQEBBQADSwAw\n" + "SAJBAMeiIvB0e2s7gXc4uxmDFeUPjVhjGaGejdkgNoAV/z1sV36G06VGj3JBGkw6\n" + "3fhixVoSfk4MJ/tvuMlu/9E4wL0CAwEAAaOBgDB+MAkGA1UdEwQCMAAwCwYDVR0P\n" + "BAQDAgXgMGQGA1UdEQRdMFuCFCouc29tZXRoaW5nLm9yLm90aGVyghMqLnRyYWZm\n" + "aWNzZXJ2ZXIub3JnghZmb28qLnRyYWZmaWNzZXJ2ZXIuY29tghYqYmFyLnRyYWZm\n" + "aWNzZXJ2ZXIubmV0MA0GCSqGSIb3DQEBCwUAA0EAQmmFmlZQ6lPudkmjJ0K1mSld\n" + "gQP8uiG6cly7NruPZn2Yc1Cha0TycSYfVkRi0dMF2RKtaVvd4uaXDNb4Qpwv3Q==\n" + "-----END CERTIFICATE-----\n"; + + +static X509 * +load_cert_from_string(const char *cert_string) +{ + BIO *bio = BIO_new_mem_buf((void *)cert_string, -1); + return PEM_read_bio_X509(bio, NULL, 0, NULL); +} + +REGRESSION_TEST(CN_match)(RegressionTest *t, int /* atype ATS_UNUSED */, int *pstatus) +{ + TestBox box(t, pstatus); + char *matching; + + box = REGRESSION_TEST_PASSED; + X509 *x = load_cert_from_string(test_certificate_cn); + box.check(x != NULL, "failed to load the test certificate"); + box.check(validate_hostname(x, (unsigned char *)test_certificate_cn_name, false, &matching) == true, "Hostname should match"); + box.check(strcmp(test_certificate_cn_name, matching) == 0, "Return hostname doesn't match lookup"); + box.check(validate_hostname(x, (unsigned char *)test_certificate_cn_name + 1, false, nullptr) == false, + "Hostname shouldn't match"); +} + +REGRESSION_TEST(bad_wildcard_SANs)(RegressionTest *t, int /* atype ATS_UNUSED */, int *pstatus) +{ + TestBox box(t, pstatus); + + box = REGRESSION_TEST_PASSED; + X509 *x = load_cert_from_string(test_certificate_bad_sans); + box.check(x != NULL, "failed to load the test certificate"); + box.check(validate_hostname(x, (unsigned char *)"something.or.other", false, nullptr) == false, "Hostname shouldn't match"); + box.check(validate_hostname(x, (unsigned char *)"a.b.c", false, nullptr) == false, "Hostname shouldn't match"); + box.check(validate_hostname(x, (unsigned char *)"0.0.0.0", true, nullptr) == false, "Hostname shouldn't match"); + box.check(validate_hostname(x, (unsigned char *)"......", true, nullptr) == false, "Hostname shouldn't match"); + box.check(validate_hostname(x, (unsigned char *)"a.b", true, nullptr) == false, "Hostname shouldn't match"); +} + +REGRESSION_TEST(wildcard_SAN_and_CN)(RegressionTest *t, int /* atype ATS_UNUSED */, int *pstatus) +{ + TestBox box(t, pstatus); + char *matching; + + box = REGRESSION_TEST_PASSED; + X509 *x = load_cert_from_string(test_certificate_cn_and_SANs); + box.check(x != NULL, "failed to load the test certificate"); + box.check(validate_hostname(x, (unsigned char *)test_certificate_cn_name, false, &matching) == true, "Hostname should match"); + box.check(strcmp(test_certificate_cn_name, matching) == 0, "Return hostname doesn't match lookup"); + + box.check(validate_hostname(x, (unsigned char *)"a.trafficserver.org", false, &matching) == true, "Hostname should match"); + box.check(strcmp("*.trafficserver.org", matching) == 0, "Return hostname doesn't match lookup"); + + box.check(validate_hostname(x, (unsigned char *)"a.*.trafficserver.org", false, nullptr) == false, "Hostname shouldn't match"); +} + +REGRESSION_TEST(IDNA_hostnames)(RegressionTest *t, int /* atype ATS_UNUSED */, int *pstatus) +{ + TestBox box(t, pstatus); + char *matching; + box = REGRESSION_TEST_PASSED; + X509 *x = load_cert_from_string(test_certificate_cn_and_SANs); + box.check(x != NULL, "failed to load the test certificate"); + box.check(validate_hostname(x, (unsigned char *)"xn--foobar.trafficserver.org", false, &matching) == true, + "Hostname should match"); + box.check(strcmp("*.trafficserver.org", matching) == 0, "Return hostname doesn't match lookup"); + + // IDNA means wildcard must match full label + box.check(validate_hostname(x, (unsigned char *)"xn--foobar.trafficserver.net", false, &matching) == false, + "Hostname shouldn't match"); +} + +REGRESSION_TEST(middle_label_match)(RegressionTest *t, int /* atype ATS_UNUSED */, int *pstatus) +{ + TestBox box(t, pstatus); + char *matching; + box = REGRESSION_TEST_PASSED; + X509 *x = load_cert_from_string(test_certificate_cn_and_SANs); + box.check(x != NULL, "failed to load the test certificate"); + box.check(validate_hostname(x, (unsigned char *)"foosomething.trafficserver.com", false, &matching) == true, + "Hostname should match"); + box.check(strcmp("foo*.trafficserver.com", matching) == 0, "Return hostname doesn't match lookup"); + box.check(validate_hostname(x, (unsigned char *)"somethingbar.trafficserver.net", false, &matching) == true, + "Hostname should match"); + box.check(strcmp("*bar.trafficserver.net", matching) == 0, "Return hostname doesn't match lookup"); + + box.check(validate_hostname(x, (unsigned char *)"a.bar.trafficserver.net", false, nullptr) == false, "Hostname shouldn't match"); + box.check(validate_hostname(x, (unsigned char *)"foo.bar.trafficserver.net", false, nullptr) == false, + "Hostname shouldn't match"); +} + +int +main(int /* argc ATS_UNUSED */, const char ** /* argv ATS_UNUSED */) +{ + diags = new Diags(NULL, NULL, stdout); + res_track_memory = 1; + + SSL_library_init(); + ink_freelists_snap_baseline(); + + RegressionTest::run(); + ink_freelists_dump(stdout); + + return RegressionTest::final_status == REGRESSION_TEST_PASSED ? 0 : 1; +}