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;
+}