diff --git a/src/tangd.c b/src/tangd.c index 9ae3739..ced38f6 100644 --- a/src/tangd.c +++ b/src/tangd.c @@ -178,6 +178,13 @@ rec(http_method_t method, const char *path, const char *body, if (json_object_set_new(rep, "key_ops", json_pack("[s]", "deriveKey")) < 0) return http_reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, NULL); + json_auto_t *pub = json_deep_copy(jwk); + if (!pub || !jose_jwk_pub(NULL, pub)) + return http_reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, NULL); + + if (json_object_set(rep, "tang_pub", pub) < 0) + return http_reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, NULL); + enc = json_dumps(rep, JSON_SORT_KEYS | JSON_COMPACT); if (!enc) return http_reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, NULL); @@ -188,10 +195,32 @@ rec(http_method_t method, const char *path, const char *body, "\r\n%s", strlen(enc), enc); } +static int +tang_version(http_method_t method, const char *path, const char *body, + regmatch_t matches[], void *misc) +{ + __attribute__((cleanup(str_cleanup))) char *out = NULL; + json_auto_t *ver = json_pack("{s:s, s:{s:b}}", + "tang_version", VERSION, + "features", "tang_pub", 1); + if (!ver) + return http_reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, NULL); + + out = json_dumps(ver, JSON_SORT_KEYS | JSON_COMPACT); + if (!out) + return http_reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, NULL); + + return http_reply(HTTP_STATUS_OK, + "Content-Type: application/json\r\n" + "Content-Length: %zu\r\n" + "\r\n%s", strlen(out), out); +} + 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_-]+)$" }, + { 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_-]+)$" }, + { tang_version, 1 << HTTP_GET, 1, "^/+version/*$" }, {} }; @@ -327,6 +356,7 @@ main(int argc, char *argv[]) char adv_thp_endpoint[MAX_URL] = {}; char adv_endpoint[MAX_URL] = {}; char rec_endpoint[MAX_URL] = {}; + char ver_endpoint[MAX_URL] = {}; if (endpoint != NULL) { char *endpoint_ptr = (char*)endpoint; while (*endpoint_ptr == '/') { @@ -335,9 +365,11 @@ main(int argc, char *argv[]) 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); + snprintf(ver_endpoint, MAX_URL, "^/%s/+version/*$", endpoint_ptr); s_dispatch[0].re = adv_thp_endpoint; s_dispatch[1].re = adv_endpoint; s_dispatch[2].re = rec_endpoint; + s_dispatch[3].re = ver_endpoint; } if (listen == 0) { /* process one-shot query from stdin */ return process_request(jwkdir, STDIN_FILENO); diff --git a/tests/adv b/tests/adv index 416dc6e..d16e4e2 100755 --- a/tests/adv +++ b/tests/adv @@ -47,6 +47,11 @@ adv_second_phase () { fetch "${ENDPOINT}"/adv | ver $TMP/db/sig.jwk fetch "${ENDPOINT}"/adv/ | ver $TMP/db/sig.jwk + # Verify /version endpoint reports tang_pub capability + ver_resp=$(fetch "${ENDPOINT}"/version) + printf '%s' "$ver_resp" | jose fmt -j- -Og tang_version -u- >/dev/null + printf '%s' "$ver_resp" | jose fmt -j- -Og features -Og tang_pub >/dev/null + # Fetching by any thumbprint should work 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 diff --git a/tests/rec b/tests/rec index d30f72f..5d6c842 100755 --- a/tests/rec +++ b/tests/rec @@ -45,11 +45,21 @@ rec_second_phase () { # 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 \ + full_resp=`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` + + # Strip tang_pub from response and verify the exchange result + test=`printf '%s' "$full_resp" | jose fmt -j- -Od tang_pub -o-` [ "$good" = "$test" ] + + # Verify tang_pub is present and contains no private material + printf '%s' "$full_resp" | jose fmt -j- -Og tang_pub -o- > $TMP/tang_pub.jwk + ! jose fmt -j $TMP/tang_pub.jwk -Og d 2>/dev/null + + # Verify tang_pub thumbprint matches the requested key identifier + [ "$(jose jwk thp -i $TMP/tang_pub.jwk)" = "$exc_kid" ] } rec_second_phase_endpoint () { @@ -59,9 +69,19 @@ rec_second_phase_endpoint () { # 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 \ + full_resp=`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` + + # Strip tang_pub from response and verify the exchange result + test=`printf '%s' "$full_resp" | jose fmt -j- -Od tang_pub -o-` [ "$good" = "$test" ] + + # Verify tang_pub is present and contains no private material + printf '%s' "$full_resp" | jose fmt -j- -Og tang_pub -o- > $TMP/tang_pub.jwk + ! jose fmt -j $TMP/tang_pub.jwk -Og d 2>/dev/null + + # Verify tang_pub thumbprint matches the requested key identifier + [ "$(jose jwk thp -i $TMP/tang_pub.jwk)" = "$exc_kid" ] } diff --git a/tests/verify-tang-pub b/tests/verify-tang-pub new file mode 100755 index 0000000..cc71dfa --- /dev/null +++ b/tests/verify-tang-pub @@ -0,0 +1,78 @@ +#!/bin/sh -e +# vim: set tabstop=8 shiftwidth=4 softtabstop=4 expandtab smarttab colorcolumn=80: +# +# Verify that a running Tang server returns tang_pub in /rec/$kid responses. +# +# Usage: verify-tang-pub [jwkdir] [url] +# jwkdir: path to Tang key directory (default: /var/db/tang) +# url: Tang server URL (default: http://localhost) + +JWKDIR="${1:-/var/db/tang}" +URL="${2:-http://localhost}" + +if [ ! -d "$JWKDIR" ]; then + echo "Error: key directory $JWKDIR does not exist" >&2 + exit 1 +fi + +exc_file="" +for f in "$JWKDIR"/*.jwk; do + [ -f "$f" ] || continue + alg=$(jose fmt -j "$f" -Og alg -u- 2>/dev/null) || continue + if [ "$alg" = "ECMR" ]; then + exc_file="$f" + break + fi +done + +if [ -z "$exc_file" ]; then + echo "Error: no ECMR exchange key found in $JWKDIR" >&2 + exit 1 +fi + +exc_kid=$(jose jwk thp -i "$exc_file") +echo "Exchange key: $(basename "$exc_file")" +echo "Thumbprint: $exc_kid" + +TMP=$(mktemp -d) +trap 'rm -rf "$TMP"' EXIT + +ver_resp=$(curl -sf "$URL/version" 2>/dev/null) || { + echo "FAIL: /version endpoint not available (server may not support tang_pub)" >&2 + exit 1 +} + +if ! printf '%s' "$ver_resp" | jose fmt -j- -Og features -Og tang_pub >/dev/null 2>&1; then + echo "FAIL: /version does not report tang_pub capability" >&2 + exit 1 +fi +echo "OK: /version reports tang_pub capability" + +jose jwk gen -i '{"alg":"ECMR"}' -o "$TMP/client.jwk" +jose jwk pub -i "$TMP/client.jwk" -o "$TMP/client.pub.jwk" + +resp=$(curl -sf -X POST \ + -H "Content-Type: application/jwk+json" \ + --data-binary @"$TMP/client.pub.jwk" \ + "$URL/rec/$exc_kid" 2>/dev/null) || { + echo "Error: /rec/$exc_kid request failed" >&2 + exit 1 +} + +tang_pub=$(printf '%s' "$resp" | jose fmt -j- -Og tang_pub -o- 2>/dev/null) || { + echo "FAIL: tang_pub not present in /rec/$exc_kid response" >&2 + exit 1 +} + +if printf '%s' "$tang_pub" | jose fmt -j- -Og d 2>/dev/null >/dev/null; then + echo "FAIL: tang_pub contains private material" >&2 + exit 1 +fi + +tang_pub_thp=$(printf '%s' "$tang_pub" | jose jwk thp -i-) +if [ "$tang_pub_thp" != "$exc_kid" ]; then + echo "FAIL: tang_pub thumbprint ($tang_pub_thp) does not match key identifier ($exc_kid)" >&2 + exit 1 +fi + +echo "OK: tang_pub is present, public-only, and matches key identifier"