You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by us...@apache.org on 2015/05/16 16:22:54 UTC
trafficserver git commit: TS-3608: Add client side hostname
validation for SSL connection
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 <us...@apache.org>
Authored: Sat May 16 17:17:06 2015 +0300
Committer: Uri Shachar <us...@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;
+}