You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by jp...@apache.org on 2014/07/30 18:53:39 UTC

git commit: TS-2957: add new sslheaders plugin

Repository: trafficserver
Updated Branches:
  refs/heads/master c030977ab -> f1350fa0c


TS-2957: add new sslheaders plugin

The sslheaders plugin injects information about the SSL session
into the client request, the server request or both. It can be used
to transmit client SSL information (eg, SSL certificate, certificate
subject, etc) to origin servers.


Project: http://git-wip-us.apache.org/repos/asf/trafficserver/repo
Commit: http://git-wip-us.apache.org/repos/asf/trafficserver/commit/f1350fa0
Tree: http://git-wip-us.apache.org/repos/asf/trafficserver/tree/f1350fa0
Diff: http://git-wip-us.apache.org/repos/asf/trafficserver/diff/f1350fa0

Branch: refs/heads/master
Commit: f1350fa0ceb609fd70dc2c1a7807ef4e5e9aa3a4
Parents: c030977
Author: James Peach <jp...@apache.org>
Authored: Tue Jul 22 12:03:09 2014 -0700
Committer: James Peach <jp...@apache.org>
Committed: Wed Jul 30 09:45:27 2014 -0700

----------------------------------------------------------------------
 CHANGES                                         |   2 +
 configure.ac                                    |   4 +-
 doc/reference/plugins/index.en.rst              |   1 +
 doc/reference/plugins/sslheaders.en.rst         |  91 ++++++
 plugins/experimental/Makefile.am                |   1 +
 plugins/experimental/sslheaders/Makefile.am     |  36 +++
 plugins/experimental/sslheaders/expand.cc       | 134 ++++++++
 plugins/experimental/sslheaders/sslheaders.cc   | 309 +++++++++++++++++++
 plugins/experimental/sslheaders/sslheaders.h    |  97 ++++++
 .../experimental/sslheaders/test_sslheaders.cc  | 251 +++++++++++++++
 plugins/experimental/sslheaders/util.cc         |  86 ++++++
 11 files changed, 1011 insertions(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/trafficserver/blob/f1350fa0/CHANGES
----------------------------------------------------------------------
diff --git a/CHANGES b/CHANGES
index d21fc8a..45af9e8 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,6 +1,8 @@
                                                          -*- coding: utf-8 -*-
 Changes with Apache Traffic Server 5.1.0
 
+  *) [TS-2957] Add new sslheaders plugin.
+
   *) [TS-2802] Add SNI support for origin server connections.
    Author: Wei Sun <su...@yahoo-inc.com>
 

http://git-wip-us.apache.org/repos/asf/trafficserver/blob/f1350fa0/configure.ac
----------------------------------------------------------------------
diff --git a/configure.ac b/configure.ac
index 01383e4..124fafd 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1944,9 +1944,10 @@ AS_IF([test "x$enable_experimental_plugins" = xyes], [
     plugins/experimental/healthchecks/Makefile
     plugins/experimental/hipes/Makefile
     plugins/experimental/metalink/Makefile
-    plugins/experimental/remap_stats/Makefile
     plugins/experimental/regex_revalidate/Makefile
+    plugins/experimental/remap_stats/Makefile
     plugins/experimental/s3_auth/Makefile
+    plugins/experimental/sslheaders/Makefile
     plugins/experimental/stale_while_revalidate/Makefile
     plugins/experimental/ts_lua/Makefile
     plugins/experimental/url_sig/Makefile
@@ -2002,4 +2003,5 @@ AC_MSG_NOTICE([Build option summary:
     EXTRA_CXX_LDFLAGS:  $EXTRA_CXX_LDFLAGS
     LIBTOOL_LINK_FLAGS: $LIBTOOL_LINK_FLAGS
     API_DEFS:           $API_DEFS
+
 ])

http://git-wip-us.apache.org/repos/asf/trafficserver/blob/f1350fa0/doc/reference/plugins/index.en.rst
----------------------------------------------------------------------
diff --git a/doc/reference/plugins/index.en.rst b/doc/reference/plugins/index.en.rst
index eff4c5b..d056326 100644
--- a/doc/reference/plugins/index.en.rst
+++ b/doc/reference/plugins/index.en.rst
@@ -74,6 +74,7 @@ directory of the Apache Traffic Server source tree. Exmperimental plugins can be
   metalink.en
   mysql_remap.en
   s3_auth.en
+  sslheaders.en
   stale_while_revalidate.en
   ts_lua.en
   xdebug.en

http://git-wip-us.apache.org/repos/asf/trafficserver/blob/f1350fa0/doc/reference/plugins/sslheaders.en.rst
----------------------------------------------------------------------
diff --git a/doc/reference/plugins/sslheaders.en.rst b/doc/reference/plugins/sslheaders.en.rst
new file mode 100644
index 0000000..bab09c2
--- /dev/null
+++ b/doc/reference/plugins/sslheaders.en.rst
@@ -0,0 +1,91 @@
+.. _sslheaders-plugin:
+
+SSLHeaders Plugin
+*****************
+
+.. 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.
+
+The ``sslheaders`` plugins injects SSL session information into
+HTTP request headers. It can operate as a global plugin or as a
+remap plugin.
+
+Plugin Options
+--------------
+
+The following options may be specified when loading the plugin in
+:file:`plugin.config` or :file:`remap.config`:
+
+--attach=WHICH  This option specifies which HTTP request the SSL headers are
+                attached to.
+
+                ``client`` causes the headers to be injected into
+                the client request. This is primarily useful if another plugin
+                should inspect then. ``server`` is the default and injects the
+                headers into the origin server request. ``both`` injects the
+                headers into both the client request and the origin server
+                request.
+
+A list of `KEY=VALUE` pairs follows any options. The `KEY` names the HTTP
+header to inject, and `VALUE` names the SSL session field.
+
+======================  ===============================================
+SSL session field       Description
+======================  ===============================================
+client.certificate      The client certificate in PEM format
+client.subject          The client certificate subject DN
+client.issuer           The client certificate issuer DN
+client.serial           The client certificate serial number in hexadecimal format
+client.signature        The client certificate signature in hexadecimal format
+client.notbefore        The client certificate validity start time
+client.notafter         The client certificate validity end time
+server.certificate      The server certificate in PEM format
+server.subject          The server certificate subject DN
+server.issuer           The server certificate issuer DN
+server.serial           The server certificate serial number in hexadecimal format
+server.signature        The server certificate signature in hexadecimal format
+server.notbefore        The server certificate validity start time
+server.notafter         The server certificate validity end time
+======================  ===============================================
+
+The `client.certificate` and `server.certificate` fields emit
+the corresponding certificate in PEM format, with newline characters
+replaced by spaces.
+
+If the ``sslheaders`` plugin activtes on non-SSL connections, it
+will delete all the configured HTTP header names so that malicious
+clients cannot inject misleading information. If any of the SSL
+fields expand to an empty string, those headers are also deleted.
+
+Examples:
+---------
+
+In this example, the origin server is interested in the subject of
+the server certificate that was used to accept a client connetion.
+We can apply the ``sslheaders`` plugin to a generic remap rule to
+provide this information. The :file:`remap.config` configuration
+would be::
+
+  regex_map https://*.example.com/ http://origin.example.com/ \
+    @plugin=sslheaders.so @pparam=SSL-Server=server.subject
+
+In this example, we have set :ts:cv:`proxy.config.ssl.client.certification_level`
+to request SSL client certificates. We can then configure ``sslheaders``
+to populate the client certificate subject globally by adding it
+to :file:`plugin.config`::
+
+  sslheaders.so SSL-Client-ID=client.subject SSL-Client-NotBefore=client.notbefore SSL-Client-NotAfter-client.notafter

http://git-wip-us.apache.org/repos/asf/trafficserver/blob/f1350fa0/plugins/experimental/Makefile.am
----------------------------------------------------------------------
diff --git a/plugins/experimental/Makefile.am b/plugins/experimental/Makefile.am
index 225feea..947273b 100644
--- a/plugins/experimental/Makefile.am
+++ b/plugins/experimental/Makefile.am
@@ -31,6 +31,7 @@ SUBDIRS = \
  regex_revalidate \
  remap_stats \
  s3_auth \
+ sslheaders \
  stale_while_revalidate \
  url_sig \
  xdebug

http://git-wip-us.apache.org/repos/asf/trafficserver/blob/f1350fa0/plugins/experimental/sslheaders/Makefile.am
----------------------------------------------------------------------
diff --git a/plugins/experimental/sslheaders/Makefile.am b/plugins/experimental/sslheaders/Makefile.am
new file mode 100644
index 0000000..0372f4d
--- /dev/null
+++ b/plugins/experimental/sslheaders/Makefile.am
@@ -0,0 +1,36 @@
+#  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 $(top_srcdir)/build/plugins.mk
+
+AM_CPPFLAGS += -I$(top_srcdir)/lib
+
+noinst_LTLIBRARIES = libsslhdr.la
+pkglib_LTLIBRARIES = sslheaders.la
+check_PROGRAMS = test_sslheaders
+
+libsslhdr_la_SOURCES = sslheaders.h expand.cc util.cc
+
+sslheaders_la_SOURCES = sslheaders.cc sslheaders.h
+sslheaders_la_LDFLAGS = $(TS_PLUGIN_LDFLAGS)
+sslheaders_la_LIBADD = libsslhdr.la
+
+test_sslheaders_SOURCES = test_sslheaders.cc sslheaders.h
+test_sslheaders_LDADD = \
+  libsslhdr.la \
+  $(top_builddir)/lib/ts/libtsutil.la
+
+TESTS = $(check_PROGRAMS)

http://git-wip-us.apache.org/repos/asf/trafficserver/blob/f1350fa0/plugins/experimental/sslheaders/expand.cc
----------------------------------------------------------------------
diff --git a/plugins/experimental/sslheaders/expand.cc b/plugins/experimental/sslheaders/expand.cc
new file mode 100644
index 0000000..caaefb9
--- /dev/null
+++ b/plugins/experimental/sslheaders/expand.cc
@@ -0,0 +1,134 @@
+/*
+ * 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 "sslheaders.h"
+#include <ink_defs.h>
+
+#include <openssl/x509.h>
+#include <openssl/pem.h>
+
+typedef void (*x509_expansion)(X509 *, BIO *);
+
+static void
+x509_expand_none(X509 *, BIO *)
+{
+  // placeholder
+}
+
+static void
+x509_expand_certificate(X509 * x509, BIO * bio)
+{
+  long remain;
+  char * ptr;
+
+  PEM_write_bio_X509(bio, x509);
+
+  // The PEM format has newlines in it. mod_ssl replaces those with spaces.
+  remain = BIO_get_mem_data(bio, &ptr);
+  for (char * nl; (nl = (char *)memchr(ptr, '\n', remain)); ptr = nl) {
+    *nl = ' ';
+    remain -= nl - ptr;
+  }
+}
+
+static void
+x509_expand_subject(X509 * x509, BIO * bio)
+{
+  X509_NAME * name = X509_get_subject_name(x509);
+  X509_NAME_print_ex(bio, name, 0 /* indent */, XN_FLAG_ONELINE);
+}
+
+static void
+x509_expand_issuer(X509 * x509, BIO * bio)
+{
+  X509_NAME * name = X509_get_issuer_name(x509);
+  X509_NAME_print_ex(bio, name, 0 /* indent */, XN_FLAG_ONELINE);
+}
+
+static void
+x509_expand_serial(X509 * x509, BIO * bio)
+{
+  ASN1_INTEGER * serial = X509_get_serialNumber(x509);
+  i2a_ASN1_INTEGER(bio, serial);
+}
+
+static void
+x509_expand_signature(X509 * x509, BIO * bio)
+{
+  ASN1_BIT_STRING * sig = x509->signature;
+  const char * ptr = (const char *)sig->data;
+  const char * end = ptr + sig->length;
+
+  // The canonical OpenSSL way to format the signature seems to be
+  // X509_signature_dump(). However that separates each byte with a ':', which is
+  // human readable, but would be annoying to parse out of headers. We format as
+  // uppercase hex to match the serial number formatting.
+
+  for (; ptr < end; ++ptr) {
+    BIO_printf(bio, "%02X", (unsigned char)(*ptr));
+  }
+
+}
+
+static void
+x509_expand_notbefore(X509 * x509, BIO * bio)
+{
+  ASN1_TIME * time = X509_get_notBefore(x509);
+  ASN1_TIME_print(bio, time);
+}
+
+static void
+x509_expand_notafter(X509 * x509, BIO * bio)
+{
+  ASN1_TIME * time = X509_get_notAfter(x509);
+  ASN1_TIME_print(bio, time);
+}
+
+static const x509_expansion expansions[SSL_HEADERS_FIELD_MAX] =
+{
+  x509_expand_none,         // SSL_HEADERS_FIELD_NONE
+  x509_expand_certificate,  // SSL_HEADERS_FIELD_CERTIFICATE
+  x509_expand_subject,      // SSL_HEADERS_FIELD_SUBJECT
+  x509_expand_issuer,       // SSL_HEADERS_FIELD_ISSUER
+  x509_expand_serial,       // SSL_HEADERS_FIELD_SERIAL
+  x509_expand_signature,    // SSL_HEADERS_FIELD_SIGNATURE
+  x509_expand_notbefore,    // SSL_HEADERS_FIELD_NOTBEFORE
+  x509_expand_notafter,     // SSL_HEADERS_FIELD_NOTBEFORE
+};
+
+bool
+SslHdrExpandX509Field(BIO * bio, X509 * x509, ExpansionField field)
+{
+  // Rewind the BIO.
+  BIO_reset(bio);
+
+  if (field < countof(expansions)) {
+    expansions[field](x509, bio);
+  }
+
+#if 0
+  if (BIO_pending(bio)) {
+    long len;
+    char * ptr;
+    len = BIO_get_mem_data(bio, &ptr);
+    SslHdrDebug("X509 field %d: %.*s", (int)field, (int)len, ptr);
+  }
+#endif
+
+  return true;
+}

