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>'].