You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by so...@apache.org on 2017/11/30 19:36:31 UTC

[trafficserver] branch master updated: Created URI Signing Plugin.

This is an automated email from the ASF dual-hosted git repository.

sorber pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git


The following commit(s) were added to refs/heads/master by this push:
     new 9ced1ef  Created URI Signing Plugin.
9ced1ef is described below

commit 9ced1efbda356fd0c954e575a9379c57cee64f82
Author: Chris Lemmons <ch...@comcast.com>
AuthorDate: Fri Nov 17 23:32:10 2017 +0000

    Created URI Signing Plugin.
---
 build/common.m4                                |  15 ++
 configure.ac                                   |  29 +++
 plugins/Makefile.am                            |   4 +
 plugins/experimental/uri_signing/Makefile.inc  |  28 +++
 plugins/experimental/uri_signing/README.md     | 129 +++++++++++
 plugins/experimental/uri_signing/config.c      | 252 +++++++++++++++++++++
 plugins/experimental/uri_signing/config.h      |  31 +++
 plugins/experimental/uri_signing/cookie.c      |  87 ++++++++
 plugins/experimental/uri_signing/cookie.h      |  20 ++
 plugins/experimental/uri_signing/jwt.c         | 290 +++++++++++++++++++++++++
 plugins/experimental/uri_signing/jwt.h         |  41 ++++
 plugins/experimental/uri_signing/match.c       |  46 ++++
 plugins/experimental/uri_signing/match.h       |  21 ++
 plugins/experimental/uri_signing/parse.c       | 197 +++++++++++++++++
 plugins/experimental/uri_signing/parse.h       |  27 +++
 plugins/experimental/uri_signing/timing.c      |  22 ++
 plugins/experimental/uri_signing/timing.h      |  44 ++++
 plugins/experimental/uri_signing/uri_signing.c | 241 ++++++++++++++++++++
 plugins/experimental/uri_signing/uri_signing.h |  22 ++
 19 files changed, 1546 insertions(+)

diff --git a/build/common.m4 b/build/common.m4
index 0302ac4..d66b002 100644
--- a/build/common.m4
+++ b/build/common.m4
@@ -195,6 +195,21 @@ AC_DEFUN([TS_TRY_COMPILE_NO_WARNING],
  CFLAGS=$ats_save_CFLAGS
 ])
 
+dnl
+dnl TS_LINK_WITH_FLAGS_IFELSE(LDFLAGS, FUNCTION-BODY,
+dnl                           [ACTIONS-IF-LINKS], [ACTIONS-IF-LINK-FAILS])
+dnl
+dnl Tries a link test with the provided flags.
+dnl
+
+AC_DEFUN([TS_LINK_WITH_FLAGS_IFELSE],
+[ats_save_LDFLAGS=$LDFLAGS
+ LDFLAGS="$LDFLAGS $1"
+ AC_LINK_IFELSE([$2],[$3],[$4])
+ LDFLAGS=$ats_save_LDFLAGS
+])
+
+
 
 dnl Iteratively interpolate the contents of the second argument
 dnl until interpolation offers no new result. Then assign the