http://git-wip-us.apache.org/repos/asf/trafficserver/blob/f1350fa0/plugins/experimental/sslheaders/sslheaders.cc
----------------------------------------------------------------------
diff --git a/plugins/experimental/sslheaders/sslheaders.cc b/plugins/experimental/sslheaders/sslheaders.cc
new file mode 100644
index 0000000..0380e52
--- /dev/null
+++ b/plugins/experimental/sslheaders/sslheaders.cc
@@ -0,0 +1,309 @@
+/*
+ * 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 "sslheaders.h"
+#include <getopt.h>
+#include <openssl/ssl.h>
+#include <openssl/x509.h>
+
+static void SslHdrExpand(SSL *, const SslHdrInstance::expansion_list&, TSMBuffer, TSMLoc);
+
+static int
+SslHdrExpandRequestHook(TSCont cont, TSEvent event, void * edata)
+{
+  const SslHdrInstance * hdr;
+  TSHttpTxn txn;
+  TSMBuffer mbuf;
+  TSMLoc    mhdr;
+  SSL *     ssl;
+
+  txn = (TSHttpTxn)edata;
+  hdr = (const SslHdrInstance *)TSContDataGet(cont);
+  ssl = (SSL *)TSHttpSsnSSLConnectionGet(TSHttpTxnSsnGet(txn));
+
+  switch (event) {
+  case TS_EVENT_HTTP_READ_REQUEST_HDR:
+    if (TSHttpTxnClientReqGet(txn, &mbuf, &mhdr) != TS_SUCCESS) {
+      goto done;
+    }
+
+    break;
+  case TS_EVENT_HTTP_SEND_REQUEST_HDR:
+    if (TSHttpTxnServerReqGet(txn, &mbuf, &mhdr) != TS_SUCCESS) {
+      goto done;
+    }
+
+    // If we are only attaching to the client request, NULL the SSL context in order to
+    // nuke the SSL headers from the server request.
+    if (hdr->attach == SSL_HEADERS_ATTACH_CLIENT) {
+      ssl = NULL;
+    }
+
+    break;
+  default:
+    goto done;
+  }
+
+  SslHdrExpand(ssl, hdr->expansions, mbuf, mhdr);
+  TSHandleMLocRelease(mbuf, TS_NULL_MLOC, mhdr);
+
+done:
+  TSHttpTxnReenable(txn, TS_EVENT_HTTP_CONTINUE);
+  return TS_EVENT_NONE;
+}
+
+static void
+SslHdrRemoveHeader(TSMBuffer mbuf, TSMLoc mhdr, const std::string& name)
+{
+  TSMLoc field;
+  TSMLoc next;
+
+  field = TSMimeHdrFieldFind(mbuf, mhdr, name.c_str(), name.size());
+  for (; field != TS_NULL_MLOC; field = next) {
+    next = TSMimeHdrFieldNextDup(mbuf, mhdr, field);
+    TSMimeHdrFieldDestroy(mbuf, mhdr, field);
+    TSHandleMLocRelease(mbuf, mhdr, field);
+  }
+}
+
+static void
+SslHdrSetHeader(TSMBuffer mbuf, TSMLoc mhdr, const std::string& name, BIO * value)
+{
+  TSMLoc field;
+  long vlen;
+  char * vptr;
+
+  vlen = BIO_get_mem_data(value, &vptr);
+
+  SslHdrDebug("SSL header '%s'", name.c_str());
+
+  field = TSMimeHdrFieldFind(mbuf, mhdr, name.c_str(), name.size());
+  if (field == TS_NULL_MLOC) {
+    TSMimeHdrFieldCreateNamed(mbuf, mhdr, name.c_str(), name.size(), &field);
+    TSMimeHdrFieldValueStringSet(mbuf, mhdr, field, -1, vptr, vlen);
+    TSMimeHdrFieldAppend(mbuf, mhdr, field);
+    TSHandleMLocRelease(mbuf, mhdr, field);
+  } else {
+    TSMLoc next;
+
+    // Overwrite the first value.
+    TSMimeHdrFieldValueStringSet(mbuf, mhdr, field, -1, vptr, vlen);
+    next = TSMimeHdrFieldNextDup(mbuf, mhdr, field);
+    TSHandleMLocRelease(mbuf, mhdr, field);
+
+    for (field = next; field != TS_NULL_MLOC; field = next) {
+      next = TSMimeHdrFieldNextDup(mbuf, mhdr, field);
+      TSMimeHdrFieldDestroy(mbuf, mhdr, field);
+      TSHandleMLocRelease(mbuf, mhdr, field);
+    }
+  }
+}
+
+// Process SSL header expansions. If this is not an SSL connection, then we need to delete the SSL headers
+// so that malicious clients cannot inject bogus information. Otherwise, we populate the header with the
+// expanded value. If the value expands to something empty, we nuke the header.
+static void
+SslHdrExpand(SSL * ssl, const SslHdrInstance::expansion_list& expansions, TSMBuffer mbuf, TSMLoc mhdr)
+{
+  if (ssl == NULL) {
+    for (SslHdrInstance::expansion_list::const_iterator e = expansions.begin(); e != expansions.end(); ++e) {
+      SslHdrRemoveHeader(mbuf, mhdr, e->name);
+    }
+  } else {
+    X509 * x509;
+    BIO * exp = BIO_new(BIO_s_mem());
+
+    for (SslHdrInstance::expansion_list::const_iterator e = expansions.begin(); e != expansions.end(); ++e) {
+      switch (e->scope) {
+      case SSL_HEADERS_SCOPE_CLIENT:
+        x509 = SSL_get_peer_certificate(ssl);
+        break;
+      case SSL_HEADERS_SCOPE_SERVER:
+        x509 = SSL_get_certificate(ssl);
+        break;
+      default:
+        x509 = NULL;
+      }
+
+      if (x509 == NULL) {
+        continue;
+      }
+
+      SslHdrExpandX509Field(exp, x509, e->field);
+      if (BIO_pending(exp)) {
+        SslHdrSetHeader(mbuf, mhdr, e->name, exp);
+      } else {
+        SslHdrRemoveHeader(mbuf, mhdr, e->name);
+      }
+
+      // Getting the peer certificate takes a reference count, but the server certificate doesn't.
+      if (x509 && e->scope == SSL_HEADERS_SCOPE_CLIENT) {
+        X509_free(x509);
+      }
+    }
+
+    BIO_free(exp);
+  }
+}
+
+static SslHdrInstance *
+SslHdrParseOptions(int argc, const char ** argv)
+{
+  static const struct option longopt[] =
+  {
+    { const_cast<char *>("attach"), required_argument, 0, 'a' },
+    {0, 0, 0, 0 }
+  };
+
+  std::auto_ptr<SslHdrInstance> hdr(new SslHdrInstance());
+
+  // We might parse arguments multiple times if we are loaded as a global
+  // plugin and a remap plugin. Reset optind so that getopt_long() does the
+  // right thing (ie. work instead of crash).
+  optind = 0;
+
+  for (;;) {
+    int opt;
+
+    opt = getopt_long(argc, (char * const *)argv, "", longopt, NULL);
+    switch (opt) {
+    case 'a':
+      if (strcmp(optarg, "client") == 0) {
+        hdr->attach = SSL_HEADERS_ATTACH_CLIENT;
+      } else if (strcmp(optarg, "server") == 0) {
+        hdr->attach = SSL_HEADERS_ATTACH_SERVER;
+      } else if (strcmp(optarg, "both") == 0) {
+        hdr->attach = SSL_HEADERS_ATTACH_BOTH;
+      } else {
+        TSError("%s: invalid attach option '%s'", PLUGIN_NAME, optarg);
+        return NULL;
+      }
+
+      break;
+    }
+
+    if (opt == -1) {
+        break;
+    }
+  }
+
+  // Pick up the remaining options as SSL header expansions.
+  for (int i = optind; i < argc; ++i) {
+    SslHdrExpansion exp;
+    if (!SslHdrParseExpansion(argv[i], exp)) {
+      // If we fail, the expansion parsing logs the error.
+      return NULL;
+    }
+
+    hdr->expansions.push_back(exp);
+  }
+
+  return hdr.release();
+}
+
+void
+TSPluginInit(int argc, const char *argv[])
+{
+  TSPluginRegistrationInfo info;
+  SslHdrInstance * hdr;
+
+  info.plugin_name = (char *)"sslheaders";
+  info.vendor_name = (char *)"Apache Software Foundation";
+  info.support_email = (char *)"dev@trafficserver.apache.org";
+
+  if (TSPluginRegister(TS_SDK_VERSION_3_0, &info) != TS_SUCCESS) {
+    SslHdrError("plugin registration failed");
+  }
+
+  hdr = SslHdrParseOptions(argc, (const char **)argv);
+  if (hdr) {
+    switch (hdr->attach) {
+    case SSL_HEADERS_ATTACH_SERVER:
+      TSHttpHookAdd(TS_HTTP_SEND_REQUEST_HDR_HOOK, hdr->cont);
+      break;
+    case SSL_HEADERS_ATTACH_BOTH: /* fallthru */
+    case SSL_HEADERS_ATTACH_CLIENT:
+      TSHttpHookAdd(TS_HTTP_READ_REQUEST_HDR_HOOK, hdr->cont);
+      TSHttpHookAdd(TS_HTTP_SEND_REQUEST_HDR_HOOK, hdr->cont);
+      break;
+    }
+  }
+}
+
+TSReturnCode
+TSRemapInit(TSRemapInterface * /* api */, char * /* err */, int /* errsz */)
+{
+  return TS_SUCCESS;
+}
+
+TSReturnCode
+TSRemapNewInstance(int argc, char * argv[], void ** instance, char * /* err */, int /* errsz */)
+{
+  SslHdrInstance * hdr;
+
+  hdr = SslHdrParseOptions(argc, (const char **)argv);
+  if (hdr) {
+    *instance = hdr;
+    return TS_SUCCESS;
+  }
+
+  return TS_ERROR;
+}
+
+void
+TSRemapDeleteInstance(void * instance)
+{
+  SslHdrInstance * hdr = (SslHdrInstance *)instance;
+  delete hdr;
+}
+
+TSRemapStatus
+TSRemapDoRemap(void * instance, TSHttpTxn txn, TSRemapRequestInfo * rri)
+{
+  SslHdrInstance * hdr = (SslHdrInstance *)instance;
+
+  switch (hdr->attach) {
+  case SSL_HEADERS_ATTACH_SERVER:
+    TSHttpTxnHookAdd(txn, TS_HTTP_SEND_REQUEST_HDR_HOOK, hdr->cont);
+    break;
+  case SSL_HEADERS_ATTACH_BOTH: /* fallthru */
+  case SSL_HEADERS_ATTACH_CLIENT:
+    TSHttpTxnHookAdd(txn, TS_HTTP_READ_REQUEST_HDR_HOOK, hdr->cont);
+    TSHttpTxnHookAdd(txn, TS_HTTP_SEND_REQUEST_HDR_HOOK, hdr->cont);
+    break;
+  }
+
+  return TSREMAP_NO_REMAP;
+}
+
+SslHdrInstance::SslHdrInstance()
+  : expansions(), attach(SSL_HEADERS_ATTACH_SERVER), cont(TSContCreate(SslHdrExpandRequestHook, NULL))
+{
+  TSContDataSet(cont, this);
+}
+
+SslHdrInstance::~SslHdrInstance()
+{
+  TSContDestroy(cont);
+}
+
+void
+SslHdrInstance::register_hooks()
+{
+
+}

