aboutsummaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-07 22:35:21 +0100
committertv <tv@krebsco.de>2026-11-07 22:35:21 +0100
commit7fed3e4478ad28151fab288d569a1268c1f3e4cf (patch)
tree1d406a10393b16b7f85295e14b855ca5ac7c741f /docs
parent41657a4d749b1f1d44663ff6504f2e784f091537 (diff)
api: signed values at the store
PUT with application/continuum-signed writes an envelope where values are signed (403/409 saying why); reads open envelopes and name the signer (x-continuum-signed-by), ?raw=true shows them. Under strict, unsigned writes reaching a signed value and the users API are 409. Auth plan M7.
Diffstat (limited to 'docs')
-rw-r--r--docs/access.md32
-rw-r--r--docs/api.md30
-rw-r--r--docs/openapi.yaml45
3 files changed, 99 insertions, 8 deletions
diff --git a/docs/access.md b/docs/access.md
index 682b22b..0366e26 100644
--- a/docs/access.md
+++ b/docs/access.md
@@ -29,6 +29,9 @@ password hashes: every member reads the document).
* **`disabled`**: the entry stays (who wrote what stays readable), and gives nothing.
An entry that doesn't read as above gives less, never more: a capability that
doesn't read is left out, and anything but `false` disables.
+* **`keys`**: the user's SSH keys, each by a name of its choosing, as an
+ `authorized_keys` line has it (`"laptop": "ssh-ed25519 AAAA…"`; Ed25519 only): what
+ it signs values with (Values signed by their users, below).
## A new swarm
@@ -231,3 +234,32 @@ curl -X PUT localhost:3000/store/_continuum/banned/<node ID> \
-H 'Content-Type: application/json' -d '{"by": "alice", "reason": "compromised"}'
```
+## Values signed by their users
+
+A member's node signs its changes, but the user a change names is its node's word: a
+member could write as any user. A swarm can have its users sign the values that
+matter themselves, with their SSH keys, so no member can write them but as a user
+signed them: the policy's `"values": "signed"` (strict; it requires signatures too).
+
+```sh
+curl -X PUT localhost:3000/store/_continuum/access/policy \
+ -H 'Content-Type: application/json' -d '{"version": 1, "values": "signed"}'
+```
+
+Signed then are what decides who may do what (every user's entry, every entry of
+`_continuum/access`, every ban), and the paths listed at `_continuum/access/userSigned`,
+a list of paths as capabilities name them, `*` for any one key:
+`[["config"], ["servers", "*", "secret"]]`. Each such value is an envelope, one string
+(`cv1.<payload>.<signature>`): the payload names the path, the value (`null`: deleted),
+the user, when, and the envelope it replaces (`prev`), and the signature is the user's
+over it, as `ssh-keygen -Y sign -n continuum-value` makes it, with one of its `keys`.
+A node takes one only from that user, for that path, replacing what is there, by one
+of the user's keys, and if the user may write there ([`api.md`](api.md), Signed values);
+it writes nothing else there, nor within or above such a value (`409`), and the users
+API changes nothing (`409`). Values there before the policy stay as they are until
+replaced: a registry whose entries hold the users' keys turns strict as it is. Without
+the policy, signed values may be written all the same, and plain ones too.
+
+What it doesn't do yet: **judge signed values on receipt.** Each node checks the
+values its users write through it; a member writing to its document directly could
+still write there.
diff --git a/docs/api.md b/docs/api.md
index 5d94fac..92289bf 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -272,6 +272,36 @@ a second (SIGNATURES), to each peer that wasn't sent them yet.
The signatures a node has are kept in its document file (`--document-file`) with the
document.
+## Signed values
+
+Where the swarm has its users sign values ([`access.md`](access.md), Values signed by
+their users), a user writes one by signing it with its SSH key: the payload, one line
+of JSON, then the signature, as `application/continuum-signed`.
+
+```sh
+# The envelope there, if any, and what replaces it names: its SHA-256 (none: "").
+read=$(curl -sf -u alice -H 'Accept: text/plain' 'localhost:3000/store/config?raw=true')
+prev=${read:+$(printf %s "$read" | sha256sum | cut -d' ' -f1)}
+printf '{"path":["config"],"value":{"mode":"fast"},"user":"alice","at":%s,"prev":"%s"}' \
+ "$(date +%s000)" "$prev" > payload
+ssh-keygen -Y sign -f ~/.ssh/id_ed25519 -n continuum-value payload
+{ cat payload; echo; cat payload.sig; } | curl -X PUT localhost:3000/store/config \
+ -u alice -H 'Content-Type: application/continuum-signed' --data-binary @-
+```
+
+The node writes it as its envelope if the authenticated user signed it (`403` if not),
+for this path, with one of its keys, may write there (`403`), and it replaces what is
+there (`prev`, the SHA-256 of the envelope it replaces; `409` if it names another:
+read again and sign anew), and only where values are signed (`409` elsewhere). It
+answers with the value, as a `PUT` does; a signed `null` deletes, with `204`. Under
+the policy, every other write reaching a signed value, at it, within it or above it,
+is `409`, and so is the users API's.
+
+A read opens envelopes: each shows as the value it holds, a signed `null` as nothing,
+and a read of one names its signer (`x-continuum-signed-by`). `?raw=true` shows them as
+they are stored, envelopes, which anyone can check; so do typed JSON and native reads,
+and streams (`/stream`).
+
## 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 1954dc0..6abfc6d 100644
--- a/docs/openapi.yaml
+++ b/docs/openapi.yaml
@@ -98,11 +98,22 @@ paths:
timestamps as `rfc3339` (the default), `unix`, `unix-ms` or `http-date`.
Concurrent writes resolve as follows: concurrent maps merge, anything else
keeps the newest write.
+
+ A value its user signed (`api.md`, Signed values) shows as the value it holds
+ (a signed `null` as nothing), and a read of one names its signer in
+ `x-continuum-signed-by`; as typed JSON, natively and with `raw`, as its
+ envelope.
parameters:
- $ref: '#/components/parameters/conflicts'
- $ref: '#/components/parameters/heads'
- $ref: '#/components/parameters/shallow'
- $ref: '#/components/parameters/at'
+ - name: raw
+ in: query
+ description: Values their users signed as stored, as envelopes (`cv1.<payload>.<signature>`), to check them.
+ schema:
+ type: boolean
+ default: false
responses:
'200':
$ref: '#/components/responses/Value'
@@ -146,6 +157,11 @@ paths:
`.float`, `.boolean`, `.counter` (decimal or `true`/`false`), or
`application/vnd.continuum.timestamp; format=rfc3339|unix|unix-ms|http-date`;
a body that isn't one is `400`, saying why.
+
+ A value its user signed (`application/continuum-signed`: the payload's line of
+ JSON, then its armored SSH signature; `api.md`, Signed values) is written as its
+ envelope where values are signed: by the authenticated user, for this path,
+ with one of its keys, replacing what is there. A signed `null` deletes (`204`).
parameters:
- name: retype
in: query
@@ -172,17 +188,27 @@ paths:
schema:
type: string
example: '2026-11-01T21:50:20.345Z'
+ application/continuum-signed:
+ schema:
+ type: string
+ example: |
+ {"path":["config"],"value":{"mode":"fast"},"user":"alice","at":1762400000000,"prev":""}
+ -----BEGIN SSH SIGNATURE-----
+ U1NIU0lH…
+ -----END SSH SIGNATURE-----
responses:
'200':
$ref: '#/components/responses/Value'
+ '204':
+ description: A signed `null` written; nothing there now.
'400':
description: Invalid path, malformed JSON or invalid UTF-8.
'401':
$ref: '#/components/responses/Unauthorized'
'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).
+ 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).
+ 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).
'415':
description: Content-Type is none the API takes (`application/json`, the default; `text/plain`; a type's own, `api.md`, Types).
'500':
@@ -274,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).
+ 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.
'415':
description: Content-Type is none the API takes (`application/json`, the default; `text/plain`; a type's own, `api.md`, Types).
'503':
@@ -307,6 +333,8 @@ paths:
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).
'404':
description: No value at this path.
+ '409':
+ description: Under a strict policy, reaching a value its user signs (`PUT` a signed `null` instead).
'500':
description: Deleting failed.
'503':
@@ -914,7 +942,7 @@ paths:
'401':
$ref: '#/components/responses/Unauthorized'
'403': { description: May not manage users. }
- '409': { description: A user exists, or the caller wouldn't manage users in a new registry. }
+ '409': { description: "A user exists, the caller wouldn't manage users in a new registry, or the policy has users sign their entries (strict, docs/access.md)." }
/admin/users/{name}:
parameters:
@@ -947,7 +975,7 @@ paths:
'201': { description: Added. }
'400': { description: Not a user name, or a capability that isn't one. }
'403': { description: May not manage users. }
- '409': { description: The user exists. }
+ '409': { description: 'The user exists, or the policy has users sign their entries (strict, docs/access.md).' }
delete:
tags: [Users]
summary: Remove a user
@@ -956,7 +984,7 @@ paths:
'204': { description: Removed. }
'403': { description: May not manage users. }
'404': { description: No such user. }
- '409': { description: It would keep the caller from managing users. }
+ '409': { description: It would keep the caller from managing users, or the policy has users sign their entries (strict, docs/access.md). }
/admin/users/{name}/disabled:
parameters:
@@ -973,7 +1001,7 @@ paths:
'204': { description: Done. }
'403': { description: May not manage users. }
'404': { description: No such user. }
- '409': { description: It would keep the caller from managing users. }
+ '409': { description: It would keep the caller from managing users, or the policy has users sign their entries (strict, docs/access.md). }
/admin/users/{name}/caps:
parameters:
@@ -998,6 +1026,7 @@ paths:
'400': { description: A capability that isn't one. }
'403': { description: May not manage users. }
'404': { description: No such user. }
+ '409': { description: The policy has users sign their entries (strict, docs/access.md). }
/admin/users/{name}/caps/{id}:
parameters:
@@ -1010,7 +1039,7 @@ paths:
'204': { description: Revoked. }
'403': { description: May not manage users. }
'404': { description: No such capability. }
- '409': { description: It would keep the caller from managing users. }
+ '409': { description: It would keep the caller from managing users, or the policy has users sign their entries (strict, docs/access.md). }
/admin/dashboard:
get: