Browse Source

Merge upstream version 14

Christoph Biedl 10 months ago
parent
commit
da1d4b8112

+ 9 - 4
.github/workflows/build.yml

@@ -5,26 +5,31 @@ on: [push, pull_request]
 
 jobs:
   build:
-    runs-on: ubuntu-18.04
+    runs-on: ubuntu-22.04
     continue-on-error: ${{ ! matrix.stable }}
     strategy:
       matrix:
         os:
           - fedora:latest
           - centos:7
-          - centos:8
+          - quay.io/centos/centos:stream8
+          - quay.io/centos/centos:stream9
           - debian:testing
           - debian:latest
           - ubuntu:rolling
+          - ubuntu:lunar
+          - ubuntu:jammy
+          - ubuntu:focal
           - ubuntu:bionic
+          - ubuntu:kinetic
         stable: [true]
         include:
-          - os: fedora:rawhide
+          - os: quay.io/fedora/fedora:rawhide
             stable: false
           - os: ubuntu:devel
             stable: false
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
 
       - name: Show OS information
         run: cat /etc/os-release 2>/dev/null || echo /etc/os-release not available

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

@@ -5,13 +5,13 @@ on: [push, pull_request]
 
 jobs:
   build:
-    runs-on: ubuntu-18.04
+    runs-on: ubuntu-22.04
     strategy:
       matrix:
         os:
           - ubuntu:latest
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
 
       - name: Show OS information
         run: cat /etc/os-release 2>/dev/null || echo /etc/os-release not available
@@ -47,7 +47,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@v1
+      - uses: codecov/codecov-action@v2
         with:
           file: build/coverage.xml
           fail_ci_if_error: true # optional (default = false)

+ 14 - 4
.github/workflows/install-dependencies

@@ -6,15 +6,15 @@ debian:*|ubuntu:*)
     apt clean
     apt update
     apt -y install gcc meson pkg-config libjose-dev jose libhttp-parser-dev \
-                   systemd gcovr curl socat
+                   systemd gcovr curl socat iproute2
     ;;
 
-fedora:*)
+*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
+                   systemd gcovr curl socat iproute
     ;;
 
 centos:*)
@@ -23,8 +23,18 @@ centos:*)
     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
+    yum -y install meson socat iproute
     yum-builddep -y tang
     ;;
+
+*centos:stream*)
+    dnf -y clean all
+    dnf -y --setopt=deltarpm=0 update
+    dnf install -y dnf-plugins-core epel-release
+    dnf config-manager -y --set-enabled powertools \
+        || dnf config-manager -y --set-enabled crb || :
+    dnf -y install meson socat iproute
+    dnf builddep -y tang --allowerasing --skip-broken --nobest
+    ;;
 esac
 # vim: set ts=8 shiftwidth=4 softtabstop=4 expandtab smarttab colorcolumn=80:

+ 45 - 0
.gitignore

@@ -0,0 +1,45 @@
+*~
+*.a
+*.o
+*.la
+*.lo
+*.log
+*.m4
+*.path
+*.service
+*.so
+*.socket
+*.swp
+*.swo
+*.trs
+.autotools
+.cproject
+.deps
+.dirstamp
+.libs/
+.project
+.settings
+.ycm_extra_conf.py
+.ycm_extra_conf.pyc
+aclocal.m4
+ar-lib
+autom4te.cache
+build
+compile
+config.guess
+config.log
+config.status
+config.sub
+configure
+configure-stamp
+depcomp
+install-sh
+libtool
+ltmain.sh
+Makefile.in
+Makefile
+missing
+tags
+src/tangd
+src/tang
+test-driver

+ 8 - 0
doc/tang.8.adoc

@@ -63,6 +63,14 @@ ifndef::freebsd[]
 link:systemd.unit.5.adoc[*systemd.unit*(5)] and link:systemd.socket.5.adoc[*systemd.socket*(5)] for more information.
 endif::[]
 
+== STANDALONE OR VIA SYSTEMD
+
+The Tang server can be run via systemd socket activation or standalone
+when the parameter *-l* is passed. The default port used is 9090 and can
+be changed with the *-p* option.
+
+    tang -l -p 9090
+
 == KEY ROTATION
 
 In order to preserve the security of the system over the long run, you need to

+ 3 - 1
meson.build