http://git-wip-us.apache.org/repos/asf/trafficserver/blob/f1350fa0/plugins/experimental/sslheaders/sslheaders.h
----------------------------------------------------------------------
diff --git a/plugins/experimental/sslheaders/sslheaders.h b/plugins/experimental/sslheaders/sslheaders.h
new file mode 100644
index 0000000..6b83226
--- /dev/null
+++ b/plugins/experimental/sslheaders/sslheaders.h
@@ -0,0 +1,97 @@
+/*
+ * 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 <ts/ts.h>
+#include <ts/remap.h>
+#include <string.h>
+#include <list>
+#include <string>
+
+extern "C" {
+typedef struct x509_st X509;
+typedef struct bio_st BIO;
+}
+
+#define PLUGIN_NAME "sslheaders"
+
+#define SslHdrDebug(fmt, ...) TSDebug(PLUGIN_NAME, "%s: " fmt, __func__, ##__VA_ARGS__)
+#define SslHdrError(fmt, ...) TSError(PLUGIN_NAME ": %s: " fmt, __func__, ##__VA_ARGS__)
+
+enum AttachOptions
+{
+  SSL_HEADERS_ATTACH_CLIENT,
+  SSL_HEADERS_ATTACH_SERVER,
+  SSL_HEADERS_ATTACH_BOTH,
+};
+
+enum ExpansionScope
+{
+  SSL_HEADERS_SCOPE_NONE = 0,
+  SSL_HEADERS_SCOPE_CLIENT, // Client certificate
+  SSL_HEADERS_SCOPE_SERVER, // Server certificate
+  SSL_HEADERS_SCOPE_SSL     // SSL connection
+};
+
+enum ExpansionField
+{
+  SSL_HEADERS_FIELD_NONE = 0,
+  SSL_HEADERS_FIELD_CERTIFICATE,  // Attach whole PEM certificate
+  SSL_HEADERS_FIELD_SUBJECT,      // Attach certificate subject
+  SSL_HEADERS_FIELD_ISSUER,       // Attach certificate issuer
+  SSL_HEADERS_FIELD_SERIAL,       // Attach certificate serial number
+  SSL_HEADERS_FIELD_SIGNATURE,    // Attach certificate signature
+  SSL_HEADERS_FIELD_NOTBEFORE,    // Attach certificate notBefore date
+  SSL_HEADERS_FIELD_NOTAFTER,     // Attach certificate notAfter date
+
+  SSL_HEADERS_FIELD_MAX
+};
+
+struct SslHdrExpansion
+{
+  SslHdrExpansion()
+    : name(), scope(SSL_HEADERS_SCOPE_NONE), field(SSL_HEADERS_FIELD_NONE) {
+  }
+
+  std::string     name; // HTTP header name
+  ExpansionScope  scope;
+  ExpansionField  field;
+
+private:
+  SslHdrExpansion& operator=(const SslHdrExpansion&);
+};
+
+struct SslHdrInstance
+{
+  typedef std::list<SslHdrExpansion> expansion_list;
+
+  SslHdrInstance();
+  ~SslHdrInstance();
+
+  expansion_list  expansions;
+  AttachOptions   attach;
+  TSCont          cont;
+
+  void register_hooks();
+
+private:
+  SslHdrInstance(const SslHdrInstance&);
+  SslHdrInstance& operator=(const SslHdrInstance&);
+};
+
+bool SslHdrParseExpansion(const char * spec, SslHdrExpansion& exp);
+bool SslHdrExpandX509Field(BIO * bio, X509 * ptr, ExpansionField field);

http://git-wip-us.apache.org/repos/asf/trafficserver/blob/f1350fa0/plugins/experimental/sslheaders/test_sslheaders.cc
----------------------------------------------------------------------
diff --git a/plugins/experimental/sslheaders/test_sslheaders.cc b/plugins/experimental/sslheaders/test_sslheaders.cc
new file mode 100644
index 0000000..0ba42e7
--- /dev/null
+++ b/plugins/experimental/sslheaders/test_sslheaders.cc
@@ -0,0 +1,251 @@
+/*
+ * 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 "sslheaders.h"
+#include <ts/TestBox.h>
+#include <stdio.h>
+#include <stdarg.h>
+#include <openssl/ssl.h>
+#include <openssl/bio.h>
+#include <openssl/pem.h>
+
+char *
+_TSstrdup(const char *str, int64_t length, const char *)
+{
+  if (length == -1) {
+    return strdup(str);
+  } else {
+    return strndup(str, length);
+  }
+}
+
+void
+_TSfree(void * ptr)
+{
+  free(ptr);
+}
+
+void
+TSDebug(const char * tag, const char * fmt, ...)
+{
+  va_list args;
+
+  fprintf(stderr, "%s", tag);
+  va_start(args, fmt);
+  vfprintf(stderr, fmt, args);
+  va_end(args);
+  fprintf(stderr, "\n");
+}
+
+void
+TSError(const char *fmt, ...)
+{
+  va_list args;
+
+  va_start(args, fmt);
+  vfprintf(stderr, fmt, args);
+  va_end(args);
+  fprintf(stderr, "\n");
+}
+
+REGRESSION_TEST(ParseExpansion)(RegressionTest * t, int /* atype ATS_UNUSED */, int * pstatus)
+{
+  TestBox box(t, pstatus);
+
+#define EXPECT_TRUE(expression, _name, _scope, _field) do { \
+  SslHdrExpansion exp; \
+  box.check(SslHdrParseExpansion(expression, exp) == true, "'%s' failed (expected success)", (expression)); \
+  box.check(strcmp(exp.name.c_str(), _name) == 0, "'%s' expected name %s, received %s", (expression), (_name), exp.name.c_str()); \
+  box.check(exp.scope == (_scope), "'%s' expected scope 0x%x (%s), received 0x%x", (expression), (_scope), #_scope, exp.scope); \
+  box.check(exp.field == (_field), "'%s' expected field 0x%x (%s), received 0x%x", (expression), (_field), #_field, exp.field); \
+} while (0)
+
+#define EXPECT_FALSE(expression) do { \
+  SslHdrExpansion exp; \
+  box.check(SslHdrParseExpansion(expression, exp) == false, "'%s' succeeded (expected failure)", (expression)); \
+} while (0)
+
+  box = REGRESSION_TEST_PASSED;
+
+  EXPECT_FALSE("");
+  EXPECT_FALSE("missing-certificate-selector");
+  EXPECT_FALSE("missing-field-selector=");
+  EXPECT_FALSE("missing-field-selector=client");
+  EXPECT_FALSE("missing-field-selector=client.");
+
+  EXPECT_TRUE("ssl-client-cert=client.certificate", "ssl-client-cert", SSL_HEADERS_SCOPE_CLIENT, SSL_HEADERS_FIELD_CERTIFICATE);
+  EXPECT_TRUE("ssl-server-signature=server.signature", "ssl-server-signature", SSL_HEADERS_SCOPE_SERVER, SSL_HEADERS_FIELD_SIGNATURE);
+
+  EXPECT_TRUE("certificate=server.certificate", "certificate", SSL_HEADERS_SCOPE_SERVER, SSL_HEADERS_FIELD_CERTIFICATE);
+  EXPECT_TRUE("subject=server.subject", "subject", SSL_HEADERS_SCOPE_SERVER, SSL_HEADERS_FIELD_SUBJECT);
+  EXPECT_TRUE("issuer=server.issuer", "issuer", SSL_HEADERS_SCOPE_SERVER, SSL_HEADERS_FIELD_ISSUER);
+  EXPECT_TRUE("serial=server.serial", "serial", SSL_HEADERS_SCOPE_SERVER, SSL_HEADERS_FIELD_SERIAL);
+  EXPECT_TRUE("signature=server.signature", "signature", SSL_HEADERS_SCOPE_SERVER, SSL_HEADERS_FIELD_SIGNATURE);
+  EXPECT_TRUE("notbefore=server.notbefore", "notbefore", SSL_HEADERS_SCOPE_SERVER, SSL_HEADERS_FIELD_NOTBEFORE);
+  EXPECT_TRUE("notafter=server.notafter", "notafter", SSL_HEADERS_SCOPE_SERVER, SSL_HEADERS_FIELD_NOTAFTER);
+
+#undef EXPECT_FALSE
+#undef EXPECT_TRUE
+}
+
+// Certificate:
+//     Data:
+//         Version: 3 (0x2)
+//         Serial Number: 16125629757001825863 (0xdfc9bed3a58ffe47)
+//     Signature Algorithm: sha1WithRSAEncryption
+//         Issuer: CN=test.sslheaders.trafficserver.apache.org
+//         Validity
+//             Not Before: Jul 23 17:51:08 2014 GMT
+//             Not After : May 12 17:51:08 2017 GMT
+//         Subject: CN=test.sslheaders.trafficserver.apache.org
+//         Subject Public Key Info:
+//             Public Key Algorithm: rsaEncryption
+//                 Public-Key: (1024 bit)
+//                 Modulus:
+//                     00:cd:ba:29:dc:57:9e:a2:30:0d:44:ed:2b:3d:06:
+//                     53:6f:46:65:1d:57:70:27:e5:2e:af:5c:73:ff:85:
+//                     74:95:4d:28:fe:de:8d:08:ed:eb:3f:da:7a:01:33:
+//                     b5:26:5d:64:c1:18:d8:dc:41:8c:c1:79:df:d0:22:
+//                     fa:8c:f6:9e:50:e0:1e:e4:28:54:db:d7:10:4e:97:
+//                     81:14:dc:b1:e5:f5:fc:f3:87:16:d9:30:07:36:30:
+//                     75:b9:5f:cf:9e:09:1e:8a:e8:80:6e:e6:c4:6e:2d:
+//                     33:ef:21:98:60:eb:7f:df:7e:13:49:4c:89:b2:5b:
+//                     6f:9e:1f:c8:2e:54:67:77:f1
+//                 Exponent: 65537 (0x10001)
+//         X509v3 extensions:
+//             X509v3 Subject Alternative Name:
+//                 DNS:test.sslheaders.trafficserver.apache.org
+//     Signature Algorithm: sha1WithRSAEncryption
+//          26:b2:1d:1c:39:7b:48:9e:8c:d9:22:80:b0:11:93:d6:91:5a:
+//          2c:b4:58:59:14:75:f7:e1:cb:08:e7:38:ac:44:1a:f7:d9:1a:
+//          43:50:3c:53:7e:d1:21:e4:ee:b0:26:f1:29:73:b4:e2:04:95:
+//          2b:f1:ff:2f:43:07:29:f8:21:e4:b0:d9:a5:3a:cd:98:99:51:
+//          23:e2:f5:2b:60:f3:fb:56:bf:d3:2f:39:25:3f:27:b0:87:68:
+//          79:16:b9:86:df:05:30:4d:0e:89:1f:a8:5b:6a:63:75:09:ec:
+//          f9:fe:eb:26:d2:d9:16:73:c2:64:a3:8a:74:fc:1a:09:44:df:
+//          42:51
+
+// Given a PEM formatted object, remove the newlines to get what we would
+// see in a HTTP header.
+static char *
+make_pem_header(const char * pem)
+{
+  char * hdr;
+  char * ptr;
+  unsigned remain;
+
+  hdr = ptr = strdup(pem);
+  remain = strlen(hdr);
+
+  for (char * nl; (nl = (char *)memchr(ptr, '\n', remain)); ptr = nl) {
+    *nl = ' ';
+    remain -= nl - ptr;
+  }
+
+  return hdr;
+}
+
+REGRESSION_TEST(ParseX509Fields)(RegressionTest * t, int /* atype ATS_UNUSED */, int * pstatus)
+{
+  // A self-signed certificate for CN=test.sslheaders.trafficserver.apache.org.
+  static const char * test_certificate =
+  "-----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"
+;
+#if 0
+  "-----BEGIN RSA PRIVATE KEY-----"
+  "MIICXgIBAAKBgQDNuincV56iMA1E7Ss9BlNvRmUdV3An5S6vXHP/hXSVTSj+3o0I"
+  "7es/2noBM7UmXWTBGNjcQYzBed/QIvqM9p5Q4B7kKFTb1xBOl4EU3LHl9fzzhxbZ"
+  "MAc2MHW5X8+eCR6K6IBu5sRuLTPvIZhg63/ffhNJTImyW2+eH8guVGd38QIDAQAB"
+  "AoGBAJLTO48DhbbxHndD4SkTe7aeAgpX3jbK7W/ARxVldNgdkpWb1gI6czxGO+7h"
+  "rXatDvx1NEi2C7QFvEN6w2CZnlCIEYLdC3JPA9qQXD66vHSVttNqwLHezm+tf3Ci"
+  "DgPoSWABHJbDc/TFHjeVDvzkGJ/x0E6CO8lMvvDRbzjcNRoBAkEA80ulSvbCpZHL"
+  "aTqMwB/djvEFyrlyDyD8WkJewkL2q7HRWimNTAU+AsYftzn9kVaIHcVC3x1T47bB"
+  "qP1yEn+eoQJBANh4TtlZOEX6ykm4KqrCQXzOU5sp3m0RmqzYGQ3g8+8X8VTHjduw"
+  "OoJ/vJo6peluh0JalDbdSkCHU0OiILYD51ECQEoEP3s46yq32ixfVaa1ixALn3l3"
+  "RY34uQ00l+N9v9GoPUqyzXvNNHpfkBKMH+pxauOzuY5rO7RRS0WAJY4fKUECQQCd"
+  "R6R6lTGm3tYVhAM0OJoeVUc3yM78Tjsk9IoXpGd4Q9wrriYrBbstUCQ3pv8fQRhz"
+  "pJ5l0pj9k5Vy4ZyEwwdRAkEA3WViCDYe+uxeXcJxqiRHFoGm7YvkqcpBk9UQaWiz"
+  "d9D304LUJ+dfMHNUmhBe/HKG35VU8dG5/0E9vkQyz99zCw=="
+  "-----END RSA PRIVATE KEY-----"
+  ;
+#endif
+
+  TestBox box(t, pstatus);
+
+  box = REGRESSION_TEST_PASSED;
+
+  BIO * exp = BIO_new(BIO_s_mem());
+  BIO * bio = BIO_new_mem_buf((void *)test_certificate, -1);
+  X509 * x509 = PEM_read_bio_X509(bio, NULL, 0, NULL);
+
+  box.check(x509 != NULL, "failed to load the test certificate");
+
+#define EXPECT_FIELD(_field, _value) do { \
+  long len; char * ptr; \
+  SslHdrExpandX509Field(exp, x509, _field); \
+  len = BIO_get_mem_data(exp, &ptr); \
+  box.check(strncmp(_value, ptr, len) == 0, "expected '%s' for %s, received '%.*s'", _value, #_field, (int)len, ptr); \
+} while (0)
+
+  // Munge the PEM certificate to what we expect in the HTTP header.
+  char * certhdr = make_pem_header(test_certificate);
+
+  EXPECT_FIELD(SSL_HEADERS_FIELD_NONE, "");
+  EXPECT_FIELD(SSL_HEADERS_FIELD_CERTIFICATE, certhdr);
+  EXPECT_FIELD(SSL_HEADERS_FIELD_SUBJECT, "CN = test.sslheaders.trafficserver.apache.org");
+  EXPECT_FIELD(SSL_HEADERS_FIELD_ISSUER, "CN = test.sslheaders.trafficserver.apache.org");
+  EXPECT_FIELD(SSL_HEADERS_FIELD_SERIAL, "DFC9BED3A58FFE47");
+  EXPECT_FIELD(SSL_HEADERS_FIELD_SIGNATURE,
+    "26B21D1C397B489E8CD92280B01193D6915A"
+    "2CB458591475F7E1CB08E738AC441AF7D91A"
+    "43503C537ED121E4EEB026F12973B4E20495"
+    "2BF1FF2F430729F821E4B0D9A53ACD989951"
+    "23E2F52B60F3FB56BFD32F39253F27B08768"
+    "7916B986DF05304D0E891FA85B6A637509EC"
+    "F9FEEB26D2D91673C264A38A74FC1A0944DF"
+    "4251");
+
+  EXPECT_FIELD(SSL_HEADERS_FIELD_NOTBEFORE, "Jul 23 17:51:08 2014 GMT");
+  EXPECT_FIELD(SSL_HEADERS_FIELD_NOTAFTER, "May 12 17:51:08 2017 GMT");
+
+#undef EXPECT_FIELD
+
+  BIO_free(exp);
+  BIO_free(bio);
+  free(certhdr);
+}
+
+int main(void)
+{
+  SSL_library_init();
+  RegressionTest::run();
+  return RegressionTest::final_status == REGRESSION_TEST_PASSED ? 0 : 1;
+}

