docs: updated to new auth challenge format and removed stale TOCTOU race condition note
Some checks failed
ci/woodpecker/pr/server-lint Pipeline failed
ci/woodpecker/pr/server-audit Pipeline failed
ci/woodpecker/pr/server-vet Pipeline failed
ci/woodpecker/pr/server-test Pipeline was successful
ci/woodpecker/pr/useragent-analyze Pipeline failed

This commit is contained in:
Skipper
2026-04-17 18:25:31 +02:00
parent 9ee86afc19
commit 4a8e51ef32
2 changed files with 9 additions and 25 deletions

View File

@@ -29,38 +29,23 @@ flowchart TD
A([Client connects]) --> B[Receive AuthChallengeRequest] A([Client connects]) --> B[Receive AuthChallengeRequest]
B --> C{pubkey in DB?} B --> C{pubkey in DB?}
C -- yes --> D[Read nonce\nIncrement nonce in DB] C -- yes --> G[Generate AuthChallenge]
D --> G
C -- no --> E[Ask all UserAgents:\nClientConnectionRequest] C -- no --> E[Ask all UserAgents:\nClientConnectionRequest]
E --> F{First response} E --> F{First response}
F -- denied --> Z([Reject connection]) F -- denied --> Z([Reject connection])
F -- approved --> F2[Cancel remaining\nUserAgent requests] F -- approved --> F2[Cancel remaining\nUserAgent requests]
F2 --> F3[INSERT client\nnonce = 1] F2 --> F3[INSERT client]
F3 --> G[Send AuthChallenge\nwith nonce] F3 --> G
G --> H[Receive AuthChallengeSolution] G --> H[Send AuthChallenge\ntimestamp + random bytes]
H --> I{Signature valid?} H --> I[Receive AuthChallengeSolution]
I -- no --> Z I --> K{Signature valid?}
I -- yes --> J([Session started]) K -- no --> Z
K -- yes --> J([Session started])
``` ```
### Known Issue: Concurrent Registration Race (TOCTOU) Auth challenges are generated from fresh random bytes plus a timestamp. They are signed as the canonical challenge payload and are not persisted in `program_client`.
Two connections presenting the same previously-unknown public key can race through the approval flow simultaneously:
1. Both check the DB → neither is registered.
2. Both request approval from user agents → both receive approval.
3. Both `INSERT` the client record → the second insert silently overwrites the first, resetting the nonce.
This means the first connection's nonce is invalidated by the second, causing its challenge verification to fail. A fix requires either serialising new-client registration (e.g. an in-memory lock keyed on pubkey) or replacing the separate check + insert with an `INSERT OR IGNORE` / upsert guarded by a unique constraint on `public_key`.
### Nonce Semantics
The `program_client.nonce` column stores the **next usable nonce** — i.e. it is always one ahead of the nonce last issued in a challenge.
- **New client:** inserted with `nonce = 1`; the first challenge is issued with `nonce = 0`.
- **Existing client:** the current DB value is read and used as the challenge nonce, then immediately incremented within the same exclusive transaction, preventing replay.
--- ---

View File

@@ -71,7 +71,6 @@ create unique index if not exists uniq_metadata_binding_client on client_metadat
create table if not exists program_client ( create table if not exists program_client (
id integer not null primary key, id integer not null primary key,
nonce integer not null default(1), -- used for auth challenge
public_key blob not null, public_key blob not null,
metadata_id integer not null references client_metadata (id) on delete cascade, metadata_id integer not null references client_metadata (id) on delete cascade,
created_at integer not null default(unixepoch ('now')), created_at integer not null default(unixepoch ('now')),