diff options
| author | tv <tv@krebsco.de> | 2026-11-07 22:35:21 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-11-07 22:35:21 +0100 |
| commit | 395f063d9128b42f76015f0711a69b08eef26d5c (patch) | |
| tree | fbc7957b597108e80e7a4131c92db6a73383cf19 /scripts | |
| parent | 42698f42eec18d24a7378fcbe1ede73add902b71 (diff) | |
scripts: sealing and opening in the reference client
continuum-signed seal and open, with age, for the readers declared or
CONTINUUM_READERS.
Auth plan M8.
Diffstat (limited to 'scripts')
| -rwxr-xr-x | scripts/continuum-signed | 72 |
1 files changed, 67 insertions, 5 deletions
diff --git a/scripts/continuum-signed b/scripts/continuum-signed index 073c840..4ce68db 100755 --- a/scripts/continuum-signed +++ b/scripts/continuum-signed @@ -1,8 +1,9 @@ #!/bin/sh -# A reference client for a swarm's values signed by their users and logins by key -# (docs/access.md, Values signed by their users; Tokens), with OpenSSH's ssh-keygen, -# curl and jq. What it does, a client of any language can: nothing here is secret but -# the user's key, which never leaves ssh-keygen (or its agent). +# A reference client for a swarm's values signed by their users, values sealed for +# their readers and logins by key (docs/access.md: Values signed by their users, Values +# sealed for their readers, Tokens), with OpenSSH's ssh-keygen, age, curl and jq. What +# it does, a client of any language can: nothing here is secret but the user's key, +# which never leaves ssh-keygen (or its agent) and age. # # continuum-signed login a token, by key (printed) # continuum-signed put PATH JSON writes JSON at PATH, signed @@ -10,6 +11,9 @@ # continuum-signed get PATH the envelope at PATH, as stored # continuum-signed verify PATH checks the envelope at PATH: for PATH, signed # by one of its user's keys as the registry says +# continuum-signed seal PATH JSON writes JSON at PATH sealed for its readers, +# signed +# continuum-signed open PATH the JSON sealed at PATH, opened # # PATH is the store's (`config/mode`), its segments without `/` or characters that need # escaping in a URL. Settings, from the environment: @@ -19,7 +23,11 @@ # CONTINUUM_KEY the private key ssh-keygen signs with, or its public half when an # agent holds it (default ~/.ssh/id_ed25519) # CONTINUUM_TOKEN a token to use; without, each command logs in by key once +# CONTINUUM_READERS whom `seal` seals for (users, comma-separated); without, the +# readers the swarm declares for PATH, else the user alone # +# `seal` seals for every key of each reader, as the registry has them; `open` opens with +# CONTINUUM_KEY, which must then be the private key itself (age reads no agent). # `verify` checks one level: the envelope against the user's keys as the node shows # the registry. The registry's entries are values signed by their users too; checking # them, and theirs, up to users one trusts, is `verify` again, on each. @@ -98,6 +106,58 @@ write() { -H 'Content-Type: application/continuum-signed' --data-binary @- } +# Whom a value at $1 is sealed for, one user a line. +readers_of() { + if [ -n "${CONTINUUM_READERS:-}" ]; then + printf '%s\n' "$CONTINUUM_READERS" | tr ',' '\n' + return + fi + readers=$(curl -sf -H "Authorization: Bearer $CONTINUUM_TOKEN" \ + "$url/store/_continuum/access/sealed" 2>/dev/null | + jq -r --arg path "$1" ' + ($path | split("/")) as $p + | [.[]? | select(type == "object" and (.path | length) == ($p | length) + and ([.path, $p] | transpose | all(.[0] == "*" or .[0] == .[1])))] + | first | .readers // empty | .[]' 2>/dev/null) || readers= + if [ -n "$readers" ]; then + printf '%s\n' "$readers" + else + printf '%s\n' "$user" + fi +} + +# Writes JSON $2 at $1 sealed for its readers (`readers_of`), signed. +seal() { + command -v age >/dev/null || die "needs age" + printf %s "$2" | jq -c . >"$work/plain" 2>/dev/null || die "not JSON: $2" + readers_of "$1" | while read -r reader; do + curl -sf -H "Authorization: Bearer $CONTINUUM_TOKEN" \ + "$url/store/_continuum/users/$reader" | jq -r '(.keys // {})[]' || + die "no user $reader" + done >"$work/recipients" + [ -s "$work/recipients" ] || die "none of the readers has a key" + age -R "$work/recipients" -o "$work/sealed" "$work/plain" || die "age couldn't seal" + sealed="cs1.$(base64 <"$work/sealed" | tr -d '\n=' | tr '/+' '_-')" + write "$1" "$(jq -n --arg sealed "$sealed" '$sealed')" +} + +# The JSON sealed at $1, opened with the user's key. +open_sealed() { + command -v age >/dev/null || die "needs age" + stored=$(raw "$1") + case $stored in + cv1.*) sealed=$(printf %s "$stored" | cut -d. -f2 | unb64url | jq -r '.value // empty') ;; + *) sealed=$stored ;; + esac + case $sealed in + cs1.*) ;; + *) die "no sealed value at $1" ;; + esac + printf %s "${sealed#cs1.}" | unb64url >"$work/sealed" + age -d -i "$key" "$work/sealed" || die "couldn't open it with $key" + echo +} + verify() { envelope=$(raw "$1") case $envelope in @@ -125,7 +185,7 @@ verify() { command=${1:-} [ $# -gt 0 ] && shift case $command in - put | delete | get | verify) + put | delete | get | verify | seal | open) # One login for all the command's requests, unless given a token. CONTINUUM_TOKEN=${CONTINUUM_TOKEN:-$(login)} [ -n "$CONTINUUM_TOKEN" ] || die "couldn't log in as $user" @@ -137,5 +197,7 @@ case $command in delete) [ $# -eq 1 ] || die "delete PATH"; write "$1" null ;; get) [ $# -eq 1 ] || die "get PATH"; raw "$1"; echo ;; verify) [ $# -eq 1 ] || die "verify PATH"; verify "$1" ;; + seal) [ $# -eq 2 ] || die "seal PATH JSON"; seal "$1" "$2" ;; + open) [ $# -eq 1 ] || die "open PATH"; open_sealed "$1" ;; *) sed -n '2,/^$/s/^# \{0,1\}//p' "$0" >&2; exit 2 ;; esac |
