1
0
Quellcode durchsuchen

Merge upstream version 15

Christoph Biedl vor 1 Jahr
Ursprung
Commit
f416ae2bae

+ 12 - 7
.github/workflows/build.yml

@@ -1,7 +1,13 @@
 ---
 name: build
 
-on: [push, pull_request]
+on:
+  push:
+    paths-ignore:
+      - '**.md'
+  pull_request:
+    paths-ignore:
+      - '**.md'
 
 jobs:
   build:
@@ -11,17 +17,15 @@ jobs:
       matrix:
         os:
           - fedora:latest
-          - centos:7
           - quay.io/centos/centos:stream8
           - quay.io/centos/centos:stream9
           - debian:testing
           - debian:latest
+          - ubuntu:latest
           - ubuntu:rolling
           - ubuntu:lunar
           - ubuntu:jammy
           - ubuntu:focal
-          - ubuntu:bionic
-          - ubuntu:kinetic
         stable: [true]
         include:
           - os: quay.io/fedora/fedora:rawhide
@@ -29,10 +33,11 @@ jobs:
           - os: ubuntu:devel
             stable: false
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
 
       - name: Show OS information
-        run: cat /etc/os-release 2>/dev/null || echo /etc/os-release not available
+        run: cat /etc/os-release 2>/dev/null || \
+          echo /etc/os-release not available
 
       - name: Install build dependencies
         run: bash .github/workflows/install-dependencies
@@ -42,7 +47,7 @@ jobs:
           mkdir -p build && cd build
           export ninja=$(command -v ninja)
           [ -z "${ninja}" ] && export ninja=$(command -v ninja-build)
-          meson .. || cat meson-logs/meson-log.txt >&2
+          meson setup .. || cat meson-logs/meson-log.txt >&2
           ${ninja}
 
       - name: Run tests

+ 9 - 3
.github/workflows/coverage.yml

@@ -1,7 +1,13 @@
 ---
 name: coverage
 
-on: [push, pull_request]
+on:
+  push:
+    paths-ignore:
+      - '**.md'
+  pull_request:
+    paths-ignore:
+      - '**.md'
 
 jobs:
   build:
@@ -11,7 +17,7 @@ jobs:
         os:
           - ubuntu:latest
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
 
       - name: Show OS information
         run: cat /etc/os-release 2>/dev/null || echo /etc/os-release not available
@@ -47,7 +53,7 @@ jobs:
           [ -z "${ninja}" ] && export ninja=$(command -v ninja-build)
           gcovr -r .. -f ../src -f src/ -e ../tests -e tests -x coverage.xml
 
-      - uses: codecov/codecov-action@v2
+      - uses: codecov/codecov-action@v3
         with:
           file: build/coverage.xml
           fail_ci_if_error: true # optional (default = false)

+ 5 - 5
.github/workflows/install-dependencies

@@ -6,24 +6,24 @@ debian:*|ubuntu:*)
     apt clean
     apt update
     apt -y install gcc meson pkg-config libjose-dev jose libhttp-parser-dev \
-                   systemd gcovr curl socat iproute2
+                   systemd gcovr curl socat iproute2 asciidoc
     ;;
 
 *fedora:*)
     echo 'max_parallel_downloads=10' >> /etc/dnf/dnf.conf
     dnf -y clean all
     dnf -y --setopt=deltarpm=0 update
-    dnf -y install gcc meson pkgconfig libjose-devel jose http-parser-devel \
-                   systemd gcovr curl socat iproute
+    dnf -y install gcc meson pkgconfig libjose-devel jose llhttp-devel \
+                   systemd gcovr curl socat iproute asciidoc
     ;;
 
-centos:*)
+centos:7)
     yum -y clean all
     yum -y --setopt=deltarpm=0 update
     yum install -y yum-utils epel-release
     yum config-manager -y --set-enabled PowerTools \
         || yum config-manager -y --set-enabled powertools || :
-    yum -y install meson socat iproute
+    yum -y install meson socat iproute asciidoc
     yum-builddep -y tang
     ;;
 

+ 51 - 27
README.md

@@ -60,11 +60,15 @@ identifying information from the client.
 ## Getting Started
 ### Dependencies
 
-Tang requires a few other software libraries:
+Tang requires two other software libraries:
 
