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 | 42698f42eec18d24a7378fcbe1ede73add902b71 (patch) | |
| tree | 5fff4e139d56ae9e30daace4dad8c731fbffa5f1 /docs | |
| parent | 64b7b2a0a94bfe92be84219c195e6335609a23e3 (diff) | |
access: values sealed for their readers
cs1.<age file>: a value encrypted with age to its readers' SSH keys,
which no node decrypts. _continuum/access/sealed declares paths whose
values must be sealed, for named readers or anyone; nodes check the
recipients by age's key tags, at the API and on receipt.
Auth plan M8, reworked: sealed values instead of containers, readers
as users only (nodes hold no keys).
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/access.md | 37 | ||||
| -rw-r--r-- | docs/api.md | 8 | ||||
| -rw-r--r-- | docs/openapi.yaml | 4 |
3 files changed, 47 insertions, 2 deletions
diff --git a/docs/access.md b/docs/access.md index 57462db..6f89b49 100644 --- a/docs/access.md +++ b/docs/access.md @@ -290,3 +290,40 @@ that checks, replacing the one there then; nothing within one; and above one, no replacing or deleting what holds one (a map made where there is none yet is fine). So a member writing to its document directly gets nothing there past the others: the registry, the policy and the bans change only as a user who may change them signed. + +## Values sealed for their readers + +Anyone may read the whole document: every member holds it. A value only some may read +is sealed for them: encrypted with age to their SSH keys (`keys`, The registry, +above), so only they open it. Every node syncs, stores and remakes it as the string it +is, and none decrypts it: `cs1.<the age file, base64url>` (binary, not armored). + +```sh +jq -r '.keys[]' <(curl -s -u alice localhost:3000/store/_continuum/users/bob) > readers +printf %s "$secret" | age -R readers | basenc --base64url -w0 | tr -d = | + sed 's/^/cs1./' | jq -R . | curl -u alice -X PUT localhost:3000/store/secrets/db -d @- +``` + +Any value anywhere may be sealed, for anyone. Where the swarm declares it, every value +must be: `_continuum/access/sealed`, a list of `{"path": [...], "readers": [...]}`, `*` +in a path for any one key, written as any of `_continuum/access` (under a strict +policy, signed). + +```json +[{"path": ["secrets", "*"], "readers": ["alice", "bob"]}, {"path": ["notes"]}] +``` + +Without `readers`, a value there must be sealed, for anyone; with them, sealed for +exactly their keys, as the registry has them when it is written. A node tells without +decrypting: an age file names each `ssh-ed25519` recipient by a tag, the first four +bytes of the SHA-256 of its key. A write leaving a value there plain, or sealed for a +reader's key too few or a stranger's too many, is refused (`409`), and with the policy +every node refuses such a change on receipt (Changes judged on every node, above). A +value signed by its user (Values signed by their users) holds the sealed value; the +signature is over the ciphertext, so every node checks who wrote it without reading +it. + +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 +are, their sizes, when and by whom they were written stay visible to every member. diff --git a/docs/api.md b/docs/api.md index 92289bf..7d7a563 100644 --- a/docs/api.md +++ b/docs/api.md @@ -302,6 +302,14 @@ and a read of one names its signer (`x-continuum-signed-by`). `?raw=true` shows they are stored, envelopes, which anyone can check; so do typed JSON and native reads, and streams (`/stream`). +## Sealed values + +A value may be sealed for its readers, encrypted with age to their SSH keys +([`access.md`](access.md), Values sealed for their readers): a string, +`cs1.<age file>`, that the API stores and shows as it is. Where the swarm declares +values sealed, a write leaving one there plain, or sealed for others than the declared +readers' keys, is `409`. + ## Protocol data `_continuum` holds what the nodes coordinate through: the swarm ID, the member registry, diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 1bd3a16..ab14710 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -208,7 +208,7 @@ paths: '403': description: The user may not write here (its capabilities, docs/access.md; in a swarm without a user registry, protocol data `_continuum` and the user isn't in `protocol-writers`), or epoch metadata or the swarm ID (`_continuum/epochs`, `_continuum/swarm`, never writable); a signed value not by this user, or whose signature doesn't check (the message says why). '409': - description: The path crosses an existing non-map value, or with `retype=false` the write would change a type (the message says where); a value not signed reaching one its user signs under a strict policy; a signed value where values aren't signed, or replacing another than the one there (read again). + description: The path crosses an existing non-map value, or with `retype=false` the write would change a type (the message says where); a value not signed reaching one its user signs under a strict policy; a signed value where values aren't signed, or replacing another than the one there (read again); a value left not sealed as the swarm declares (`docs/access.md`, Values sealed for their readers). '415': description: Content-Type is none the API takes (`application/json`, the default; `text/plain`; a type's own, `api.md`, Types). '500': @@ -300,7 +300,7 @@ paths: '307': description: A negative index, made positive (`Location`). '409': - description: Not a list there, or the index is past its end (the message says which, and the length); with `increment`, not a counter there; with `cast`, no cast possible (the message says why); under a strict policy, reaching a value its user signs. + description: Not a list there, or the index is past its end (the message says which, and the length); with `increment`, not a counter there; with `cast`, no cast possible (the message says why); under a strict policy, reaching a value its user signs; a value left not sealed as the swarm declares. '415': description: Content-Type is none the API takes (`application/json`, the default; `text/plain`; a type's own, `api.md`, Types). '503': |