@@ -1,5 +1,5 @@
 project('tang', 'c',
-  version: '11',
+  version: '14',
   license: 'GPL3+',
   default_options: [
     'c_std=c99',
@@ -50,6 +50,8 @@ add_project_arguments(
   language: 'c'
 )
 
+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')

+ 8 - 2
src/keys.c

@@ -307,6 +307,9 @@ create_new_keys(const char* jwkdir)
 {
     const char* alg[] = {"ES512", "ECMR", NULL};
     char path[PATH_MAX];
+
+    /* Set default umask for file creation. */
+    umask(0337);
     for (int i = 0; alg[i] != NULL; i++) {
         json_auto_t* jwk = jwk_generate(alg[i]);
         if (!jwk) {
@@ -369,9 +372,12 @@ load_keys(const char* jwkdir)
                 continue;
             }
             filepath[sizeof(filepath) - 1] = '\0';
-            json_auto_t* json = json_load_file(filepath, 0, NULL);
+            json_error_t error;
+            json_auto_t* json = json_load_file(filepath, 0, &error);
             if (!json) {
-                fprintf(stderr, "Invalid JSON file (%s); skipping\n", filepath);
+                fprintf(stderr, "Cannot load JSON file (%s); skipping\n", filepath);
+                fprintf(stderr, "error text %s, line %d, col %d, pos %d\n",
+                    error.text, error.line, error.column, error.position);
                 continue;
             }
 

+ 1 - 0
src/meson.build

@@ -2,6 +2,7 @@ tangd = executable('tangd',
   'http.c',
   'keys.c',
   'tangd.c',
+  'socket.c',
   dependencies: [jose, http_parser],
   install: true,
   install_dir: libexecdir

+ 245 - 0
src/socket.c

@@ -0,0 +1,245 @@
+/* vim: set tabstop=8 shiftwidth=4 softtabstop=4 expandtab smarttab colorcolumn=80: */
+/*
+ * Copyright (c) 2022 Nikos Mavrogiannopoulos
+ *
+ * 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/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netdb.h>
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#include <sys/select.h>
+#include <errno.h>
+#include <sys/wait.h>
+#include <signal.h>
+
+#include "socket.h"
+
+#define MAX(x,y) ((x)>(y)?(x):(y))
+
+typedef struct socket_list {
+	int s;
+	int family;
+	struct sockaddr addr;
+	struct socket_list *next;
+} socket_list;
+
+static void free_socket_list(socket_list *slist)
+{
+	socket_list *ptr, *oldptr;
+
+	for (ptr = slist; ptr != NULL;) {
+		if (ptr->s >= 0)
+			close(ptr->s);
+		oldptr = ptr;
+		ptr = ptr->next;
+		free(oldptr);
+	}
+}
+
+static int listen_port(socket_list **slist, int port)
+{
+	struct addrinfo hints, *res, *ptr;
+	int y, r, s;
+	char portname[6], strip[64];
+	socket_list *lm;
+
+	snprintf(portname, sizeof(portname), "%d", port);
+	memset(&hints, 0, sizeof(hints));
+	hints.ai_socktype = SOCK_STREAM;
+	hints.ai_flags = AI_PASSIVE;
+
+	*slist = NULL;
+
+	/* listen to all available (IPv4 and IPv6) address */
+	if ((r = getaddrinfo(NULL, portname, &hints, &res)) != 0) {
+		fprintf(stderr, "getaddrinfo() failed: %s\n", gai_strerror(r));
+		return -1;
+	}
+
+	for (ptr = res; ptr != NULL; ptr = ptr->ai_next) {
+		s = socket(ptr->ai_family, SOCK_STREAM, 0);
+		if (s < 0) {
+			perror("socket() failed");
+			continue;
+		}
+
+		if (ptr->ai_family == AF_INET)
+			fprintf(stderr, "Listening on %s:%d\n", inet_ntop(ptr->ai_family,
+				&((struct sockaddr_in*)ptr->ai_addr)->sin_addr, strip,
+				sizeof(strip)), port);
+		else if (ptr->ai_family == AF_INET6)
+			fprintf(stderr, "Listening on [%s]:%d\n", inet_ntop(ptr->ai_family,
+				&((struct sockaddr_in6*)ptr->ai_addr)->sin6_addr, strip,
+				sizeof(strip)), port);
+
+#if defined(IPV6_V6ONLY)
+		if (ptr->ai_family == AF_INET6) {
+			y = 1;
+			/* avoid listen on ipv6 addresses failing
+			 * because already listening on ipv4 addresses: */
+			if (setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY,
+				   (const void *) &y, sizeof(y)) < 0) {
+				perror("setsockopt(IPV6_V6ONLY) failed");
+			}
+		}
+#endif
+
+		y = 1;
+		if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR,
+			       (const void *) &y, sizeof(y)) < 0) {
+			perror("setsockopt(SO_REUSEADDR) failed");
+		}
+
+		if (bind(s, ptr->ai_addr, ptr->ai_addrlen) < 0) {
+			perror("bind() failed");
+			close(s);
+			continue;
+		}
+
+		if (listen(s, 1024) < 0) {
+			perror("listen() failed");
+			close(s);
+			r = -1;
+			goto cleanup;
+		}
+
+		lm = calloc(1, sizeof(socket_list));
+		if (lm == NULL) {
+			close(s);
+			r = -1;
+			goto cleanup;
+		}
+		lm->s = s;
+		lm->family = ptr->ai_family;
+		memcpy(&lm->addr, ptr->ai_addr, sizeof(*ptr->ai_addr));
+		lm->next = *slist;
+		*slist = lm;
+	}
+
+	if (*slist == NULL)
+		r = -1;
+	else
+		r = 0;
+
+ cleanup:
+	freeaddrinfo(res);
+	fflush(stderr);
+
+	return r;
+}
+
+static void spawn_process(int fd, const char *jwkdir,
+			  process_request_func pfunc,
+			  socket_list *slist)
+{
+	pid_t pid;
+	socket_list *ptr;
+
+	pid = fork();
+	if (pid == 0) { /* child */
+		for (ptr = slist; ptr != NULL; ptr = ptr->next) {
+			close(ptr->s);
+		}
+		/* Ensure that both stdout and stdin are set */
+		if (dup2(fd, STDOUT_FILENO) < 0) {
+			perror("dup2");
+			close(fd);
+			return;
+		}
+
+		close(fd);
+
+		pfunc(jwkdir, STDOUT_FILENO);
+		free_socket_list(slist);
+		exit(0);
+	} else if (pid == -1) {
+		perror("fork failed");
+	}
+	close(fd);
+}
+
+static void handle_child(int sig)
+{
+	pid_t pid;
+	int status;
+
+	while ((pid = waitpid(-1, &status, WNOHANG)) > 0);
+}
+
+int run_service(const char *jwkdir, int port, process_request_func pfunc)
+{
+	socket_list *slist, *ptr;
+	int r, n = 0, accept_fd;
+	fd_set read_fds;
+	struct timeval tv;
+
+	struct sigaction new_action;
+
+	/* Set up the structure to specify the new action. */
+	new_action.sa_handler = handle_child;
+	sigemptyset (&new_action.sa_mask);
+	new_action.sa_flags = 0;
+	sigaction(SIGCHLD, &new_action, NULL);
+
+	r = listen_port(&slist, port);
+	if (r < 0) {
+		fprintf(stderr, "Could not listen port (%d)\n", port);
+		return -1;
+	}
+
+	while (1) {
+		FD_ZERO(&read_fds);
+		for (ptr = slist; ptr != NULL; ptr = ptr->next) {
+			if (ptr->s > FD_SETSIZE) {
+				fprintf(stderr, "exceeded FD_SETSIZE\n");
+				free_socket_list(slist);
+				return -1;
+			}
+			FD_SET(ptr->s, &read_fds);
+			n = MAX(n, ptr->s);
+		}
+		tv.tv_sec = 1200;
+		tv.tv_usec = 0;
+		n = select(n+1, &read_fds, NULL, NULL, &tv);
+		if (n == -1 && errno == EINTR)
+			continue;
+		if (n < 0) {
+			perror("select");
+			free_socket_list(slist);
+			return -1;
+		}
+
+		for (ptr = slist; ptr != NULL; ptr = ptr->next) {
+			if (FD_ISSET(ptr->s, &read_fds)) {
+				accept_fd = accept(ptr->s, NULL, 0);
+				if (accept_fd < 0) {
+					perror("accept");
+					continue;
+				}
+
+				spawn_process(accept_fd, jwkdir, pfunc, slist);
+			}
+		}
+
+	}
+
+	return 0;
+}

+ 21 - 0
src/socket.h

@@ -0,0 +1,21 @@
+/* vim: set tabstop=8 shiftwidth=4 softtabstop=4 expandtab smarttab colorcolumn=80: */
+/*
+ * Copyright (c) 2022 Nikos Mavrogiannopoulos
+ *
+ * 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/>.
+ */
+
+typedef int (*process_request_func)(const char *jwkdir, int in_fileno);
+
+int run_service(const char *jwkdir, int port, process_request_func);

+ 4 - 0
src/tangd-keygen.in

@@ -38,6 +38,10 @@ set_perms() {
 [ $# -eq 3 ] && sig=$2 && exc=$3
 
 THP_DEFAULT_HASH=S256     # SHA-256.
+
+# Set default umask for file creation.
+umask 0337
+
 jwe=$(jose jwk gen -i '{"alg":"ES512"}')
 [ -z "$sig" ] && sig=$(echo "$jwe" | jose jwk thp -i- -a "${THP_DEFAULT_HASH}")
 echo "$jwe" > "$1/$sig.jwk"

+ 4 - 0
src/tangd-rotate-keys.in

@@ -79,6 +79,10 @@ cd "${JWKDIR}" || error "Unable to change to keys directory '${JWKDIR}'"
 
     # Create a new set of keys.
     DEFAULT_THP_HASH="S256"
+
+    # Set default umask for file creation.
+    umask 0337
+
     for alg in "ES512" "ECMR"; do
         json="$(printf '{"alg": "%s"}' "${alg}")"
         jwe="$(jose jwk gen --input "${json}")"

+ 76 - 12
src/tangd.c

@@ -26,9 +26,35 @@
 #include <stdlib.h>
 #include <string.h>
 #include <unistd.h>
+#include <getopt.h>
 
 #include <jose/jose.h>
 #include "keys.h"
+#include "socket.h"
+
+static const struct option long_options[] = {
+	{"port", 1, 0, 'p'},
+	{"listen", 0, 0, 'l'},
+	{"version", 0, 0, 'v'},
+	{"help", 0, 0, 'h'},
+	{NULL, 0, 0, 0}
+};
+
+static void
+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, "  -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");
+}
+
+static void
+print_version(void)
+{
+	fprintf(stderr, "tangd %s\n", VERSION);
+}
 
 static void
 str_cleanup(char **str)
@@ -165,10 +191,12 @@ static struct http_dispatch dispatch[] = {
     {}
 };
 
-int
-main(int argc, char *argv[])
+#define DEFAULT_PORT 9090
+
+static int
+process_request(const char *jwkdir, int in_fileno)
 {
-    struct http_state state = { .dispatch = dispatch, .misc = argv[1] };
+    struct http_state state = { .dispatch = dispatch, .misc = (char*)jwkdir };
     struct http_parser parser = { .data = &state };
     struct stat st = {};
     char req[4096] = {};
@@ -177,23 +205,18 @@ main(int argc, char *argv[])
 
     http_parser_init(&parser, HTTP_REQUEST);
 
-    if (argc != 2) {
-        fprintf(stderr, "Usage: %s <jwkdir>\n", argv[0]);
-        return EXIT_FAILURE;
-    }
-
-    if (stat(argv[1], &st) != 0) {
-        fprintf(stderr, "Error calling stat() on path: %s: %m\n", argv[1]);
+    if (stat(jwkdir, &st) != 0) {
+        fprintf(stderr, "Error calling stat() on path: %s: %m\n", jwkdir);
         return EXIT_FAILURE;
     }
 
     if (!S_ISDIR(st.st_mode)) {
-        fprintf(stderr, "Path is not a directory: %s\n", argv[1]);
+        fprintf(stderr, "Path is not a directory: %s\n", jwkdir);
         return EXIT_FAILURE;
     }
 
     for (;;) {
-        r = read(STDIN_FILENO, &req[rcvd], sizeof(req) - rcvd - 1);
+        r = read(in_fileno, &req[rcvd], sizeof(req) - rcvd - 1);
         if (r == 0)
             return rcvd > 0 ? EXIT_FAILURE : EXIT_SUCCESS;
         if (r < 0)
@@ -214,3 +237,44 @@ main(int argc, char *argv[])
 
     return EXIT_SUCCESS;
 }
+
+int
+main(int argc, char *argv[])
+{
+    int listen = 0;
+    int port = DEFAULT_PORT;
+    const char *jwkdir = NULL;
+
+    while (1) {
+	int c = getopt_long(argc, argv, "lp:vh", long_options, NULL);
+	if (c == -1)
+            break;
+
+	switch(c) {
+            case 'v':
+		print_version();
+		return EXIT_SUCCESS;
+	    case 'h':
+		print_help(argv[0]);
+		return EXIT_SUCCESS;
+	    case 'p':
+		port = atoi(optarg);
+		break;
+	    case 'l':
+		listen = 1;
+		break;
+	}
+    }
+
+    if (optind >= argc) {
+        fprintf(stderr, "Usage: %s [OPTION] <jwkdir>\n", argv[0]);
+	return EXIT_FAILURE;
+    }
+    jwkdir = argv[optind++];
+
+    if (listen == 0) { /* process one-shot query from stdin */
+	return process_request(jwkdir, STDIN_FILENO);
+    } else { /* listen and process all incoming connections */
+	return run_service(jwkdir, port, process_request);
+    }
+}

+ 101 - 104
tests/adv

@@ -20,118 +20,115 @@
 
 . helpers
 
-sanity_check
-
 trap 'on_exit' EXIT
 export TMP=`mktemp -d`
 mkdir -p $TMP/db
 
-tangd-keygen $TMP/db sig exc
-# Make sure keys generated by tangd-keygen have proper permissions.
-valid_key_perm "${TMP}/db/sig.jwk"
-valid_key_perm "${TMP}/db/exc.jwk"
-
-jose jwk gen -i '{"alg": "ES512"}' -o $TMP/db/.sig.jwk
-jose jwk gen -i '{"alg": "ES512"}' -o $TMP/db/.oth.jwk
-
-export PORT=$(random_port)
-start_server "${PORT}"
-export PID=$!
-sleep 0.5
-
-# Make sure requests on the root fail
-fetch / && 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
-
-# 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
-
-# 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
-
-# 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
-
-# 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
-
-# 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
-
-# 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` \
-    | jose fmt -j- -Og signatures -A \
+adv_startup () {
+    tangd-keygen $TMP/db sig exc
+    # Make sure keys generated by tangd-keygen have proper permissions.
+    valid_key_perm "${TMP}/db/sig.jwk"
+    valid_key_perm "${TMP}/db/exc.jwk"
+
+    jose jwk gen -i '{"alg": "ES512"}' -o $TMP/db/.sig.jwk
+    jose jwk gen -i '{"alg": "ES512"}' -o $TMP/db/.oth.jwk
+}
+
+adv_second_phase () {
+    # Make sure requests on the root fail
+    fetch / && 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
+
+    # 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
+
+    # 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
+
+    # 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
+
+    # 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
+
+    # 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
+
+    # 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` \
+        | 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)"
-
-# Check that new keys will be created if none exist.
-rm -rf "${TMP}/db" && mkdir -p "${TMP}/db"
-fetch /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.
-cd "${TMP}/db"
-for k in *.jwk; do
-    # Check for the key name (SHA-256).
-    test "${k}" = "$(jose jwk thp -a "${THP_DEFAULT_HASH}" -i "${k}")".jwk
-    # Rotate the key.
-    mv -f -- "${k}" ".${k}"
-done
-cd -
-fetch /adv
-
-# Lets's now test with multiple pairs of keys.
-for i in 1 2 3 4 5 6 7 8 9; do
-    tangd-keygen "${TMP}"/db other-sig-${i} other-exc-${i}
-    # Make sure the requested keys exist and are valid.
-    validate_sig "${TMP}/db/other-sig-${i}.jwk"
-    validate_exc "${TMP}/db/other-exc-${i}.jwk"
+    THP_DEFAULT_HASH=S256     # SHA-256.
+    test "$(tang-show-keys $PORT)" = "$(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
+
+    # 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.
+    cd "${TMP}/db"
+    for k in *.jwk; do
+        # Check for the key name (SHA-256).
+        test "${k}" = "$(jose jwk thp -a "${THP_DEFAULT_HASH}" -i "${k}")".jwk
+        # Rotate the key.
+        mv -f -- "${k}" ".${k}"
+    done
+    cd -
+    fetch /adv
+
+    # Lets's now test with multiple pairs of keys.
+    for i in 1 2 3 4 5 6 7 8 9; do
+        tangd-keygen "${TMP}"/db other-sig-${i} other-exc-${i}
+        # Make sure the requested keys exist and are valid.
+        validate_sig "${TMP}/db/other-sig-${i}.jwk"
+        validate_exc "${TMP}/db/other-exc-${i}.jwk"
+
+        # Make sure keys generated by tangd-keygen have proper permissions.
+        valid_key_perm "${TMP}/db/other-sig-${i}.jwk"
+        valid_key_perm "${TMP}/db/other-exc-${i}.jwk"
+    done
 
-    # Make sure keys generated by tangd-keygen have proper permissions.
-    valid_key_perm "${TMP}/db/other-sig-${i}.jwk"
-    valid_key_perm "${TMP}/db/other-exc-${i}.jwk"
-done
+    # Verify the advertisement is correct.
+    validate "$(fetch /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}"
+	done
+    done
+
+    # Now let's test keys rotation.
+    tangd-rotate-keys -d "${TMP}/db"
+    for i in 1 2 3 4 5 6 7 8 9; do
+	# Make sure keys were excluded from advertisement.
+	validate_sig "${TMP}/db/.other-sig-${i}.jwk"
+	validate_exc "${TMP}/db/.other-exc-${i}.jwk"
+    done
 
-# Verify the advertisement is correct.
-validate "$(fetch /adv)"
+    # And test also that we have valid keys after rotation.
+    thp=
+    for jwk in "${TMP}"/db/*.jwk; do
+	validate_sig "${jwk}" && thp="$(jose jwk thp -a "${THP_DEFAULT_HASH}" \
+										-i "${jwk}")"
 
-# 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}"
+        # Make sure keys generated by tangd-rotate-keys have proper permissions.
+	valid_key_perm "${jwk}"
     done
-done
-
-# Now let's test keys rotation.
-tangd-rotate-keys -d "${TMP}/db"
-for i in 1 2 3 4 5 6 7 8 9; do
-    # Make sure keys were excluded from advertisement.
-    validate_sig "${TMP}/db/.other-sig-${i}.jwk"
-    validate_exc "${TMP}/db/.other-exc-${i}.jwk"
-done
-
-# And test also that we have valid keys after rotation.
-thp=
-for jwk in "${TMP}"/db/*.jwk; do
-    validate_sig "${jwk}" && thp="$(jose jwk thp -a "${THP_DEFAULT_HASH}" \
-                                    -i "${jwk}")"
-
-    # Make sure keys generated by tangd-rotate-keys have proper permissions.
-    valid_key_perm "${jwk}"
-done
-[ -z "${thp}" ] && die "There should be valid keys after rotation"
-test "$(tang-show-keys $PORT)" = "${thp}"
+    [ -z "${thp}" ] && die "There should be valid keys after rotation"
+    test "$(tang-show-keys $PORT)" = "${thp}"
+}

+ 32 - 0
tests/adv-socat

@@ -0,0 +1,32 @@
+#!/bin/sh -ex
+# vim: set tabstop=8 shiftwidth=4 softtabstop=4 expandtab smarttab colorcolumn=80:
+#
+# Copyright (c) 2016 Red Hat, Inc.
+# Author: Nathaniel McCallum <npmccallum@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
+
+export PORT=$(random_port)
+start_server "${PORT}"
+export PID=$!
+wait_for_port ${PORT}
+
+adv_second_phase

+ 29 - 0
tests/adv-standalone

@@ -0,0 +1,29 @@
+#!/bin/sh -ex
+# vim: set tabstop=8 shiftwidth=4 softtabstop=4 expandtab smarttab colorcolumn=80:
+#
+# Copyright (c) 2022 Nikos Mavrogiannopoulos
+#
+# 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
+
+export PORT=$(random_port)
+start_standalone_server "${PORT}"
+export PID=$!
+wait_for_port ${PORT}
+
+adv_second_phase

+ 32 - 1
tests/helpers

@@ -34,12 +34,43 @@ random_port() {
     fi
 }
 
+check_if_port_listening() {
+    if [ -n "${TANG_BSD}" ]; then
+        sockstat -l|grep "[\:\.]${1}" >/dev/null 2>&1
+    else
+
+        ss -anl|grep "[\:\.]${1}"|grep LISTEN >/dev/null 2>&1
+    fi
+}
+
+wait_for_port()
+{
+    local port="${1}"
+    sleep 1
+
+    local i=0
+    while [ "${i}" -lt 90 ]; do
+        check_if_port_listening "${port}" && return 0
+        i=$((i + 1))
+        echo "try ${i}: waiting for port" >&2
+        sleep 1
+    done
+    return 1
+}
+
 start_server() {
     "${SOCAT}" TCP-LISTEN:"${1}",bind=127.0.0.1,fork SYSTEM:"${VALGRIND} tangd ${TMP}/db" &
 }
 
+start_standalone_server() {
+    ${VALGRIND} tangd -p ${1} -l ${TMP}/db &
+}
+
 on_exit() {
-    if [ "$PID" ]; then kill "${PID}"; wait "${PID}" || true; fi
+    if [ "${PID}" ]; then
+        kill "${PID}" || true
+        wait "${PID}" || true
+    fi
     [ -d "${TMP}" ] && rm -rf "${TMP}"
 }
 

+ 4 - 2
tests/meson.build

@@ -40,8 +40,10 @@ if socat.found()
   env.set('SOCAT', socat.path())
 endif
 
-test('adv', find_program('adv'), env: env, timeout: 60)
-test('rec', find_program('rec'), env: env)
+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)
 
 # vim:set ts=2 sw=2 et:

+ 24 - 28
tests/rec

@@ -20,38 +20,34 @@
 
 . helpers
 
-sanity_check
-
 trap 'on_exit' EXIT
 export TMP=`mktemp -d`
 mkdir -p $TMP/db
 
-# Generate the server keys
-tangd-keygen $TMP/db sig exc
-# Make sure keys generated by tangd-keygen have proper permissions.
-valid_key_perm "${TMP}/db/sig.jwk"
-valid_key_perm "${TMP}/db/exc.jwk"
-
-# Generate the client keys
-exc_kid=`jose jwk thp -i $TMP/db/exc.jwk`
-tmp=`jose fmt -j $TMP/db/exc.jwk -Od x -d y -d d -o-`
-jose jwk gen -i "$tmp" -o $TMP/exc.jwk
-jose jwk pub -i $TMP/exc.jwk -o $TMP/exc.pub.jwk
-
-# Start the server
-export PORT=$(random_port)
-start_server "${PORT}"
-export PID=$!
-sleep 0.5
-
-# Make sure that GET fails
-curl -sf http://127.0.0.1:$PORT/rec && expected_fail
-curl -sf http://127.0.0.1:$PORT/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 \
+rec_startup () {
+    # Generate the server keys
+    tangd-keygen $TMP/db sig exc
+    # Make sure keys generated by tangd-keygen have proper permissions.
+    valid_key_perm "${TMP}/db/sig.jwk"
+    valid_key_perm "${TMP}/db/exc.jwk"
+
+    # Generate the client keys
+    exc_kid=`jose jwk thp -i $TMP/db/exc.jwk`
+    tmp=`jose fmt -j $TMP/db/exc.jwk -Od x -d y -d d -o-`
+    jose jwk gen -i "$tmp" -o $TMP/exc.jwk
+    jose jwk pub -i $TMP/exc.jwk -o $TMP/exc.pub.jwk
+}
+
+rec_second_phase () {
+    # Make sure that GET fails
+    curl -sf http://127.0.0.1:$PORT/rec && expected_fail
+    curl -sf http://127.0.0.1:$PORT/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/rec/${exc_kid} < $TMP/exc.pub.jwk`
-[ "$good" = "$test" ]
+    [ "$good" = "$test" ]
+}

+ 33 - 0
tests/rec-socat

@@ -0,0 +1,33 @@
+#!/bin/sh -ex
+# vim: set tabstop=8 shiftwidth=4 softtabstop=4 expandtab smarttab colorcolumn=80:
+#
+# Copyright (c) 2016 Red Hat, Inc.
+# Author: Nathaniel McCallum <npmccallum@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
+export PORT=$(random_port)
+start_server "${PORT}"
+export PID=$!
+wait_for_port ${PORT}
+
+rec_second_phase

+ 33 - 0
tests/rec-standalone

@@ -0,0 +1,33 @@
+#!/bin/sh -ex
+# vim: set tabstop=8 shiftwidth=4 softtabstop=4 expandtab smarttab colorcolumn=80:
+#
+# Copyright (c) 2016 Red Hat, Inc.
+# Author: Nathaniel McCallum <npmccallum@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
+export PORT=$(random_port)
+start_standalone_server "${PORT}"
+export PID=$!
+wait_for_port ${PORT}
+
+rec_second_phase

+ 6 - 1
units/meson.build

@@ -3,6 +3,11 @@ tangd_service = configure_file(
   output: 'tangd@.service',
   configuration: data
 )
+tangd_socket = configure_file(
+  input: 'tangd.socket.in',
+  output: 'tangd.socket',
+  configuration: data
+)
 if host_machine.system() == 'freebsd'
   tangd_rc = configure_file(
     input: 'tangd.rc.in',
@@ -12,7 +17,7 @@ if host_machine.system() == 'freebsd'
     install_mode: ['rwxr-xr-x', 'root', 'wheel']
   )
 else
-  units += join_paths(meson.current_source_dir(), 'tangd.socket')
+  units += tangd_socket
   units += tangd_service
 endif
 

+ 3 - 21
units/tangd.rc.in

@@ -4,44 +4,26 @@
 #
 
 # Should probably in the future allow running as non-root
-# and enable multiple interfaces in some cleaner way.
+# and enable multiple interfaces in some way in the future.
 
 # PROVIDE: tangd
 # REQUIRE: NETWORKING DAEMON
-# KEYWORD: nojail
 
 . /etc/rc.subr
 
 name="tangd"
 desc="Network Presence Binding Daemon (tang)"
 rcvar="tangd_enable"
-command="/usr/local/bin/socat"
 
 load_rc_config $name
 : ${tangd_enable:=no}
-: ${tangd_ip="127.0.0.1"}
 : ${tangd_port="8888"}
 : ${tangd_jwkdir="@jwkdir@"}
 : ${tangd_logfile="/var/log/tang"}
-: ${tangd_executable="@libexecdir@/tangd"}
 
-pidfile="/var/run/${name}.pid"
-required_files="@libexecdir@/${name}"
 required_dirs="${tangd_jwkdir}"
 
-start_postcmd="${name}_poststart"
-_tangd_listen_args="TCP-LISTEN:${tangd_port},bind=${tangd_ip},fork"
-command_args="${_tangd_listen_args} SYSTEM:\"${tangd_executable} ${tangd_jwkdir} 2>> ${tangd_logfile} \" &"
-
-# Since we may not be the only socat running we can't use the built-in process
-# management, so we'll need to use a pid file and find the pid from unique arguments.
-tangd_poststart() {
-  ps_pid=`ps ax -o pid= -o command= | grep ${_tangd_listen_args} | grep -v grep | awk '{print $1}'`
-  if [ -z "$ps_pid" ]; then
-    err 1 "Cannot get pid for ${command} ${command_args}"
-  fi
-  echo $ps_pid > ${pidfile}
-  return $?
-}
+command="@libexecdir@/${name}"
+command_args="-p ${tangd_port} -l ${tangd_jwkdir} 2>> ${tangd_logfile} &"
 
 run_rc_command "$1"

+ 0 - 9
units/tangd.socket

@@ -1,9 +0,0 @@
-[Unit]
-Description=Tang Server socket
-
-[Socket]
-ListenStream=80
-Accept=true
-
-[Install]
-WantedBy=sockets.target

+ 13 - 0
units/tangd.socket.in

@@ -0,0 +1,13 @@
+[Unit]
+Description=Tang Server socket
+Documentation=man:tang(8)
+
+[Socket]
+ListenStream=80
+Accept=true
+
+ExecStartPre=-/usr/bin/chmod --silent 0440 -- @jwkdir@/*.jwk @jwkdir@/.*.jwk
+ExecStartPre=-/usr/bin/chown -R @user@:@group@ @jwkdir@
+
+[Install]
+WantedBy=sockets.target

+ 2 - 0
units/tangd@.service.in

@@ -1,5 +1,6 @@
 [Unit]
 Description=Tang Server
+Documentation=man:tang(8)
 
 [Service]
 StandardInput=socket
@@ -7,3 +8,4 @@ StandardOutput=socket
 StandardError=journal
 ExecStart=@libexecdir@/tangd @jwkdir@
 User=@user@
+Group=@group@