api: merge v3 proofs and add host approval pages and listener
This commit is contained in:
+122
-60
@@ -1,71 +1,133 @@
|
||||
# Host API
|
||||
|
||||
`handle(&Api, Caller, Request<Bytes>)` answers JSON requests for `host.sausage`.
|
||||
The transport supplies the caller from the process table, never from request JSON or headers.
|
||||
The transport hook is outside this module.
|
||||
Use one `Cred` per host process.
|
||||
SQLite and script work must run on a blocking thread in the transport.
|
||||
|
||||
T25 Note 1 pauses authorization routes for security decisions C1 and H2.
|
||||
T27 will change the mutation methods to require typed proofs.
|
||||
This module currently exposes only reads, request filing, and SSH operations.
|
||||
It does not expose decisions, enrollment, revocation, or secret entry.
|
||||
Those routes return 404 until the orchestrator authorizes further work.
|
||||
The API retains the passkey manager and entry key for the later routes.
|
||||
The host supplies their configuration and host-only paths.
|
||||
Tests use RP ID `localhost` and origin `http://localhost:8080`.
|
||||
# Host API and host pages
|
||||
|
||||
`handle(&Api, Caller, Request<Bytes>)` answers requests without a transport.
|
||||
The later `wasi:http` hook supplies the caller from the process table.
|
||||
It must not accept caller identity from headers, environment variables, or JSON.
|
||||
Reads are open to every caller.
|
||||
Successful responses use status 200 and JSON.
|
||||
JSON routes return 200 on success.
|
||||
Errors use `{"error":"kind","message":"kind"}` with static, secret-free text.
|
||||
Unknown routes return 404.
|
||||
Invalid JSON or parameters return 400.
|
||||
Conflicts return 409.
|
||||
Request filing permits 32 attempts per process within a 60-second window.
|
||||
The next attempt returns 429 until that window expires.
|
||||
Explicit requests and automatic SSH requests share this budget.
|
||||
Reads and granted SSH operations do not spend the budget.
|
||||
Missing routes return 404. Invalid parameters return 400. Authorization failures return 403.
|
||||
Conflicts return 409. The filing budget returns 429.
|
||||
|
||||
## Current routes
|
||||
## Host listener
|
||||
|
||||
- `GET /whoami` returns the trusted caller.
|
||||
- `GET /cred/items` returns public item snapshots.
|
||||
- `GET /cred/items/{id}` returns one public snapshot.
|
||||
- `GET /cred/requests?state=pending` returns requests with diffs and summaries.
|
||||
`sausage run WORLD` starts the console and the host listener with one shared `Arc<Cred>`.
|
||||
The listener owns its passkey connection and handles requests on a dedicated thread.
|
||||
The console receives the same credentials store, including its decision notifications.
|
||||
The listener stops when the console returns.
|
||||
No protected harness or process module needs a change.
|
||||
|
||||
Options on `sausage run`:
|
||||
|
||||
- `--api-listen 127.0.0.1:8081`: the default listener address.
|
||||
- `--api-rp-id localhost`: the default WebAuthn RP ID.
|
||||
- `--api-origin http://localhost:8081`: the default browser origin.
|
||||
|
||||
The origin must name the host pages, not the world server.
|
||||
The listener speaks HTTP. A production HTTPS origin needs a reverse proxy to this listener.
|
||||
The world's UI can link to the host pages. It does not serve their code.
|
||||
The listener identifies browser calls as `/host/browser`, with the host PID and no conversation.
|
||||
Passkey proofs supply the approver identity independently of that caller.
|
||||
|
||||
## Pages and assets
|
||||
|
||||
- `GET /approve/{request}` renders the host's stored summary and public details.
|
||||
The page sends only the human's decision and optional limit overrides.
|
||||
The host builds the binding from the stored request and its payload digest.
|
||||
- `GET /register` registers the first passkey or adds another passkey.
|
||||
- `GET /entry/{item}/{field}` displays a blank secret-entry form.
|
||||
The host embeds its public encryption key and the selected field's public metadata.
|
||||
- `GET /app.js` serves the shared ceremony code and WebCrypto encryption.
|
||||
|
||||
The checked-in pages use plain HTML and JavaScript with no build step or external dependencies.
|
||||
The host escapes review data before it enters HTML.
|
||||
No page includes a stored secret value.
|
||||
|
||||
## Public JSON routes
|
||||
|
||||
- `GET /whoami`: the transport's caller.
|
||||
- `GET /cred/items`: public item snapshots.
|
||||
- `GET /cred/items/{id}`: one public snapshot.
|
||||
- `GET /cred/requests?state=pending`: requests with diffs and summaries.
|
||||
Omit `state` to return every request.
|
||||
- `GET /cred/requests/{id}` returns one request with its diff and summary.
|
||||
- `POST /cred/requests` accepts `{"kind":"grant","key":"stable-key","request":{...}}`.
|
||||
- `GET /cred/requests/{id}`: one request with its diff and summary.
|
||||
- `POST /cred/requests`: `{"kind":"grant","key":"stable-key","request":{...}}`.
|
||||
The request contains `items` and optional `duration_secs`, `max_uses`, and `validator`.
|
||||
Kind `edit` accepts an `OperationRequest` with `item`, `base_version`, and `change`.
|
||||
This route returns `{"request":"id"}` without approving the request.
|
||||
The key prefixes `inject:` and `ssh:` belong to automatic requests.
|
||||
An explicit request with either prefix returns 400.
|
||||
- `GET /cred/grants` returns the grant ledger.
|
||||
- `POST /ssh/identity` accepts `{"host":"example.com"}`.
|
||||
It returns `{"item":"id","public_key":"base64 SSH public key blob","user":null}`.
|
||||
Kind `edit` accepts `OperationRequest` with `item`, `base_version`, and `change`.
|
||||
The response is `{"request":"id"}`. Filing does not approve anything.
|
||||
- `GET /cred/grants`: the grant ledger.
|
||||
|
||||
Explicit request keys cannot begin with `inject:` or `ssh:`.
|
||||
Those prefixes belong to automatic requests.
|
||||
Explicit requests, automatic SSH requests, and new ceremonies share a per-process budget.
|
||||
The budget permits 32 attempts per 60-second window.
|
||||
Reads and granted SSH operations do not spend that budget.
|
||||
|
||||
## Decisions
|
||||
|
||||
- `POST /cred/requests/{id}/challenge`: `{"decision":"approve","overrides":{...}}`.
|
||||
Decisions are `approve` or `deny`.
|
||||
Overrides use T27's typed `Overrides` structure. Unknown fields fail.
|
||||
A denial accepts only default overrides.
|
||||
With no passkey, the response is `{"unlocked":true}`.
|
||||
Otherwise, the response is `{"id":"challenge id","options":{...}}`.
|
||||
- `POST /cred/requests/{id}/decide`: `{"challenge_id":"id","assertion":{...}}`.
|
||||
The host reads the decision and overrides from the verified binding.
|
||||
An optional explicit decision must match that binding.
|
||||
The library checks the request ID, decision, overrides, and stored payload digest inside its transaction.
|
||||
Without passkeys, this route accepts `{"decision":"approve","overrides":{...}}` instead.
|
||||
The library checks its `Unlocked` proof inside the transaction.
|
||||
- `POST /cred/grants/{id}/revoke`: `{"key":"stable-key"}`.
|
||||
This route files a revocation request and returns `{"request":"id"}`.
|
||||
Use the normal challenge and decision routes to approve it.
|
||||
|
||||
Successful mutations return `{"ok":true}`.
|
||||
A successful assertion produces a non-cloneable `Verified` proof.
|
||||
A replay or a mismatched binding cannot authorize another action.
|
||||
|
||||
## Passkeys
|
||||
|
||||
- `GET /cred/passkeys`: public credential IDs and labels, not serialized passkeys.
|
||||
- `POST /cred/passkeys/register/challenge`: `{"label":"new label"}`.
|
||||
The returned assertion challenge binds the exact label.
|
||||
- `POST /cred/passkeys/register/start`: `{"label":"new label","gate":{"id":"challenge id","response":{...}}}`.
|
||||
The first registration permits an absent gate, as D53 requires.
|
||||
Every later registration needs the existing passkey's enrollment assertion.
|
||||
- `POST /cred/passkeys/register/finish`: `{"challenge_id":"id","response":{...}}`.
|
||||
|
||||
WebAuthn options and responses use the library's browser JSON formats.
|
||||
The host retains the RP configuration and pending ceremony state.
|
||||
Restart discards pending ceremonies but retains registered passkeys.
|
||||
|
||||
## Secret entry
|
||||
|
||||
- `GET /cred/entry/key`: `{"public_key":[...]}` in SEC1 format.
|
||||
- `POST /cred/items/{id}/fields/{field}/challenge`: `{"ciphertext_sha256":"lowercase hex"}`.
|
||||
The host loads the field metadata and constructs the binding itself.
|
||||
- `POST /cred/items/{id}/fields/{field}`: `{"challenge_id":"id","assertion":{...},"envelope":{...}}`.
|
||||
|
||||
The field parameter is its stable ID.
|
||||
Secret entry requires an assertion. Register a passkey first.
|
||||
The host compares the item, field, and envelope hash before it calls the encrypted field API.
|
||||
That API checks the typed proof and metadata before it decrypts inside its transaction.
|
||||
Public fields cannot use secret entry.
|
||||
Non-UTF-8 plaintext and invalid envelopes fail without an echo.
|
||||
See `cred/entry.rs` for the ECDH, HKDF, AES-GCM, and hash formats.
|
||||
|
||||
## SSH
|
||||
|
||||
- `POST /ssh/identity`: `{"host":"example.com"}`.
|
||||
The response is `{"item":"id","public_key":"base64 SSH public key blob","user":null}`.
|
||||
A public `user` field supplies the optional default user.
|
||||
- `POST /ssh/sign` accepts `{"host":"example.com","item":"id","payload":"base64"}`.
|
||||
It returns `{"signature":"base64 SSH wire signature"}`.
|
||||
- `POST /ssh/sign`: `{"host":"example.com","item":"id","payload":"base64"}`.
|
||||
The response is `{"signature":"base64 SSH wire signature"}`.
|
||||
|
||||
SSH operations match SSH rules and enforce free use or a caller grant.
|
||||
Both operations evaluate item and grant validators.
|
||||
Identity discovery checks the grant without spending a use.
|
||||
Signing reserves one grant use.
|
||||
Both operations evaluate item and grant validators through QuickJS, as D58 requires.
|
||||
Identity discovery checks access without spending a signing use.
|
||||
Signing reserves one use. Preparation commits its pending audit row in the same transaction.
|
||||
Successful operations complete that row. Signing records only the payload hash, never the payload.
|
||||
An ungranted caller receives 403 with `error`, `message`, `item`, and `request`.
|
||||
The host files that request before it responds.
|
||||
Two matching items produce 409.
|
||||
The signing item must match the selected host rule.
|
||||
The host never returns a private key or decrypted field value.
|
||||
|
||||
## Held routes
|
||||
|
||||
The original brief also requests these route groups:
|
||||
|
||||
- Request challenges and decisions.
|
||||
- Passkey enrollment and listing.
|
||||
- Grant revocation through an approved request.
|
||||
- The entry public key, entry challenges, and encrypted field writes.
|
||||
|
||||
Their final contracts depend on the security decisions and T27's typed proof interface.
|
||||
The host must not trust world-served pages to describe the approved action.
|
||||
No public route accepts a caller identity, a raw binding, or an approver string.
|
||||
The host files the request before it responds.
|
||||
Two matching items produce 409. The signing item must match the selected host rule.
|
||||
No SSH response returns a private key or decrypted field value.
|
||||
|
||||
Reference in New Issue
Block a user