diff --git a/configure.ac b/configure.ac
index 6a9a4a8..eff7c32 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1255,6 +1255,35 @@ AC_CHECK_LIB([brotlienc],[BrotliEncoderCreateInstance],[AC_SUBST([LIB_BROTLIENC]
 AC_SUBST(has_brotli)
 AM_CONDITIONAL([HAS_BROTLI], [ test "x${has_brotli}" = "x1" ])
 
+#
+# Enable experimental/uri_singing plugin
+# This is here, instead of above, because it needs to know if PCRE is available.
+#
+AC_CHECK_HEADERS([jansson.h], [
+  AC_MSG_CHECKING([whether jansson is dynamic])
+  TS_LINK_WITH_FLAGS_IFELSE([-shared -fPIC -l:libjansson.a],[AC_LANG_PROGRAM(
+                            [#include <jansson.h>],
+                            [(void) json_object();])],
+                            [AC_MSG_RESULT([no]);  LIBJANSSON=-l:libjansson.a],
+                            [AC_MSG_RESULT([yes]); LIBJANSSON=-ljansson])
+  ],
+  [LIBJANSSON=])
+
+AC_CHECK_HEADERS([cjose/cjose.h], [
+  AC_MSG_CHECKING([whether cjose is dynamic])
+  TS_LINK_WITH_FLAGS_IFELSE([-shared -fPIC -l:libcjose.a],[AC_LANG_PROGRAM(
+                            [#include <cjose/cjose.h>],
+                            [(void) cjose_jws_import("", 0, NULL);])],
+                            [AC_MSG_RESULT([no]);  LIBCJOSE=-l:libcjose.a],
+                            [AC_MSG_RESULT([yes]); LIBCJOSE=-lcjose])
+  ],
+  [LIBCJOSE=])
+AC_CHECK_LIB([crypto],[HMAC],[has_libcrypto=1],[has_libcrypto=0])
+
+AM_CONDITIONAL([BUILD_URI_SIGNING_PLUGIN], [test ! -z "${LIBCJOSE}" -a ! -z "${LIBJANSSON}" -a "x${enable_pcre}" = "xyes" -a "x${has_libcrypto}" = "x1"])
+AC_SUBST([LIBCJOSE])
+AC_SUBST([LIBJANSSON])
+
 # Check for backtrace() support
 has_backtrace=0
 AC_CHECK_HEADERS([execinfo.h], [has_backtrace=1],[])
diff --git a/plugins/Makefile.am b/plugins/Makefile.am
index 5e0db4d..2120cc1 100644
--- a/plugins/Makefile.am
+++ b/plugins/Makefile.am
@@ -76,6 +76,10 @@ include experimental/stream_editor/Makefile.inc
 include experimental/ts_lua/Makefile.inc
 include experimental/url_sig/Makefile.inc
 
+if BUILD_URI_SIGNING_PLUGIN
+include experimental/uri_signing/Makefile.inc
+endif
+
 if BUILD_MEMCACHED_REMAP_PLUGIN
 include experimental/memcached_remap/Makefile.inc
 endif
diff --git a/plugins/experimental/uri_signing/Makefile.inc b/plugins/experimental/uri_signing/Makefile.inc
new file mode 100644
index 0000000..9499479
--- /dev/null
+++ b/plugins/experimental/uri_signing/Makefile.inc
@@ -0,0 +1,28 @@
+#  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.
+
+pkglib_LTLIBRARIES += experimental/uri_signing/uri_signing.la
+
+experimental_uri_signing_uri_signing_la_SOURCES = \
+  experimental/uri_signing/uri_signing.c          \
+  experimental/uri_signing/config.c               \
+  experimental/uri_signing/cookie.c               \
+  experimental/uri_signing/jwt.c                  \
+  experimental/uri_signing/match.c                \
+  experimental/uri_signing/parse.c                \
+  experimental/uri_signing/timing.c
+
+experimental_uri_signing_uri_signing_la_LIBADD = @LIBJANSSON@ @LIBCJOSE@ @LIBPCRE@ -lm -lcrypto
diff --git a/plugins/experimental/uri_signing/README.md b/plugins/experimental/uri_signing/README.md
new file mode 100644
index 0000000..7a90bfd
--- /dev/null
+++ b/plugins/experimental/uri_signing/README.md
@@ -0,0 +1,129 @@
+URI Signing Plugin
+==================
+
+This remap plugin implements the draft URI Signing protocol documented here:
+https://tools.ietf.org/html/draft-ietf-cdni-uri-signing-12 .
+
+It takes a single argument: the name of a config file that contains key information.
+
+**Nota bene:** Take care in ordering the plugins. In general, this plugin
+should be first on the remap line. This is for two reasons. First, if no valid
+token is present, it is probably not useful to continue processing the request
+in future plugins.  Second, and more importantly, the signature should be
+verified _before_ any other plugins modify the request. If another plugin drops
+or modifies the query string, the token might be missing entirely by the time
+this plugin gets the URI.
+
+Config
+------
+
+The config file should be a JSON object that maps issuer names to JWK-sets.
+Exactly one of these JWK-sets must have an additional member indicating the
+renewal key.
+
+    {
+      "Kabletown URI Authority": {
+        "renewal_kid": "Second Key",
+        "keys": [
+          {
+            "alg": "HS256",
+            "kid": "First Key",
+            "kty": "oct",
+            "k": "Kh_RkUMj-fzbD37qBnDf_3e_RvQ3RP9PaSmVEpE24AM"
+          },
+          {
+            "alg": "HS256",
+            "kid": "Second Key",
+            "kty": "oct",
+            "k": "fZBpDBNbk2GqhwoB_DGBAsBxqQZVix04rIoLJ7p_RlE"
+          }
+        ]
+      }
+    }
+
+If there is not precisely one renewal key, the plugin will not load.
+
+Although the `kid` and `alg` parameters are optional in JWKs generally, both
+members must be present in keys used for URI signing.
+
+Usage
+-----
+
+The URI signing plugin will block all requests that do not bear a valid JWT, as
+defined by the URI Signing protocol. Clients that do not present a valid JWT
+will receive a 403 Forbidden response, instead of receiving content.
+
+Tokens will be found in either of these places:
+
+  - A query parameter named `URISigningPackage`. The value must be the JWT.
+  - A cookie named `URISigningPackage`. The value of the cookie must be the JWT.
+
+Path parameters will not be searched for JWTs.
+
+### Supported Claims
+
+The following claims are understood:
+
+  - `iss`: Must be present. The issuer is used to locate the key for verification.
+  - `sub`: Validated last, after key verification. **Only `uri-regex` is supported!**
+  - `exp`: Expired tokens are not valid.
+  - `iat`: May be present, but is not validated.
+  - `cdniv`: Must be missing or 1.
+  - `cdnistt`: If present, must be 1.
+  - `cdniets`: If cdnistt is 1, this must be present and non-zero.
+
+### Unsupported Claims
+
+These claims are not supported. If they are present, the token will not validate:
+
+  - `aud`
+  - `nbf`
+  - `jti`
+
+In addition, the `sub` containers of `uri`, `uri-pattern`, and `uri-hash` are
+**not supported**.
+
+### Token Renewal
+
+If the `cdnistt` and `cdniets` claims are present, the token will be renewed.
+The new token will be returned via a `Set-Cookie` header as a session cookie.
+
+However, instead of setting the expiration to be `cdniets` seconds from the
+expiration of the previous cookie, it is set to `cdniets` seconds from the time
+it was validated. This is to prevent a crafty client from repeatedly renewing
+tokens in quick succession to create a super-token that lasts long into the
+future, thereby circumventing the intent of the `exp` claim.
+
+### JOSE Header
+
+The JOSE header of the JWT should contain a `kid` parameter. This is used to
+quickly select the key that was used to sign the token. If it is provided, only
+the key with a matching `kid` will be used for validation. Otherwise, all
+possible keys for that issuer must be tried, which is considerably more
+expensive.
+
+Building
+--------
+
+To build from source, you will need these libraries installed:
+
+  - [cjose](https://github.com/cisco/cjose)
+  - [jansson](https://github.com/akheron/jansson)
+  - pcre
+  - OpenSSL
+
+… as well as compiler toolchain.
+
+This builds in-tree with the rest of the ATS plugins. Of special note, however,
+are the first two libraries: cjose and jansson. These libraries are not
+currently used anywhere else, so they may not be installed.
+
+As of this writing, both libraries install a dynamic library and a static
+archive. However, by default, the static archive is not compiled with Position
+Independent Code. The build script will detect this and build a dynamic
+dependency on these libraries, so they will have to be distributed with the
+plugin.
+
+If you would like to statically link them, you will need to ensure that they are
+compiled with the `-fPIC` flag in their CFLAGs. If the archives have PIC, the
+build scripts will automatically statically link them.
diff --git a/plugins/experimental/uri_signing/config.c b/plugins/experimental/uri_signing/config.c
new file mode 100644
index 0000000..3d698c5
--- /dev/null
+++ b/plugins/experimental/uri_signing/config.c
@@ -0,0 +1,252 @@
+/*
+ * 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 "uri_signing.h"
+#include "config.h"
+#include "timing.h"
+
+#include <ts/ts.h>
+
+#include <cjose/cjose.h>
+#include <jansson.h>
+
+#include <string.h>
+#include <search.h>
+#include <errno.h>
+
+#define JSONError(err) PluginError("json-err: %s:%d:%d: %s", (err).source, (err).line, (err).column, (err).text)
+
+struct config {
+  struct hsearch_data *issuers;
+  cjose_jwk_t ***jwkis;
+  char **issuer_names;
+  struct signer signer;
+};
+
+cjose_jwk_t **
+find_keys(struct config *cfg, const char *issuer)
+{
+  ENTRY *entry;
+  if (!hsearch_r((ENTRY){.key = (char *)issuer}, FIND, &entry, cfg->issuers) || !entry) {
+    PluginDebug("Unable to locate any keys at %p for issuer %s in %p->%p", entry, issuer, cfg, cfg->issuers);
+    return NULL;
+  }
+  int n = 0;
+  for (cjose_jwk_t **jwks = entry->data; *jwks; ++jwks, ++n) {
+    ;
+  }
+  PluginDebug("Located %d keys for issuer %s in %p->%p", n, issuer, cfg, cfg->issuers);
+  return entry->data;
+}
+
+cjose_jwk_t *
+find_key_by_kid(struct config *cfg, const char *issuer, const char *kid)
+{
+  const char *this_kid;
+  cjose_jwk_t **jwkis = find_keys(cfg, issuer);
+  if (!jwkis) {
+    return NULL;
+  }
+  for (cjose_jwk_t **jwks = jwkis; *jwks; ++jwks) {
+    if ((this_kid = cjose_jwk_get_kid(*jwks, NULL)) && !strcmp(this_kid, kid)) {
+      return *jwks;
+    }
+  }
+  return NULL;
+}
+
+struct config *
+config_new(size_t n)
+{
+  PluginDebug("Creating new config object with size %ld", n);
+  struct config *cfg = malloc(sizeof *cfg);
+
+  cfg->issuers = calloc(1, sizeof *cfg->issuers);
+  if (!hcreate_r(n * 2, cfg->issuers)) {
+    PluginError("Unable to create config table (%d)!", errno);
+    free(cfg);
+    return NULL;
+  }
+  PluginDebug("Created table with size %d", cfg->issuers->size);
+
+  cfg->jwkis    = malloc((n + 1) * sizeof *cfg->jwkis);
+  cfg->jwkis[n] = NULL;
+
+  cfg->issuer_names    = malloc((n + 1) * sizeof *cfg->issuer_names);
+  cfg->issuer_names[n] = NULL;
+
+  cfg->signer.issuer = NULL;
+  cfg->signer.jwk    = NULL;
+  cfg->signer.alg    = NULL;
+
+  PluginDebug("New config object created at %p", cfg);
+  return cfg;
+}
+
+void
+config_delete(struct config *cfg)
+{
+  if (!cfg) {
+    return;
+  }
+  hdestroy_r(cfg->issuers);
+
+  for (cjose_jwk_t ***jwkis = cfg->jwkis; *jwkis; ++jwkis) {
+    for (cjose_jwk_t **jwks = *jwkis; *jwks; ++jwks) {
+      cjose_jwk_release(*jwks);
+    }
+    free(*jwkis);
+  }
+  free(cfg->jwkis);
+
+  for (char **name = cfg->issuer_names; *name; ++name) {
+    free(*name);
+  }
+  free(cfg->issuer_names);
+
+  if (cfg->signer.alg) {
+    free(cfg->signer.alg);
+  }
+  free(cfg);
+}
+
+cjose_jwk_t *
+load_jwk(json_t *obj, cjose_err *err)
+{
+  char *s = json_dumps(obj, JSON_COMPACT);
+  if (!s) {
+    PluginError("Failed to re-serialize JSON sub-object.");
+    return NULL;
+  }
+
+  cjose_jwk_t *jwk = cjose_jwk_import(s, strlen(s), err);
+  free(s);
+  return jwk;
+}
+
+struct config *
+read_config(const char *path)
+{
+  json_error_t err    = {0};
+  json_t *issuer_json = json_load_file(path, 0, &err);
+  if (!issuer_json) {
+    JSONError(err);
+    goto fail;
+  }
+
+  if (!json_is_object(issuer_json)) {
+    PluginError("Config file is not a valid JSON object");
+    goto issuer_fail;
+  }
+
+  size_t issuers_ct = json_object_size(issuer_json);
+  if (!issuers_ct) {
+    PluginError("Config file contains no issuers.");
+    goto issuer_fail;
+  }
+
+  struct config *cfg = config_new(issuers_ct);
+  if (!cfg) {
+    PluginError("Unable to allocate config.");
+    goto issuer_fail;
+  }
+
+  cjose_jwk_t ***jwkis = cfg->jwkis;
+  char **issuer        = cfg->issuer_names;
+  const char *json_issuer;
+  json_t *jwks;
+  json_object_foreach(issuer_json, json_issuer, jwks)
+  {
+    *issuer         = strdup(json_issuer);
+    json_t *key_ary = json_object_get(jwks, "keys");
+    if (!key_ary) {
+      PluginError("Failed to get keys member from jwk for issuer %s", *issuer);
+      *jwkis = NULL;
+      goto cfg_fail;
+    }
+    PluginDebug("Created table with size %d", cfg->issuers->size);
+
+    const char *renewal_kid  = NULL;
+    json_t *renewal_kid_json = json_object_get(jwks, "renewal_kid");
+    if (renewal_kid_json) {
+      renewal_kid = json_string_value(renewal_kid_json);
+    }
+
+    size_t jwks_ct     = json_array_size(key_ary);
+    cjose_jwk_t **jwks = (*jwkis++ = malloc((jwks_ct + 1) * sizeof *jwks));
+    PluginDebug("Created table with size %d", cfg->issuers->size);
+    if (!hsearch_r(((ENTRY){(char *)*issuer, jwks}), ENTER, &(ENTRY *){0}, cfg->issuers)) {
+      PluginDebug("Failed to store keys for issuer %s", *issuer);
+    } else {
+      PluginDebug("Stored keys for %s at %16p", *issuer, jwks);
+    }
+
+    json_t *jwk_obj;
+    cjose_err jwk_err = {0};
+    for (size_t idx = 0; (idx < jwks_ct) && (jwk_obj = json_array_get(key_ary, idx)); ++idx, ++jwks) {
+      if ((*jwks = load_jwk(jwk_obj, &jwk_err))) {
+        const char *kid = cjose_jwk_get_kid(*jwks, NULL);
+        PluginDebug("Stored jwk %ld for issuer %s, kid %s, cfg %p->%p", idx, *issuer, kid ? kid : "<no kid>", cfg, cfg->issuers);
+        if (renewal_kid && kid && !strcmp(kid, renewal_kid)) {
+          if (cfg->signer.issuer) {
+            PluginError("Cannot load multiple renewal keys for a single remap. iss:\"%s\", kid:\"%s\"; iss:\"%s\", kid:\"%s\"",
+                        cfg->signer.issuer, cjose_jwk_get_kid(cfg->signer.jwk, NULL), *issuer, kid);
+            goto cfg_fail;
+          } else {
+            cfg->signer.issuer = *issuer;
+            cfg->signer.jwk    = *jwks;
+
+            const char *jwk_alg = json_string_value(json_object_get(jwk_obj, "alg"));
+            if (!jwk_alg) {
+              PluginError("Cannot load JWK algorithm for renewal key.");
+              goto cfg_fail;
+            }
+            cfg->signer.alg = strdup(jwk_alg);
+          }
+        }
+      } else {
+        PluginError("Failed to load jwk %ld for issuer %s: %s", idx, *issuer, jwk_err.message);
+        goto cfg_fail;
+      }
+    }
+    *jwks = NULL;
+    ++issuer;
+  }
+  if (!cfg->signer.issuer) {
+    PluginError("Cannot load remap without signing key.");
+    goto cfg_fail;
+  }
+  json_decref(issuer_json);
+  PluginDebug("Loaded config file successfully.");
+  return cfg;
+cfg_fail:
+  config_delete(cfg);
+issuer_fail:
+  json_decref(issuer_json);
+fail:
+  return NULL;
+}
+
+struct signer *
+config_signer(struct config *cfg)
+{
+  if (!cfg) {
+    return NULL;
+  }
+  return &cfg->signer;
+}
diff --git a/plugins/experimental/uri_signing/config.h b/plugins/experimental/uri_signing/config.h
new file mode 100644
index 0000000..cfefcfa
--- /dev/null
+++ b/plugins/experimental/uri_signing/config.h
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+
+struct config;
+struct _cjose_jwk_int;
+struct signer {
+  char *issuer;
+  struct _cjose_jwk_int *jwk;
+  char *alg;
+};
+
+struct config *read_config(const char *path);
+void config_delete(struct config *g);
+struct signer *config_signer(struct config *);
+struct _cjose_jwk_int **find_keys(struct config *cfg, const char *issuer);
+struct _cjose_jwk_int *find_key_by_kid(struct config *cfg, const char *issuer, const char *kid);
diff --git a/plugins/experimental/uri_signing/cookie.c b/plugins/experimental/uri_signing/cookie.c
new file mode 100644
index 0000000..70dd1aa
--- /dev/null
+++ b/plugins/experimental/uri_signing/cookie.c
@@ -0,0 +1,87 @@
+/*
+ * 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 "cookie.h"
+#include "uri_signing.h"
+#include <ts/ts.h>
+#include <string.h>
+
+const char *
+next_cookie(const char *cookie, size_t *cookie_ct, const char **k, size_t *k_ct, const char **v, size_t *v_ct)
+{
+  if (!k || !k_ct || !v || !v_ct || !cookie_ct || !*cookie_ct) {
+    return NULL;
+  }
+  const char *end = cookie + *cookie_ct;
+
+  while (cookie != end && (*cookie == ' ' || *cookie == '\t' || *cookie == '\v')) {
+    ++cookie;
+  }
+
+  *k = cookie;
+
+  while (cookie != end && *cookie != '=' && *cookie != ';') {
+    ++cookie;
+  }
+
+  if (cookie == end || *cookie != '=') {
+    /* Cookies that don't have an equal are treated as values, not keys. */
+    *v    = *k;
+    *v_ct = cookie - *v;
+    *k    = NULL;
+    *k_ct = 0;
+    goto done;
+  }
+
+  *k_ct = cookie - *k;
+  ++cookie;
+  *v = cookie;
+
+  while (cookie != end && *cookie != ';') {
+    ++cookie;
+  }
+
+  *v_ct = cookie - *v;
+
+done:
+  PluginDebug("Checking next cookie with %ld bytes of key and %ld bytes of value", *k_ct, *v_ct);
+  if (cookie != end) {
+    ++cookie;
+  }
+  *cookie_ct = end - cookie;
+  return cookie;
+}
+
+const char *
+get_cookie_value(const char **cookie, size_t *cookie_ct, const char *key, size_t *ct)
+{
+  PluginDebug("Parsing cookie %.*s looking for %s", (int)*cookie_ct, *cookie, key);
+  const char *k, *v;
+  size_t k_ct, v_ct;
+  size_t key_ct = strlen(key);
+  while ((*cookie = next_cookie(*cookie, cookie_ct, &k, &k_ct, &v, &v_ct))) {
+    PluginDebug("Checking cookie '%.*s' '%.*s'", (int)k_ct, k, (int)v_ct, v);
+    if (key_ct == k_ct && (k_ct == 0 || !strncmp(k, key, k_ct))) {
+      PluginDebug("Found value for %s: (%p)%.*s", key, v, (int)v_ct, v);
+      *ct = v_ct;
+      return v;
+    }
+  }
+  *ct = 0;
+  return NULL;
+}
diff --git a/plugins/experimental/uri_signing/cookie.h b/plugins/experimental/uri_signing/cookie.h
new file mode 100644
index 0000000..a5d0f20
--- /dev/null
+++ b/plugins/experimental/uri_signing/cookie.h
@@ -0,0 +1,20 @@
+/*
+ * 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 <stddef.h>
+const char *get_cookie_value(const char **cookie, size_t *cookie_ct, const char *key, size_t *ct);
diff --git a/plugins/experimental/uri_signing/jwt.c b/plugins/experimental/uri_signing/jwt.c
new file mode 100644
index 0000000..e34a7a1
--- /dev/null
+++ b/plugins/experimental/uri_signing/jwt.c
@@ -0,0 +1,290 @@
+/*
+ * 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 "uri_signing.h"
+#include "jwt.h"
+#include "match.h"
+#include "ts/ts.h"
+#include <jansson.h>
+#include <cjose/cjose.h>
+#include <math.h>
+#include <time.h>
+#include <string.h>
+
+double
+parse_number(json_t *num)
+{
+  if (!json_is_number(num)) {
+    return NAN;
+  }
+  return json_number_value(num);
+}
+
+int
+parse_integer_default(json_t *num, int def)
+{
+  if (!json_is_integer(num)) {
+    return def;
+  }
+  return json_integer_value(num);
+}
+
+struct jwt *
+parse_jwt(json_t *raw)
+{
+  if (!raw) {
+    return NULL;
+  }
+
+  struct jwt *jwt = malloc(sizeof *jwt);
+  jwt->raw        = raw;
+  jwt->iss        = json_string_value(json_object_get(raw, "iss"));
+  jwt->sub        = json_string_value(json_object_get(raw, "sub"));
+  jwt->aud        = json_string_value(json_object_get(raw, "aud"));
+  jwt->exp        = parse_number(json_object_get(raw, "exp"));
+  jwt->nbf        = parse_number(json_object_get(raw, "nbf"));
+  jwt->iat        = parse_number(json_object_get(raw, "iat"));
+  jwt->jti        = json_string_value(json_object_get(raw, "jti"));
+  jwt->cdniv      = parse_integer_default(json_object_get(raw, "cdniv"), 1);
+  jwt->cdniets    = json_integer_value(json_object_get(raw, "cdniets"));
+  jwt->cdnistt    = json_integer_value(json_object_get(raw, "cdnistt"));
+  return jwt;
+}
+
+void
+jwt_delete(struct jwt *jwt)
+{
+  if (!jwt) {
+    return;
+  }
+  json_decref(jwt->raw);
+  free(jwt);
+}
+
+double
+now(void)
+{
+  struct timespec t;
+  if (!clock_gettime(CLOCK_REALTIME, &t)) {
+    return (double)t.tv_sec + 1.0e-9 * (double)t.tv_nsec;
+  }
+  return NAN;
+}
+
+bool
+unsupported_string_claim(const char *str)
+{
+  return !str;
+}
+
+bool
+unsupported_date_claim(double t)
+{
+  return isnan(t);
+}
+
+bool
+jwt_validate(struct jwt *jwt)
+{
+  if (!jwt) {
+    PluginDebug("Initial JWT Failure: NULL argument");
+    return false;
+  }
+
+  if (jwt->cdniv != 1) { /* Only support the very first version! */
+    PluginDebug("Initial JWT Failure: wrong version");
+    return false;
+  }
+
+  if (!jwt->sub) { /* Mandatory claim. Will be validated after key verification. */
+    PluginDebug("Initial JWT Failure: missing sub");
+    return false;
+  }
+
+  if (!unsupported_string_claim(jwt->aud)) {
+    PluginDebug("Initial JWT Failure: missing sub");
+    return false;
+  }
+
+  if (now() > jwt->exp) {
+    PluginDebug("Initial JWT Failure: expired token");
+    return false;
+  }
+
+  if (!unsupported_date_claim(jwt->nbf)) {
+    PluginDebug("Initial JWT Failure: nbf unsupported");
+    return false;
+  }
+
+  if (!unsupported_string_claim(jwt->jti)) {
+    PluginDebug("Initial JWT Failure: nonse unsupported");
+    return false;
+  }
+
+  if (jwt->cdnistt < 0 || jwt->cdnistt > 1) {
+    PluginDebug("Initial JWT Failure: unsupported value for cdnistt: %d", jwt->cdnistt);
+    return false;
+  }
+
+  return true;
+}
+
+bool
+jwt_check_uri(struct jwt *jwt, const char *uri)
+{
+  static const char CONT_URI_STR[]         = "uri";
+  static const char CONT_URI_PATTERN_STR[] = "uri-pattern";
+  static const char CONT_URI_REGEX_STR[]   = "uri-regex";
+
+  if (!jwt || !uri) {
+    return false;
+  }
+
+  const char *kind = jwt->sub, *container = jwt->sub;
+  while (*container && *container != ':') {
+    ++container;
+  }
+  if (!*container) {
+    return false;
+  }
+  ++container;
+
+  size_t len = container - kind;
+  PluginDebug("Comparing with match kind \"%.*s\" on \"%s\" to \"%s\"", (int)len - 1, kind, container, uri);
+  switch (len) {
+  case sizeof CONT_URI_STR:
+    if (!strncmp(CONT_URI_STR, kind, len - 1)) {
+      return !strcmp(container, uri);
+    }
+    PluginDebug("Expected kind %s, but did not find it in \"%.*s\"", CONT_URI_STR, (int)len - 1, kind);
+    break;
+  case sizeof CONT_URI_PATTERN_STR:
+    if (!strncmp(CONT_URI_PATTERN_STR, kind, len - 1)) {
+      return match_glob(container, uri);
+    }
+    PluginDebug("Expected kind %s, but did not find it in \"%.*s\"", CONT_URI_PATTERN_STR, (int)len - 1, kind);
+    break;
+  case sizeof CONT_URI_REGEX_STR:
+    if (!strncmp(CONT_URI_REGEX_STR, kind, len - 1)) {
+      return match_regex(container, uri);
+    }
+    PluginDebug("Expected kind %s, but did not find it in \"%.*s\"", CONT_URI_REGEX_STR, (int)len - 1, kind);
+    break;
+  }
+  PluginDebug("Unknown match kind \"%.*s\"", (int)len - 1, kind);
+  return false;
+}
+
+void
+renew_copy_string(json_t *new_json, const char *name, const char *old)
+{
+  if (old) {
+    json_object_set_new(new_json, name, json_string(old));
+  }
+}
+
+void
+renew_copy_real(json_t *new_json, const char *name, double old)
+{
+  if (!isnan(old)) {
+    json_object_set_new(new_json, name, json_real(old));
+  }
+}
+
+void
+renew_copy_integer(json_t *new_json, const char *name, double old)
+{
+  /* Integers have no sentinel value and cannot be missing. */
+  json_object_set_new(new_json, name, json_integer(old));
+}
+
+char *
+renew(struct jwt *jwt, const char *iss, cjose_jwk_t *jwk, const char *alg, const char *package)
+{
+  char *s = NULL;
+  if (jwt->cdnistt != 1) {
+    PluginDebug("Not renewing jwt, cdnistt != 1");
+    return NULL;
+  }
+
+  if (jwt->cdniets == 0) {
+    PluginDebug("Not renewing jwt, cdniets == 0");
+    return NULL;
+  }
+
+  json_t *new_json = json_object();
+  renew_copy_string(new_json, "iss", iss); /* use issuer of new signing key */
+  renew_copy_string(new_json, "sub", jwt->sub);
+  renew_copy_string(new_json, "aud", jwt->aud);
+  renew_copy_real(new_json, "exp", now() + jwt->cdniets); /* expire ets seconds hence */
+  renew_copy_real(new_json, "nbf", jwt->nbf);
+  renew_copy_real(new_json, "iat", now()); /* issued now */
+  renew_copy_string(new_json, "jti", jwt->jti);
+  renew_copy_integer(new_json, "cdniv", jwt->cdniv);
+  renew_copy_integer(new_json, "cdniets", jwt->cdniets);
+  renew_copy_integer(new_json, "cdnistt", jwt->cdnistt);
+
+  char *pt = json_dumps(new_json, JSON_COMPACT);
+
+  cjose_header_t *hdr = cjose_header_new(NULL);
+  if (!hdr) {
+    PluginDebug("Unable to create new jose header.");
+    goto fail_json;
+  }
+
+  cjose_err err;
+  const char *kid = cjose_jwk_get_kid(jwk, &err);
+  if (!kid) {
+    PluginDebug("Unable to get kid from signing key: %s", err.message);
+    goto fail_hdr;
+  }
+  if (!cjose_header_set(hdr, CJOSE_HDR_KID, kid, &err)) {
+    PluginDebug("Unable to set kid of jose header to %s: %s", kid, err.message);
+    goto fail_hdr;
+  }
+  if (!cjose_header_set(hdr, "alg", alg, &err)) {
+    PluginDebug("Unable to set alg of jose header to %s: %s", alg, err.message);
+    goto fail_hdr;
+  }
+
+  cjose_jws_t *jws = cjose_jws_sign(jwk, hdr, (uint8_t *)pt, strlen(pt), &err);
+  if (!jws) {
+    char *hdr_str = json_dumps((json_t *)hdr, JSON_COMPACT);
+    PluginDebug("Unable to sign new key: %s. {%p(%s), \"%s\", \"%s\"}", err.message, jwk, kid, hdr_str, pt);
+    free(hdr_str);
+    goto fail_hdr;
+  }
+
+  const char *jws_str;
+  if (!cjose_jws_export(jws, &jws_str, &err)) {
+    PluginDebug("Unable to export jws: %s", err.message);
+    goto fail_jws;
+  }
+
+  const char *fmt = "%s=%s";
+  size_t s_ct;
+  s = malloc(s_ct = (1 + snprintf(NULL, 0, fmt, package, jws_str)));
+  snprintf(s, s_ct, fmt, package, jws_str);
+fail_jws:
+  cjose_jws_release(jws);
+fail_hdr:
+  cjose_header_release(hdr);
+fail_json:
+  free(pt);
+  return s;
+}
diff --git a/plugins/experimental/uri_signing/jwt.h b/plugins/experimental/uri_signing/jwt.h
new file mode 100644
index 0000000..bfe1f5f
--- /dev/null
+++ b/plugins/experimental/uri_signing/jwt.h
@@ -0,0 +1,41 @@
+/*
+ * 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 <stdbool.h>
+#include <jansson.h>
+
+struct jwt {
+  json_t *raw;
+  const char *iss;
+  const char *sub;
+  const char *aud;
+  double exp;
+  double nbf;
+  double iat;
+  const char *jti;
+  int cdniv;
+  int cdniets;
+  int cdnistt;
+};
+struct jwt *parse_jwt(json_t *raw);
+void jwt_delete(struct jwt *jwt);
+bool jwt_validate(struct jwt *jwt);
+bool jwt_check_uri(struct jwt *jwt, const char *uri);
+
+struct _cjose_jwk_int;
+char *renew(struct jwt *jwt, const char *iss, struct _cjose_jwk_int *jwk, const char *alg, const char *package);
diff --git a/plugins/experimental/uri_signing/match.c b/plugins/experimental/uri_signing/match.c
new file mode 100644
index 0000000..ad376a2
--- /dev/null
+++ b/plugins/experimental/uri_signing/match.c
@@ -0,0 +1,46 @@
+/*
+ * 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 "uri_signing.h"
+#include "ts/ts.h"
+#include <stdbool.h>
+#include <pcre.h>
+#include <string.h>
+
+bool
+match_glob(const char *needle, const char *haystack)
+{
+  return false;
+}
+
+bool
+match_regex(const char *pattern, const char *uri)
+{
+  const char *err;
+  int err_off;
+  PluginDebug("Testing regex pattern /%s/ against \"%s\"", pattern, uri);
+  pcre *re = pcre_compile(pattern, PCRE_ANCHORED | PCRE_UCP | PCRE_UTF8, &err, &err_off, NULL);
+  if (!re) {
+    PluginDebug("Regex /%s/ failed to compile.", pattern);
+    return false;
+  }
+
+  int rc = pcre_exec(re, NULL, uri, strlen(uri), 0, 0, NULL, 0);
+  pcre_free(re);
+  return rc >= 0;
+}
diff --git a/plugins/experimental/uri_signing/match.h b/plugins/experimental/uri_signing/match.h
new file mode 100644
index 0000000..92b906d
--- /dev/null
+++ b/plugins/experimental/uri_signing/match.h
@@ -0,0 +1,21 @@
+/*
+ * 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 <stdbool.h>
+bool match_glob(const char *needle, const char *haystack);
+bool match_regex(const char *pattern, const char *uri);
diff --git a/plugins/experimental/uri_signing/parse.c b/plugins/experimental/uri_signing/parse.c
new file mode 100644
index 0000000..cd8d7e8
--- /dev/null
+++ b/plugins/experimental/uri_signing/parse.c
@@ -0,0 +1,197 @@
+/*
+ * 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 "uri_signing.h"
+#include "parse.h"
+#include "config.h"
+#include "jwt.h"
+#include "cookie.h"
+#include "timing.h"
+#include <cjose/cjose.h>
+#include <jansson.h>
+#include <string.h>
+#include <ts/ts.h>
+#include <inttypes.h>
+
+cjose_jws_t *
+get_jws_from_query(const char *uri, size_t uri_ct, const char *paramName)
+{
+  PluginDebug("Parsing JWS from query string: %.*s", (int)uri_ct, uri);
+  const char *query = uri;
+  const char *end   = uri + uri_ct;
+  while (query != end && *query != '?') {
+    ++query;
+  }
+  if (query == end) {
+    return NULL;
+  }
+
+  ++query;
+
+  const char *key   = query, *key_end;
+  const char *value = query, *value_end;
+  for (;;) {
+    while (value != end && *value != '=') {
+      ++value;
+    }
+
+    if (value == end) {
+      break;
+    }
+    key_end   = value;
+    value_end = ++value;
+    while (value_end != end && *value_end != '&') {
+      ++value_end;
+    }
+
+    if (!strncmp(paramName, key, (size_t)(key_end - key))) {
+      PluginDebug("Decoding JWS: %.*s", (int)(key_end - key), key);
+      cjose_err err    = {0};
+      cjose_jws_t *jws = cjose_jws_import(value, (size_t)(value_end - value), &err);
+      if (!jws) {
+        PluginDebug("Unable to read JWS: %.*s, %s", (int)(key_end - key), key, err.message ? err.message : "");
+      } else {
+        PluginDebug("Parsed JWS: %.*s (%16p)", (int)(key_end - key), key, jws);
+      }
+      return jws;
+    }
+
+    if (value_end == end) {
+      break;
+    }
+
+    key = value = value_end + 1;
+  }
+  PluginDebug("Unable to locate signing key in uri: %.*s", (int)uri_ct, uri);
+  return NULL;
+}
+
+cjose_jws_t *
+get_jws_from_cookie(const char **cookie, size_t *cookie_ct, const char *paramName)
+{
+  PluginDebug("Parsing JWS from cookie: %.*s", (int)*cookie_ct, *cookie);
+  size_t value_ct;
+  const char *value = get_cookie_value(cookie, cookie_ct, paramName, &value_ct);
+  PluginDebug("Got jws string: (%p) %.*s", value, (int)value_ct, value);
+  if (!value || !value_ct) {
+    return NULL;
+  }
+  cjose_err err    = {0};
+  cjose_jws_t *jws = cjose_jws_import(value, value_ct, &err);
+  if (!jws) {
+    PluginDebug("Unable to read JWS: %.*s, %s", (int)value_ct, value, err.message ? err.message : "");
+  } else {
+    PluginDebug("Parsed JWS: %.*s (%16p)", (int)value_ct, value, jws);
+  }
+  return jws;
+}
+
+struct jwt *
+validate_jws(cjose_jws_t *jws, struct config *cfg, const char *uri, size_t uri_ct)
+{
+  struct timer t;
+  int64_t last_mark = 0;
+  start_timer(&t);
+
+#define TimerDebug(msg)                                             \
+  do {                                                              \
+    int64_t new_mark = mark_timer(&t);                              \
+    PluginDebug("Spent %" PRId64 " ns " msg, new_mark - last_mark); \
+    last_mark = new_mark;                                           \
+  } while (0)
+
+  PluginDebug("Validating JWS for %16p", jws);
+  cjose_err cerr = {0};
+  size_t pt_ct;
+  const char *pt;
+  if (!cjose_jws_get_plaintext(jws, (uint8_t **)&pt, &pt_ct, &cerr)) {
+    PluginDebug("Cannot get plaintext for %16p", jws);
+    return false;
+  }
+
+  TimerDebug("getting jws plaintext");
+
+  json_error_t jerr = {0};
+  struct jwt *jwt   = parse_jwt(json_loadb(pt, pt_ct, 0, &jerr));
+  TimerDebug("parsing jwt");
+  if (!jwt) {
+    if (jerr.text[0]) {
+      PluginDebug("Cannot parse json for %16p: %.*s '%s'", jws, (int)pt_ct, pt, jerr.text);
+    } else {
+      PluginDebug("Cannot parse jwt for %16p: %.*s", jws, (int)pt_ct, pt);
+    }
+    return NULL;
+  }
+
+  if (!jwt_validate(jwt)) {
+    PluginDebug("Initial validation of JWT failed for %16p", jws);
+    goto jwt_fail;
+  }
+  TimerDebug("inital validation of jwt");
+
+  cjose_header_t *hdr = cjose_jws_get_protected(jws);
+  TimerDebug("getting header of jws");
+  if (!hdr) {
+    PluginDebug("Cannot get protected header for %16p", jws);
+    goto jwt_fail;
+  }
+
+  const char *kid = cjose_header_get(hdr, "kid", NULL);
+  TimerDebug("getting kid of jws header");
+  if (kid) {
+    cjose_jwk_t *jwk = find_key_by_kid(cfg, jwt->iss, kid);
+    TimerDebug("finding key for jwt");
+    if (!jwk) {
+      PluginDebug("Cannot find key %s for issuer %s for %16p", kid, jwt->iss, jws);
+      goto jwt_fail;
+    }
+    if (!cjose_jws_verify(jws, jwk, NULL)) {
+      PluginDebug("Key %s for issuer %s for %16p does not validate.", kid, jwt->iss, jws);
+      goto jwt_fail;
+    }
+    TimerDebug("checking crypto signature for jwt");
+  } else {
+    PluginDebug("Searching all keys for issuer %s for %16p", jwt->iss, jws);
+    cjose_jwk_t **jwks;
+    for (jwks = find_keys(cfg, jwt->iss); jwks && *jwks; ++jwks) {
+      if (cjose_jws_verify(jws, *jwks, NULL)) {
+        break;
+      }
+    }
+    TimerDebug("checking the crypto signature of all possible keys for jwt");
+    if (!jwks || !*jwks) {
+      if (!jwks) {
+        PluginDebug("No keys found for issuer %s for %16p.", jwt->iss, jws);
+      } else {
+        PluginDebug("No valid key for issuer %s found for %16p", jwt->iss, jws);
+      }
+      goto jwt_fail;
+    }
+  }
+
+  if (!jwt_check_uri(jwt, uri)) {
+    PluginDebug("Valid key for %16p that does not match uri.", jws);
+    goto jwt_fail;
+  }
+  TimerDebug("verifying sub claim");
+
+  return jwt;
+jwt_fail:
+  jwt_delete(jwt);
+  return NULL;
+}
diff --git a/plugins/experimental/uri_signing/parse.h b/plugins/experimental/uri_signing/parse.h
new file mode 100644
index 0000000..8002f87
--- /dev/null
+++ b/plugins/experimental/uri_signing/parse.h
@@ -0,0 +1,27 @@
+/*
+ * 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 <stdlib.h>
+
+struct _cjose_jws_int;
+struct _cjose_jws_int *get_jws_from_query(const char *uri, size_t uri_ct, const char *paramName);
+struct _cjose_jws_int *get_jws_from_cookie(const char **cookie, size_t *cookie_ct, const char *paramName);
+
+struct config;
+struct jwt;
+struct jwt *validate_jws(struct _cjose_jws_int *jws, struct config *cfg, const char *uri, size_t uri_ct);
diff --git a/plugins/experimental/uri_signing/timing.c b/plugins/experimental/uri_signing/timing.c
new file mode 100644
index 0000000..8158cf2
--- /dev/null
+++ b/plugins/experimental/uri_signing/timing.c
@@ -0,0 +1,22 @@
+/*
+ * 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 "timing.h"
+
+extern inline void start_timer(struct timer *);
+extern inline int64_t mark_timer(struct timer *);
diff --git a/plugins/experimental/uri_signing/timing.h b/plugins/experimental/uri_signing/timing.h
new file mode 100644
index 0000000..9511bcd
--- /dev/null
+++ b/plugins/experimental/uri_signing/timing.h
@@ -0,0 +1,44 @@
+/*
+ * 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 <stdint.h>
+#include <time.h>
+
+struct timer {
+  int started;
+  struct timespec start;
+};
+
+inline void
+start_timer(struct timer *t)
+{
+  t->started = !clock_gettime(CLOCK_THREAD_CPUTIME_ID, &t->start);
+}
+
+inline int64_t
+mark_timer(struct timer *t)
+{
+  struct timespec now;
+  if (!t->started) {
+    return 0;
+  }
+  if (clock_gettime(CLOCK_THREAD_CPUTIME_ID, &now)) {
+    return 0;
+  }
+  return (now.tv_sec - t->start.tv_sec) * (int64_t)1000000000 - (int64_t)t->start.tv_nsec + (int64_t)now.tv_nsec;
+}
diff --git a/plugins/experimental/uri_signing/uri_signing.c b/plugins/experimental/uri_signing/uri_signing.c
new file mode 100644
index 0000000..a33f03b
--- /dev/null
+++ b/plugins/experimental/uri_signing/uri_signing.c
@@ -0,0 +1,241 @@
+/*
+ * 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 "uri_signing.h"
+#include "config.h"
+#include "parse.h"
+#include "jwt.h"
+#include "timing.h"
+
+#include <ts/ts.h>
+#include <ts/remap.h>
+
+#include <stdio.h>
+#include <string.h>
+#include <inttypes.h>
+
+#include <cjose/cjose.h>
+
+/* Plugin registration. */
+TSReturnCode
+TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size)
+{
+  if (!api_info) {
+    strncpy(errbuf, "[tsremap_init] - Invalid TSRemapInterface argument", (size_t)(errbuf_size - 1));
+    return TS_ERROR;
+  }
+
+  if (api_info->tsremap_version < TSREMAP_VERSION) {
+    snprintf(errbuf, errbuf_size - 1, "[TSRemapInit] - Incorrect API version %ld.%ld", api_info->tsremap_version >> 16,
+             (api_info->tsremap_version & 0xffff));
+    return TS_ERROR;
+  }
+
+  TSDebug(PLUGIN_NAME, "plugin is succesfully initialized");
+  return TS_SUCCESS;
+}
+
+/* Create a new remap instance. *ih is passed to DoRemap and DeleteInstance. */
+TSReturnCode
+TSRemapNewInstance(int argc, char *argv[], void **ih, char *errbuf, int errbuf_size)
+{
+  if (argc != 3) {
+    snprintf(errbuf, errbuf_size - 1,
+             "[TSRemapNewKeyInstance] - Argument count wrong (%d)... Need exactly two pparam= (config file name).", argc);
+    return TS_ERROR;
+  }
+
+  TSDebug(PLUGIN_NAME, "Initializing remap function of %s -> %s with config from %s", argv[0], argv[1], argv[2]);
+
+  const char *install_dir = TSInstallDirGet();
+  size_t config_file_ct   = snprintf(NULL, 0, "%s/%s/%s", install_dir, "etc/trafficserver", argv[2]);
+  char *config_file       = malloc(config_file_ct + 1);
+  (void)snprintf(config_file, config_file_ct + 1, "%s/%s/%s", install_dir, "etc/trafficserver", argv[2]);
+  TSDebug(PLUGIN_NAME, "config file name: %s", config_file);
+  struct config *cfg = read_config(config_file);
+  if (!cfg) {
+    snprintf(errbuf, errbuf_size, "Unable to open config file: \"%s\"", config_file);
+    free(config_file);
+    return TS_ERROR;
+  }
+  free(config_file);
+  *ih = cfg;
+
+  return TS_SUCCESS;
+}
+
+/* Delete remap instance. */
+void
+TSRemapDeleteInstance(void *ih)
+{
+  config_delete(ih);
+}
+
+int
+add_cookie(TSCont cont, TSEvent event, void *edata)
+{
+  struct timer t;
+  start_timer(&t);
+
+  TSHttpTxn txn = (TSHttpTxn)edata;
+  char *cookie  = TSContDataGet(cont);
+  TSMBuffer buffer;
+  TSMLoc hdr;
+  TSMLoc field;
+  if (!cookie) {
+    goto fail;
+  }
+
+  if (TSHttpTxnClientRespGet(txn, &buffer, &hdr) == TS_ERROR) {
+    goto fail;
+  }
+
+  if (TSMimeHdrFieldCreateNamed(buffer, hdr, "Set-Cookie", 10, &field) != TS_SUCCESS) {
+    goto fail;
+  }
+
+  if (TSMimeHdrFieldAppend(buffer, hdr, field) != TS_SUCCESS) {
+    goto fail_field;
+  }
+
+  if (TSMimeHdrFieldValueStringInsert(buffer, hdr, field, 0, cookie, -1) != TS_SUCCESS) {
+    goto fail_field;
+  }
+
+  PluginDebug("Added cookie to request: %s", cookie);
+
+fail_field:
+  TSHandleMLocRelease(buffer, hdr, field);
+fail:
+  free(cookie);
+  TSContDestroy(cont);
+  TSHttpTxnReenable(txn, TS_EVENT_HTTP_CONTINUE);
+
+  PluginDebug("Spent %" PRId64 " ns uri_signing cookie.", mark_timer(&t));
+  return 0;
+}
+
+TSCont
+cont_new(char *cookie)
+{
+  TSCont cont = TSContCreate(add_cookie, NULL);
+  if (!cont) {
+    PluginError("Cannot create continuation!");
+    free(cookie); /* Nobody else is going to do it at this point. */
+    return NULL;
+  }
+  TSContDataSet(cont, cookie);
+  return cont;
+}
+
+/* Execute remap request. */
+TSRemapStatus
+TSRemapDoRemap(void *ih, TSHttpTxn txnp, TSRemapRequestInfo *rri)
+{
+  struct timer t;
+  start_timer(&t);
+
+  const int max_cpi       = 20;
+  int64_t checkpoints[20] = {0};
+  int cpi                 = 0;
+
+  const char *package = "URISigningPackage";
+  int url_ct          = 0;
+  const char *url     = TSUrlStringGet(rri->requestBufp, rri->requestUrl, &url_ct);
+  if (cpi < max_cpi) {
+    checkpoints[cpi++] = mark_timer(&t);
+  }
+  cjose_jws_t *jws = get_jws_from_query(url, url_ct, package);
+  if (cpi < max_cpi) {
+    checkpoints[cpi++] = mark_timer(&t);
+  }
+  int checked_cookies = 0;
+  if (!jws) {
+  check_cookies:
+    ++checked_cookies;
+
+    TSMLoc field;
+    TSMBuffer buffer;
+    TSMLoc hdr;
+
+    if (TSHttpTxnClientReqGet(txnp, &buffer, &hdr) == TS_ERROR) {
+      goto fail;
+    }
+
+    field = TSMimeHdrFieldFind(buffer, hdr, "Cookie", 6);
+    if (field == TS_NULL_MLOC) {
+      goto fail;
+    }
+
+    const char *client_cookie;
+    int client_cookie_ct;
+    client_cookie = TSMimeHdrFieldValueStringGet(buffer, hdr, field, 0, &client_cookie_ct);
+    if (!client_cookie || !client_cookie_ct) {
+      goto fail;
+    }
+    size_t client_cookie_sz_ct = client_cookie_ct;
+  check_more_cookies:
+    if (cpi < max_cpi) {
+      checkpoints[cpi++] = mark_timer(&t);
+    }
+    jws = get_jws_from_cookie(&client_cookie, &client_cookie_sz_ct, package);
+  }
+  if (!jws) {
+    goto fail;
+  }
+  if (cpi < max_cpi) {
+    checkpoints[cpi++] = mark_timer(&t);
+  }
+  struct jwt *jwt = validate_jws(jws, (struct config *)ih, url, url_ct);
+  if (cpi < max_cpi) {
+    checkpoints[cpi++] = mark_timer(&t);
+  }
+  if (!jwt) {
+    if (!checked_cookies) {
+      goto check_cookies;
+    } else {
+      goto check_more_cookies;
+    }
+  }
+
+  struct signer *signer = config_signer((struct config *)ih);
+  char *cookie          = renew(jwt, signer->issuer, signer->jwk, signer->alg, package);
+  if (cpi < max_cpi) {
+    checkpoints[cpi++] = mark_timer(&t);
+  }
+  if (cookie) {
+    PluginDebug("Scheduling cookie callback for %.*s", url_ct, url);
+    TSCont cont = cont_new(cookie);
+    TSHttpTxnHookAdd(txnp, TS_HTTP_SEND_RESPONSE_HDR_HOOK, cont);
+  } else {
+    PluginDebug("No cookie scheduled for %.*s", url_ct, url);
+  }
+
+  int64_t last_mark = 0;
+  for (int i = 0; i < cpi; ++i) {
+    PluginDebug("Spent %" PRId64 " ns in checkpoint %d.", checkpoints[i] - last_mark, i);
+    last_mark = checkpoints[i];
+  }
+  PluginDebug("Spent %" PRId64 " ns uri_signing verification of %.*s.", mark_timer(&t), url_ct, url);
+  return TSREMAP_NO_REMAP;
+fail:
+  PluginDebug("Invalid JWT for %.*s", url_ct, url);
+  TSHttpTxnSetHttpRetStatus(txnp, TS_HTTP_STATUS_FORBIDDEN);
+  PluginDebug("Spent %" PRId64 " ns uri_signing verification of %.*s.", mark_timer(&t), url_ct, url);
+  return TSREMAP_DID_REMAP;
+}
diff --git a/plugins/experimental/uri_signing/uri_signing.h b/plugins/experimental/uri_signing/uri_signing.h
new file mode 100644
index 0000000..6cb5046
--- /dev/null
+++ b/plugins/experimental/uri_signing/uri_signing.h
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+#define PLUGIN_NAME "uri_signing"
+
+#define PluginDebug(...) TSDebug("uri_signing", PLUGIN_NAME " " __VA_ARGS__)
+#define PluginError(...) PluginDebug(__VA_ARGS__), TSError(PLUGIN_NAME " " __VA_ARGS__)

-- 
To stop receiving notification emails like this one, please contact
['"commits@trafficserver.apache.org" <co...@trafficserver.apache.org>'].