aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/access.md4
-rwxr-xr-xscripts/continuum-signed72
2 files changed, 71 insertions, 5 deletions
diff --git a/docs/access.md b/docs/access.md
index 6f89b49..4a0fd0e 100644
--- a/docs/access.md
+++ b/docs/access.md
@@ -323,6 +323,10 @@ value signed by its user (Values signed by their users) holds the sealed value;
signature is over the ciphertext, so every node checks who wrote it without reading
it.
+`scripts/continuum-signed` seals and opens them too, with age: `seal secrets/db
+'{"password": "…"}'` seals for the readers declared there (or `CONTINUUM_READERS`),
+signed, and `open secrets/db` opens it with the user's key.
+
It guards against mistakes, not against a writer: one who means to leak a value has
it. A reader removed keeps what it read: values stay sealed for the readers they were
written for until written again, sealed for those declared then. Which values there
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