diff --git a/doc/tang.8.adoc b/doc/tang.8.adoc index e0c9f7d..f7ef06e 100644 --- a/doc/tang.8.adoc +++ b/doc/tang.8.adoc @@ -71,6 +71,27 @@ be changed with the *-p* option. tang -l -p 9090 +== ENDPOINT + +The Tang server can be provided an endpoint. This endpoint will act as a prefix +for the URL to be accessed by the client. This endpoint can be specified with +the *-e* option. + + tang -l -p 9090 -e this/is/an/endpoint + +When endpoint is specified, the endpoint will be prepended to the normal adv/rec +URL. If no endpoint is provided, and assuming port 9090 is used, Tang server +will listen on next URLs: + + http://localhost:9090/adv (GET) + http://localhost:9090/rec (POST) + +If endpoint is provided, and assuming endpoint is /this/is/an/endpoint/, and +assuming also port 9090 is used, Tang server will listen on next URLs: + + http://localhost:9090/this/is/an/endpoint/adv (GET) + http://localhost:9090/this/is/an/endpoint/rec (POST) + == KEY ROTATION In order to preserve the security of the system over the long run, you need to diff --git a/src/tang-show-keys b/src/tang-show-keys index 0c33c3a..3ba82e7 100755 --- a/src/tang-show-keys +++ b/src/tang-show-keys @@ -20,14 +20,23 @@ set -e -if [ $# -gt 1 ]; then - echo "Usage: $0 []" >&2 +if [ $# -gt 2 ]; then + echo "Usage: $0 [] []" >&2 exit 1 fi port=${1-80} -adv=$(curl -sSf "localhost:$port/adv") +if test -n "$2"; then + first_letter=$(printf %.1s "$2") + if [ "${first_letter}" = "/" ]; then + adv=$(curl -sSf "localhost:$port$2/adv") + else + adv=$(curl -sSf "localhost:$port/$2/adv") + fi +else + adv=$(curl -sSf "localhost:$port/adv") +fi THP_DEFAULT_HASH=S256 # SHA-256. jose fmt --json "${adv}" -g payload -y -o- \ diff --git a/src/tangd.c b/src/tangd.c index 7f197f6..863956c 100644 --- a/src/tangd.c +++ b/src/tangd.c @@ -32,8 +32,11 @@ #include "keys.h" #include "socket.h" +const unsigned int MAX_URL = 256; + static const struct option long_options[] = { {"port", 1, 0, 'p'}, + {"endpoint", 1, 0, 'e'}, {"listen", 0, 0, 'l'}, {"version", 0, 0, 'v'}, {"help", 0, 0, 'h'}, @@ -45,6 +48,7 @@ print_help(const char *name) { fprintf(stderr, "Usage: %s [OPTIONS] \n", name); fprintf(stderr, " -p, --port=PORT Specify the port to listen (default 9090)\n"); + fprintf(stderr, " -e, --endpoint=ENDPOINT Specify endpoint to listen (empty by default)\n"); fprintf(stderr, " -l, --listen Run as a service and wait for connections\n"); fprintf(stderr, " -v, --version Display program version\n"); fprintf(stderr, " -h, --help Show this help message\n"); @@ -184,7 +188,7 @@ rec(http_method_t method, const char *path, const char *body, "\r\n%s", strlen(enc), enc); } -static struct http_dispatch dispatch[] = { +static struct http_dispatch s_dispatch[] = { { adv, 1 << HTTP_GET, 2, "^/+adv/+([0-9A-Za-z_-]+)$" }, { adv, 1 << HTTP_GET, 2, "^/+adv/*$" }, { rec, 1 << HTTP_POST, 2, "^/+rec/+([0-9A-Za-z_-]+)$" }, @@ -196,7 +200,7 @@ static struct http_dispatch dispatch[] = { static int process_request(const char *jwkdir, int in_fileno) { - struct http_state state = { .dispatch = dispatch, .misc = (char*)jwkdir }; + struct http_state state = { .dispatch = s_dispatch, .misc = (char*)jwkdir }; http_parser_t parser; struct stat st = {}; char req[4096] = {}; @@ -244,9 +248,10 @@ main(int argc, char *argv[]) int listen = 0; int port = DEFAULT_PORT; const char *jwkdir = NULL; + const char *endpoint = NULL; while (1) { - int c = getopt_long(argc, argv, "lp:vh", long_options, NULL); + int c = getopt_long(argc, argv, "lp:e:vh", long_options, NULL); if (c == -1) break; @@ -260,6 +265,9 @@ main(int argc, char *argv[]) case 'p': port = atoi(optarg); break; + case 'e': + endpoint = optarg; + break; case 'l': listen = 1; break; @@ -272,9 +280,24 @@ main(int argc, char *argv[]) } jwkdir = argv[optind++]; + char adv_thp_endpoint[MAX_URL]; + char adv_endpoint[MAX_URL]; + char rec_endpoint[MAX_URL]; + if (endpoint != NULL) { + char *endpoint_ptr = (char*)endpoint; + while (*endpoint_ptr == '/') { + endpoint_ptr++; + } + snprintf(adv_thp_endpoint, MAX_URL, "^/%s/+adv/+([0-9A-Za-z_-]+)$", endpoint_ptr); + snprintf(adv_endpoint, MAX_URL, "^/%s/+adv/*$", endpoint_ptr); + snprintf(rec_endpoint, MAX_URL, "^/%s/+rec/+([0-9A-Za-z_-]+)$", endpoint_ptr); + s_dispatch[0].re = adv_thp_endpoint; + s_dispatch[1].re = adv_endpoint; + s_dispatch[2].re = rec_endpoint; + } if (listen == 0) { /* process one-shot query from stdin */ - return process_request(jwkdir, STDIN_FILENO); + return process_request(jwkdir, STDIN_FILENO); } else { /* listen and process all incoming connections */ - return run_service(jwkdir, port, process_request); + return run_service(jwkdir, port, process_request); } } diff --git a/tests/adv b/tests/adv index 8e6dd0e..416dc6e 100755 --- a/tests/adv +++ b/tests/adv @@ -36,47 +36,47 @@ adv_startup () { adv_second_phase () { # Make sure requests on the root fail - fetch / && expected_fail + fetch "${ENDPOINT}"/ && expected_fail # The request should fail (404) for non-signature key IDs - fetch /adv/`jose jwk thp -i $TMP/db/exc.jwk` && expected_fail - fetch /adv/`jose jwk thp -a S512 -i $TMP/db/exc.jwk` && expected_fail + fetch "${ENDPOINT}"/adv/`jose jwk thp -i $TMP/db/exc.jwk` && expected_fail + fetch "${ENDPOINT}"/adv/`jose jwk thp -a S512 -i $TMP/db/exc.jwk` && expected_fail # The default advertisement fetch should succeed and pass verification - fetch /adv - fetch /adv | ver $TMP/db/sig.jwk - fetch /adv/ | ver $TMP/db/sig.jwk + fetch "${ENDPOINT}"/adv + fetch "${ENDPOINT}"/adv | ver $TMP/db/sig.jwk + fetch "${ENDPOINT}"/adv/ | ver $TMP/db/sig.jwk # Fetching by any thumbprint should work - fetch /adv/`jose jwk thp -i $TMP/db/sig.jwk` | ver $TMP/db/sig.jwk - fetch /adv/`jose jwk thp -a S512 -i $TMP/db/sig.jwk` | ver $TMP/db/sig.jwk + fetch "${ENDPOINT}"/adv/`jose jwk thp -i $TMP/db/sig.jwk` | ver $TMP/db/sig.jwk + fetch "${ENDPOINT}"/adv/`jose jwk thp -a S512 -i $TMP/db/sig.jwk` | ver $TMP/db/sig.jwk # Requesting an adv by an advertised key ID should't be signed by hidden keys - fetch /adv/`jose jwk thp -i $TMP/db/sig.jwk` | ver $TMP/db/.sig.jwk && expected_fail - fetch /adv/`jose jwk thp -i $TMP/db/sig.jwk` | ver $TMP/db/.oth.jwk && expected_fail + fetch "${ENDPOINT}"/adv/`jose jwk thp -i $TMP/db/sig.jwk` | ver $TMP/db/.sig.jwk && expected_fail + fetch "${ENDPOINT}"/adv/`jose jwk thp -i $TMP/db/sig.jwk` | ver $TMP/db/.oth.jwk && expected_fail # Verify that the default advertisement is not signed with hidden signature keys - fetch /adv/ | ver $TMP/db/.oth.jwk && expected_fail - fetch /adv/ | ver $TMP/db/.sig.jwk && expected_fail + fetch "${ENDPOINT}"/adv/ | ver $TMP/db/.oth.jwk && expected_fail + fetch "${ENDPOINT}"/adv/ | ver $TMP/db/.sig.jwk && expected_fail # A private key advertisement is signed by all advertised keys and the requested private key - fetch /adv/`jose jwk thp -i $TMP/db/.sig.jwk` | ver $TMP/db/sig.jwk - fetch /adv/`jose jwk thp -i $TMP/db/.sig.jwk` | ver $TMP/db/.sig.jwk - fetch /adv/`jose jwk thp -i $TMP/db/.sig.jwk` | ver $TMP/db/.oth.jwk && expected_fail + fetch "${ENDPOINT}"/adv/`jose jwk thp -i $TMP/db/.sig.jwk` | ver $TMP/db/sig.jwk + fetch "${ENDPOINT}"/adv/`jose jwk thp -i $TMP/db/.sig.jwk` | ver $TMP/db/.sig.jwk + fetch "${ENDPOINT}"/adv/`jose jwk thp -i $TMP/db/.sig.jwk` | ver $TMP/db/.oth.jwk && expected_fail # Verify that the advertisements contain the cty parameter - fetch /adv | jose fmt -j- -Og protected -SyOg cty -Sq "jwk-set+json" -E - fetch /adv/`jose jwk thp -i $TMP/db/.sig.jwk` \ + fetch "${ENDPOINT}"/adv | jose fmt -j- -Og protected -SyOg cty -Sq "jwk-set+json" -E + fetch "${ENDPOINT}"/adv/`jose jwk thp -i $TMP/db/.sig.jwk` \ | jose fmt -j- -Og signatures -A \ -g 0 -Og protected -SyOg cty -Sq "jwk-set+json" -EUUUUU \ -g 1 -Og protected -SyOg cty -Sq "jwk-set+json" -EUUUUU THP_DEFAULT_HASH=S256 # SHA-256. - test "$(tang-show-keys $PORT)" = "$(jose jwk thp -a "${THP_DEFAULT_HASH}" -i $TMP/db/sig.jwk)" + test "$(tang-show-keys $PORT $ENDPOINT)" = "$(jose jwk thp -a "${THP_DEFAULT_HASH}" -i $TMP/db/sig.jwk)" # Check that new keys will be created if none exist. rm -rf "${TMP}/db" && mkdir -p "${TMP}/db" - fetch /adv + fetch "${ENDPOINT}"/adv # Now let's make sure the new keys were named using our default thumbprint # hash and then rotate them and check if we still create new keys. @@ -88,7 +88,7 @@ adv_second_phase () { mv -f -- "${k}" ".${k}" done cd - - fetch /adv + fetch "${ENDPOINT}"/adv # Lets's now test with multiple pairs of keys. for i in 1 2 3 4 5 6 7 8 9; do @@ -103,12 +103,12 @@ adv_second_phase () { done # Verify the advertisement is correct. - validate "$(fetch /adv)" + validate "$(fetch "${ENDPOINT}"/adv)" # And make sure we can fetch an adv by its thumbprint. for jwk in "${TMP}"/db/other-sig-*.jwk; do for alg in $(jose alg -k hash); do - fetch /adv/"$(jose jwk thp -a "${alg}" -i "${jwk}")" | ver "${jwk}" + fetch "${ENDPOINT}"/adv/"$(jose jwk thp -a "${alg}" -i "${jwk}")" | ver "${jwk}" done done @@ -130,5 +130,5 @@ adv_second_phase () { valid_key_perm "${jwk}" done [ -z "${thp}" ] && die "There should be valid keys after rotation" - test "$(tang-show-keys $PORT)" = "${thp}" + test "$(tang-show-keys $PORT $ENDPOINT)" = "${thp}" } diff --git a/tests/adv-socat-endpoint b/tests/adv-socat-endpoint new file mode 100644 index 0000000..c4deb88 --- /dev/null +++ b/tests/adv-socat-endpoint @@ -0,0 +1,33 @@ +#!/bin/sh -ex +# +# Copyright (c) 2023 Red Hat, Inc. +# Author: Sergio Arroutbi +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +. adv + +sanity_check + +adv_startup + +port=$(random_port) +export PORT=$((port+3)) +export ENDPOINT="/api/dee-hms" +start_server_endpoint "${PORT}" "${ENDPOINT}" +export PID=$! +wait_for_port ${PORT} + +adv_second_phase diff --git a/tests/adv-standalone-endpoint b/tests/adv-standalone-endpoint new file mode 100644 index 0000000..de7a7fe --- /dev/null +++ b/tests/adv-standalone-endpoint @@ -0,0 +1,31 @@ +#!/bin/sh -ex +# +# Copyright (c) 2023 Red Hat, Inc. +# Author: Sergio Arroutbi +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +. adv + +adv_startup + +port=$(random_port) +export PORT=$((port+1)) +export ENDPOINT="/api/dee-hms" +start_standalone_server_endpoint "${PORT}" "${ENDPOINT}" +export PID=$! +wait_for_port ${PORT} + +adv_second_phase diff --git a/tests/helpers b/tests/helpers index 55447ae..5ce3daf 100755 --- a/tests/helpers +++ b/tests/helpers @@ -30,7 +30,12 @@ random_port() { if [ -n "${TANG_BSD}" ]; then jot -r 1 1024 65536 else - shuf -i 1024-65536 -n 1 + if test -f /dev/urandom; + then + shuf -i 1024-65535 -n 1 --random-file=/dev/urandom + else + shuf -i 1024-65535 -n 1 + fi fi } @@ -62,10 +67,18 @@ start_server() { "${SOCAT}" TCP-LISTEN:"${1}",bind=127.0.0.1,fork SYSTEM:"${VALGRIND} tangd ${TMP}/db" & } +start_server_endpoint() { + "${SOCAT}" TCP-LISTEN:"${1}",bind=127.0.0.1,fork SYSTEM:"${VALGRIND} tangd ${TMP}/db -e ${ENDPOINT}" & +} + start_standalone_server() { ${VALGRIND} tangd -p ${1} -l ${TMP}/db & } +start_standalone_server_endpoint() { + ${VALGRIND} tangd -p ${1} -l ${TMP}/db -e ${2} & +} + on_exit() { if [ "${PID}" ]; then kill "${PID}" || true diff --git a/tests/meson.build b/tests/meson.build index 7d184f8..e2e9924 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -41,9 +41,13 @@ if socat.found() endif test('adv-standalone', find_program('adv-standalone'), env: env, timeout: 360) +test('adv-standalone-endpoint', find_program('adv-standalone-endpoint'), env: env, timeout: 360) test('adv-socat', find_program('adv-socat'), env: env, timeout: 360) +test('adv-socat-endpoint', find_program('adv-socat-endpoint'), env: env, timeout: 360) test('rec-standalone', find_program('rec-standalone'), env: env, timeout: 360) +test('rec-standalone-endpoint', find_program('rec-standalone-endpoint'), env: env, timeout: 360) test('rec-socat', find_program('rec-socat'), env: env, timeout: 360) +test('rec-socat-endpoint', find_program('rec-socat-endpoint'), env: env, timeout: 360) test('test-keys', test_keys, env: env, timeout: 360) # vim:set ts=2 sw=2 et: diff --git a/tests/rec b/tests/rec index f15637d..d30f72f 100755 --- a/tests/rec +++ b/tests/rec @@ -51,3 +51,17 @@ rec_second_phase () { http://127.0.0.1:$PORT/rec/${exc_kid} < $TMP/exc.pub.jwk` [ "$good" = "$test" ] } + +rec_second_phase_endpoint () { + # Make sure that GET fails + curl -sf http://127.0.0.1:$PORT/$ENDPOINT/rec && expected_fail + curl -sf http://127.0.0.1:$PORT/$ENDPOINT/rec/ && expected_fail + + # Make a recovery request (NOTE: this is insecure! Don't do this in real code!) + good=`jose jwk exc -i '{"alg":"ECMR","key_ops":["deriveKey"]}' -l $TMP/exc.jwk -r $TMP/db/exc.jwk` + test=`curl -sf -X POST \ + -H "Content-Type: application/jwk+json" \ + --data-binary @- \ + http://127.0.0.1:$PORT/$ENDPOINT/rec/${exc_kid} < $TMP/exc.pub.jwk` + [ "$good" = "$test" ] +} diff --git a/tests/rec-socat-endpoint b/tests/rec-socat-endpoint new file mode 100644 index 0000000..f62d919 --- /dev/null +++ b/tests/rec-socat-endpoint @@ -0,0 +1,34 @@ +#!/bin/sh -ex +# +# Copyright (c) 2023 Red Hat, Inc. +# Author: Sergio Arroutbi +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +. rec + +sanity_check + +rec_startup + +# Start the server +port=$(random_port) +export PORT=$((port+4)) +export ENDPOINT="api/dee-hms" +start_server_endpoint "${PORT}" "${ENDPOINT}" +export PID=$! +wait_for_port ${PORT} + +rec_second_phase_endpoint diff --git a/tests/rec-standalone-endpoint b/tests/rec-standalone-endpoint new file mode 100755 index 0000000..2e7e1b8 --- /dev/null +++ b/tests/rec-standalone-endpoint @@ -0,0 +1,34 @@ +#!/bin/sh -ex +# +# Copyright (c) 2023 Red Hat, Inc. +# Author: Sergio Arroutbi +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +. rec + +sanity_check + +rec_startup + +# Start the server +port=$(random_port) +export PORT=$((port+2)) +export ENDPOINT="api/dee-hms" +start_standalone_server_endpoint "${PORT}" "${ENDPOINT}" +export PID=$! +wait_for_port ${PORT} + +rec_second_phase_endpoint