http://git-wip-us.apache.org/repos/asf/trafficserver/blob/f1350fa0/plugins/experimental/sslheaders/util.cc
----------------------------------------------------------------------
diff --git a/plugins/experimental/sslheaders/util.cc b/plugins/experimental/sslheaders/util.cc
new file mode 100644
index 0000000..c25b512
--- /dev/null
+++ b/plugins/experimental/sslheaders/util.cc
@@ -0,0 +1,86 @@
+/*
+ * 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 "sslheaders.h"
+#include <memory>
+#include <ink_defs.h>
+
+// Count of fields (not including SSL_HEADERS_FIELD_NONE).
+#define NUMFIELDS (SSL_HEADERS_FIELD_MAX - 1)
+
+static const struct _f { const char * name; ExpansionField field; } fields[] = {
+  { "certificate", SSL_HEADERS_FIELD_CERTIFICATE },
+  { "subject", SSL_HEADERS_FIELD_SUBJECT },
+  { "issuer", SSL_HEADERS_FIELD_ISSUER },
+  { "serial", SSL_HEADERS_FIELD_SERIAL },
+  { "signature", SSL_HEADERS_FIELD_SIGNATURE },
+  { "notbefore", SSL_HEADERS_FIELD_NOTBEFORE },
+  { "notafter", SSL_HEADERS_FIELD_NOTAFTER },
+};
+
+// Static assert to guarantee the fields table is current.
+extern char assert_fields_are_populated[((sizeof(fields)/sizeof(fields[0])) - NUMFIELDS) == 0 ? 0 : -1];
+
+bool
+SslHdrParseExpansion(const char * spec, SslHdrExpansion& exp)
+{
+  const char * sep = strchr(spec, '=');
+  const char * selector;
+
+  // First, split on '=' to separate the header name from the SSL expansion.
+  sep = strchr(spec, '=');
+  if (sep == NULL) {
+    SslHdrError("%s: missing '=' in SSL header expansion '%s'", PLUGIN_NAME, spec);
+    return false;
+  }
+
+  exp.name = std::string(spec, sep - spec);
+  selector = sep + 1;
+
+  // Next, split on '.' to separate the certificate selector from the field selector.
+  sep = strchr(selector, '.');
+  if (sep == NULL) {
+    SslHdrError("%s: missing '.' in SSL header expansion '%s'", PLUGIN_NAME, spec);
+    return false;
+  }
+
+  if (strncmp(selector, "server.", 7) == 0) {
+    exp.scope = SSL_HEADERS_SCOPE_SERVER;
+  } else if (strncmp(selector, "client.", 7) == 0) {
+    exp.scope = SSL_HEADERS_SCOPE_CLIENT;
+  } else if (strncmp(selector, "ssl.", 4) == 0) {
+    exp.scope = SSL_HEADERS_SCOPE_SSL;
+    SslHdrError("%s: the SSL header expansion scope is not implemented: '%s'", PLUGIN_NAME, spec);
+    return false;
+  } else {
+    SslHdrError("%s: invalid SSL header expansion '%s'", PLUGIN_NAME, spec);
+    return false;
+  }
+
+  // Push sep to point to the field selector.
+  selector = sep + 1;
+  for (unsigned i = 0; i < countof(fields); ++i) {
+    if (strcmp(selector, fields[i].name) == 0) {
+      exp.field = fields[i].field;
+      return true;
+    }
+  }
+
+  SslHdrError("%s: invalid SSL certificate field selector '%s'", PLUGIN_NAME, spec);
+  return false;
+}