-1. http-parser >= 2.8.0 - https://github.com/nodejs/http-parser
-2. systemd - https://github.com/systemd/systemd
-3. jose >= 8 - https://github.com/latchset/jose
+1. jose >= 8 - https://github.com/latchset/jose
+2. Either:
+   - llhttp - https://github.com/nodejs/llhttp
+   - http_parser >= 2.8 - https://github.com/nodejs/http-parser
+
+http_parser is unmaintained, but llhttp is not availalbe in all
+distributions - notably Debian and CentOS.
 
 #### Fedora
 
@@ -76,44 +80,49 @@ additional settings (such as SETGID directories) out of the box. To install it:
 If you really want to build from source on Fedora, you will need the following
 packages:
 
-1. http-parser - ``http-parser-devel``
-2. systemd - ``systemd``
+1. llhttp - ``llhttp-devel``
+2. systemd - ``systemd`` (desirable but not strictly required)
 3. jose - ``jose``, ``libjose-devel``
-4. curl - curl (only needed for running tests)
+4. curl - ``curl`` (only needed for running tests)
+5. socat - ``socat`` (only needed for running tests)
 
 #### OpenWrt
 
 Tang is also capable of running on devices without systemd even for example
 OpenWrt (see: [this PR](https://github.com/openwrt/packages/pull/5447)).
 Instead of using systemd for socket activation you can use another daemon for
-spawning services like xinetd.
+spawning services like xinetd. As of version 12 tang can also be run as a
+standalone server without a separate socket listener.
 
 An example of configuration file for Tang using xinetd can be found in the
 `units/` directory as 'tangdx'.  Using that will also require installing the
 wrapper from the 'units/' directroy 'tangdw' in '/usr/libexec/tangdw'.
 
-#### FreeBSD, HardenedBSD and OPNsense
+#### FreeBSD
 
-Tang is also capable of running on FreeBSD Unix variants. The build is simple
-and differs only sligtly from the general instructions.
+Tang is also capable of running on FreeBSD Unix variants. It is available in
+the ports tree and package system.  As root you can install it with:
 
-    (as root) # pkg install jose git meson pkgconf jansson openssl asciidoc http-parser socat
-    $ mkdir build && cd build
-    $ meson .. --prefix=/usr/local --localstatedir=/usr/local/var
-    $ ninja
-    (as root) # ninja install
-    (as root) # mkdir -m 0700 /usr/local/var/db/tang
-    (as root) service tangd enable
-    (as root) service tangd start
+    # pkg install tang
+    # service tangd enable
+    # service tangd start
 
-Once built it does not require the many packages above, but still requires
-jose, socat and http_parser. 
+#### OPNsense
+
+Tang can be installed on OPNsense by enabling the FreeBSD package repositories
+and then installing. There are some extra steps to minimize the installation.
 
-FreeBSD, HardendedBSD, and OPNsense use inetd rather than systemd or
-xinetd. To limit the need to manage inetd configuration which has a shared
-config file, tangd is instead packaged to depend on `socat`.  Of course,
-if desired it may be configured to run instead from inetd.conf in which case
-the socat package will no longer be required.
+As root enable the FreeBSD repository, download tang, jose, and llhttp.
+Then disable the FreeBSD repository to prevent installing extraneous
+dependencies not needed by tang. And finally install the downloaded packages
+and start the server:
+
+    # vi /usr/local/etc/pkg/repos/FreeBSD.conf (set enabled to yes)
+    # pkg download tang jose llhttp
+    # vi /usr/local/etc/pkg/repos/FreeBSD.conf (set enabled back to no)
+    # pkg install /var/cache/pkg/tang-*.pkg /var/cache/pkg/jose-*.pkg /var/cache/pkg/llhttp-*.pkg
+    # service tangd enable
+    # service tangd start
 
 #### Docker Container
 
@@ -129,7 +138,7 @@ protect.
 Building Tang is fairly straightforward:
 
     $ mkdir build && cd build
-    $ meson .. --prefix=/usr
+    $ meson setup .. --prefix=/usr
     $ ninja
     $ sudo ninja install
 
@@ -137,6 +146,21 @@ You can even run the tests if you'd like:
 
     $ meson test
 
+#### FreeBSD
+
+The build is simple and differs only sligtly from the general instructions.
+
+    (as root) # pkg install jose git meson pkgconf jansson asciidoc llhttp socat
+    $ mkdir build && cd build
+    $ meson setup .. --prefix=/usr/local
+    $ ninja
+    $ meson test # if you want to run the tests
+    (as root) # ninja install
+    (as root) # mkdir -m 0700 /var/db/tang
+
+Once built it does not require the many packages above, but still requires
+jose and llhttp. 
+
 ### Server Enablement
 
 Once installed, starting a Tang server is simple:

+ 21 - 0
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

+ 14 - 5
meson.build

@@ -1,5 +1,5 @@
 project('tang', 'c',
-  version: '14',
+  version: '15',
   license: 'GPL3+',
   default_options: [
     'c_std=c99',
@@ -55,13 +55,22 @@ add_project_arguments('-DVERSION="'+meson.project_version() + '"', language : 'c
 jose = dependency('jose', version: '>=8')
 a2x = find_program('a2x', required: false)
 compiler = meson.get_compiler('c')
-if not compiler.has_header('http_parser.h',args : '-I/usr/local/include')
-  error('http-parser devel files not found.')
+
+http_lib = []
+if compiler.has_header('llhttp.h', args: '-I/usr/local/include')
+  http_lib = 'llhttp'
+  add_project_arguments('-DUSE_LLHTTP', language: 'c')
+else
+  if not compiler.has_header('http_parser.h', args: '-I/usr/local/include')
+    error('neither llhttp nor http-parser devel files found.')
+  endif
+  http_lib = 'http_parser'
 endif
+
 if host_machine.system() == 'freebsd'
-  http_parser = compiler.find_library('http_parser',dirs : '/usr/local/lib')
+  http_parser = compiler.find_library(http_lib, dirs : '/usr/local/lib')
 else
-  http_parser = compiler.find_library('http_parser')
+  http_parser = compiler.find_library(http_lib)
 endif
 
 licenses = ['COPYING']

+ 5 - 5
src/http.c

@@ -36,7 +36,7 @@ HTTP_METHOD_MAP(XX)
 };
 
 static int
-on_url(http_parser *parser, const char *at, size_t length)
+on_url(http_parser_t *parser, const char *at, size_t length)
 {
     struct http_state *state = parser->data;
 
@@ -51,7 +51,7 @@ on_url(http_parser *parser, const char *at, size_t length)
 }
 
 static int
-on_body(http_parser *parser, const char *at, size_t length)
+on_body(http_parser_t *parser, const char *at, size_t length)
 {
     struct http_state *state = parser->data;
 
@@ -66,7 +66,7 @@ on_body(http_parser *parser, const char *at, size_t length)
 }
 
 static int
-on_message_complete(http_parser *parser)
+on_message_complete(http_parser_t *parser)
 {
     struct http_state *state = parser->data;
     const char *addr = NULL;
@@ -132,7 +132,7 @@ egress:
     return 0;
 }
 
-const http_parser_settings http_settings = {
+const http_settings_t http_settings = {
     .on_url = on_url,
     .on_body = on_body,
     .on_message_complete = on_message_complete,
@@ -140,7 +140,7 @@ const http_parser_settings http_settings = {
 
 int
 http_reply(const char *file, int line,
-           enum http_status code, const char *fmt, ...)
+           http_status_t code, const char *fmt, ...)
 {
     const char *msg = NULL;
     va_list ap;

+ 29 - 4
src/http.h

@@ -19,12 +19,37 @@
 
 #pragma once
 
-#include <http_parser.h>
 #include <sys/types.h>
 #include <regex.h>
 
+#ifdef USE_LLHTTP
+#include <llhttp.h>
+
+typedef llhttp_method_t http_method_t;
+typedef llhttp_status_t http_status_t;
+typedef llhttp_settings_t http_settings_t;
+typedef llhttp_t http_parser_t;
+#define tang_http_parser_init(parser, settings) llhttp_init(parser, HTTP_REQUEST, settings)
+#define tang_http_parser_errno(parser) parser.error
+#define tang_http_errno_description(parser, errno) llhttp_get_error_reason(parser)
+#define tang_http_parser_resume(parser) llhttp_resume(parser)
+#else
+/* Legacy http-parser. */
+#include <http_parser.h>
+
+typedef enum http_method http_method_t;
+typedef enum http_status http_status_t;
+typedef http_parser_settings http_settings_t;
+typedef struct http_parser http_parser_t;
+
+#define tang_http_parser_init(parser, settings) http_parser_init(parser, HTTP_REQUEST)
+#define tang_http_parser_errno(parser) parser.http_errno
+#define tang_http_errno_description(parser, errno) http_errno_description(errno)
+#define tang_http_parser_resume(parser) http_parser_pause(parser, 0)
+#endif /* USE_LLHTTP */
+
 struct http_dispatch {
-    int (*func)(enum http_method method, const char *path,
+    int (*func)(http_method_t method, const char *path,
                 const char *body, regmatch_t matches[], void *misc);
     uint64_t methods;
     size_t nmatches;
@@ -43,11 +68,11 @@ struct http_state {
     void *misc;
 };
 
-extern const http_parser_settings http_settings;
+extern const http_settings_t http_settings;
 
 int __attribute__ ((format(printf, 4, 5)))
 http_reply(const char *file, int line,
-           enum http_status code, const char *fmt, ...);
+           http_status_t code, const char *fmt, ...);
 
 #define http_reply(code, ...) \
     http_reply(__FILE__, __LINE__, code, __VA_ARGS__)

+ 1 - 2
src/socket.c

@@ -178,10 +178,9 @@ static void spawn_process(int fd, const char *jwkdir,
 
 static void handle_child(int sig)
 {
-	pid_t pid;
 	int status;
 
-	while ((pid = waitpid(-1, &status, WNOHANG)) > 0);
+	while ((waitpid(-1, &status, WNOHANG)) > 0);
 }
 
 int run_service(const char *jwkdir, int port, process_request_func pfunc)

+ 12 - 3
src/tang-show-keys

@@ -20,14 +20,23 @@
 
 set -e
 
-if [ $# -gt 1 ]; then
-    echo "Usage: $0 [<port>]" >&2
+if [ $# -gt 2 ]; then
+    echo "Usage: $0 [<port>] [<endpoint>]" >&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- \

+ 80 - 13
src/tangd.c

@@ -32,8 +32,11 @@
 #include "keys.h"
 #include "socket.h"
 
+#define 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] <jwkdir>\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");
@@ -64,7 +68,7 @@ str_cleanup(char **str)
 }
 
 static int
-adv(enum http_method method, const char *path, const char *body,
+adv(http_method_t method, const char *path, const char *body,
     regmatch_t matches[], void *misc)
 {
     __attribute__((cleanup(str_cleanup))) char *adv = NULL;
@@ -101,7 +105,7 @@ adv(enum http_method method, const char *path, const char *body,
 }
 
 static int
-rec(enum http_method method, const char *path, const char *body,
+rec(http_method_t method, const char *path, const char *body,
     regmatch_t matches[], void *misc)
 {
     __attribute__((cleanup(str_cleanup))) char *enc = NULL;
@@ -184,7 +188,7 @@ rec(enum http_method 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_-]+)$" },
@@ -193,17 +197,56 @@ static struct http_dispatch dispatch[] = {
 
 #define DEFAULT_PORT 9090
 
+static size_t
+tang_http_parser_execute(http_parser_t *parser, const char* data, size_t len)
+{
+#ifdef USE_LLHTTP
+    llhttp_errno_t error;
+    size_t parsed_len;
+
+    /*
+     * Unlike http_parser, which returns the number of parsed
+     * bytes in the _execute() call, llhttp returns an error
+     * code.
+     */
+
+    if (data == NULL || len == 0) {
+        error = llhttp_finish(parser);
+    } else {
+        error = llhttp_execute(parser, data, len);
+    }
+
+    parsed_len = len;
+    /*
+     * Adjust number of parsed bytes in case of error.
+     */
+    if (error != HPE_OK) {
+        parsed_len = llhttp_get_error_pos(parser) - data;
+
+        /* This isn't a real pause, just a way to stop parsing early. */
+        if (error == HPE_PAUSED_UPGRADE) {
+            llhttp_resume_after_upgrade(parser);
+        }
+    }
+
+    return parsed_len;
+#else
+    return http_parser_execute(parser, &http_settings, data, len);
+#endif
+}
+
 static int
 process_request(const char *jwkdir, int in_fileno)
 {
-    struct http_state state = { .dispatch = dispatch, .misc = (char*)jwkdir };
-    struct http_parser parser = { .data = &state };
+    struct http_state state = { .dispatch = s_dispatch, .misc = (char*)jwkdir };
+    http_parser_t parser;
     struct stat st = {};
     char req[4096] = {};
     size_t rcvd = 0;
     int r = 0;
 
-    http_parser_init(&parser, HTTP_REQUEST);
+    tang_http_parser_init(&parser, &http_settings);
+    parser.data = &state;
 
     if (stat(jwkdir, &st) != 0) {
         fprintf(stderr, "Error calling stat() on path: %s: %m\n", jwkdir);
@@ -224,17 +267,22 @@ process_request(const char *jwkdir, int in_fileno)
 
         rcvd += r;
 
-        r = http_parser_execute(&parser, &http_settings, req, rcvd);
-        if (parser.http_errno != 0) {
+        r = tang_http_parser_execute(&parser, req, rcvd);
+        switch (tang_http_parser_errno(parser)) {
+        case HPE_OK:
+            break;
+        case HPE_PAUSED:
+            tang_http_parser_resume(&parser);
+            break;
+        default:
             fprintf(stderr, "HTTP Parsing Error: %s\n",
-                    http_errno_description(parser.http_errno));
+                    tang_http_errno_description(&parser, tang_http_parser_errno(parser)));
             return EXIT_SUCCESS;
         }
 
         memmove(req, &req[r], rcvd - r);
         rcvd -= r;
     }
-
     return EXIT_SUCCESS;
 }
 
@@ -244,9 +292,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 +309,9 @@ main(int argc, char *argv[])
 	    case 'p':
 		port = atoi(optarg);
 		break;
+	    case 'e':
+		endpoint = optarg;
+		break;
 	    case 'l':
 		listen = 1;
 		break;
@@ -272,9 +324,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);
     }
 }

+ 23 - 23
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}"
 }

+ 33 - 0
tests/adv-socat-endpoint

@@ -0,0 +1,33 @@
+#!/bin/sh -ex
+#
+# Copyright (c) 2023 Red Hat, Inc.
+# Author: Sergio Arroutbi <sarroutb@redhat.com>
+#
+# 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 <http://www.gnu.org/licenses/>.
+#
+
+. 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

+ 31 - 0
tests/adv-standalone-endpoint

@@ -0,0 +1,31 @@
+#!/bin/sh -ex
+#
+# Copyright (c) 2023 Red Hat, Inc.
+# Author: Sergio Arroutbi <sarroutb@redhat.com>
+#
+# 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 <http://www.gnu.org/licenses/>.
+#
+
+. 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

+ 14 - 1
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

+ 9 - 5
tests/meson.build

@@ -40,10 +40,14 @@ if socat.found()
   env.set('SOCAT', socat.path())
 endif
 
-test('adv-standalone', find_program('adv-standalone'), env: env, timeout: 60)
-test('adv-socat', find_program('adv-socat'), env: env, timeout: 60)
-test('rec-standalone', find_program('rec-standalone'), env: env, timeout: 60)
-test('rec-socat', find_program('rec-socat'), env: env)
-test('test-keys', test_keys, env: env, timeout: 60)
+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:

+ 14 - 0
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" ]
+}

+ 34 - 0
tests/rec-socat-endpoint

@@ -0,0 +1,34 @@
+#!/bin/sh -ex
+#
+# Copyright (c) 2023 Red Hat, Inc.
+# Author: Sergio Arroutbi <sarroutb@redhat.com>
+#
+# 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 <http://www.gnu.org/licenses/>.
+#
+
+. 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

+ 34 - 0
tests/rec-standalone-endpoint

@@ -0,0 +1,34 @@
+#!/bin/sh -ex
+#
+# Copyright (c) 2023 Red Hat, Inc.
+# Author: Sergio Arroutbi <sarroutb@redhat.com>
+#
+# 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 <http://www.gnu.org/licenses/>.
+#
+
+. 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