145 lines
6.2 KiB
Markdown
145 lines
6.2 KiB
Markdown
|
|
# approval-service
|
||
|
|
|
||
|
|
Phase 1 der ProofedMesh content-gebundenen Approvals ([DEV-608](/DEV/issues/DEV-608),
|
||
|
|
siehe `docs/architecture/v1.1.md §4.3`). Der Dienst nimmt menschliche
|
||
|
|
Freigaben an, bindet sie kryptographisch an einen deterministisch gerenderten
|
||
|
|
JSON-Inhalt (`H_content = sha256(canonical_json)`), verifiziert eine WebAuthn-
|
||
|
|
Assertion gegen die Challenge `nonce || H_content` und erzeugt daraus eine
|
||
|
|
DSSE-signierte Approval-Attestation, die ins Transparency-Log eingetragen
|
||
|
|
wird.
|
||
|
|
|
||
|
|
## Endpoints
|
||
|
|
|
||
|
|
Alle Routen sind JSON-API, Pocket-ID-Session-Cookie-geschützt.
|
||
|
|
|
||
|
|
| Methode | Pfad | Zweck |
|
||
|
|
| ------- | --------------------------- | -------------------------------------------------------------------------------------------------- |
|
||
|
|
| `POST` | `/api/v1/sessions` | Startet eine Approval-Session. Antwort: `{session_id, active_until}`. |
|
||
|
|
| `POST` | `/api/v1/approvals/preview` | Rendert Freigabe-Inhalt serverseitig, berechnet `H_content`, legt Challenge und Prüfziffer ab. |
|
||
|
|
| `POST` | `/api/v1/approvals/confirm` | Nimmt die WebAuthn-Assertion entgegen, signiert und loggt die Approval. |
|
||
|
|
| `GET` | `/healthz` | Liveness. |
|
||
|
|
|
||
|
|
### `POST /api/v1/sessions`
|
||
|
|
|
||
|
|
Erzeugt eine Approval-Session an die aktuelle Pocket-ID-`sub`. `active_until`
|
||
|
|
ist in Phase 1 fest auf `SessionTTL` (Default 15 min).
|
||
|
|
|
||
|
|
### `POST /api/v1/approvals/preview`
|
||
|
|
|
||
|
|
Request:
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"session_id": "sess_…",
|
||
|
|
"content": { "...application-defined..." }
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Response:
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"preview_id": "pv_…",
|
||
|
|
"content_hash": "sha256:<hex>",
|
||
|
|
"verification_digits": "734219",
|
||
|
|
"webauthn_challenge": "<b64url>",
|
||
|
|
"expires_at": "2026-01-01T12:00:00Z"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
`verification_digits` ist eine 6-stellige Prüfziffer aus `H_content`, die der
|
||
|
|
Mensch im UI vor der Freigabe mit der Signaturaufforderung abgleichen MUSS.
|
||
|
|
`webauthn_challenge` ist bereits `nonce || H_content` (b64url), damit der
|
||
|
|
Browser direkt eine WebAuthn-Assertion anfordern kann.
|
||
|
|
|
||
|
|
### `POST /api/v1/approvals/confirm`
|
||
|
|
|
||
|
|
Request:
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"preview_id": "pv_…",
|
||
|
|
"assertion": {
|
||
|
|
"credential_id": "<b64url>",
|
||
|
|
"client_data_json": "<b64url>",
|
||
|
|
"authenticator_data": "<b64url>",
|
||
|
|
"signature": "<b64url>"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Response:
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"approval_id": "ap_…",
|
||
|
|
"attestation_ref": "sha256:<hex>",
|
||
|
|
"log_index": 42,
|
||
|
|
"tlog_status": "logged"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Wenn der TLog-Submit scheitert, bleibt die Row `tlog_status =
|
||
|
|
"attestation-pending"`; die Antwort meldet dann `log_index: null,
|
||
|
|
tlog_status: "attestation-pending"`. Ein späterer Reconciler zieht den
|
||
|
|
Submit nach (ADR-0005, idempotent per `content_hash`).
|
||
|
|
|
||
|
|
## Fehler
|
||
|
|
|
||
|
|
| Zustand | Code | Body `error.code` |
|
||
|
|
| -------------------------------------------- | ---- | ------------------------- |
|
||
|
|
| Session abgelaufen / unbekannt | 403 | `session_expired` |
|
||
|
|
| `H_content`-Mismatch (Assertion vs. Preview) | 400 | `content_hash_mismatch` |
|
||
|
|
| Challenge abgelaufen | 403 | `challenge_expired` |
|
||
|
|
| WebAuthn-Assertion ungültig | 401 | `assertion_invalid` |
|
||
|
|
| Signing-Service nicht erreichbar | 502 | `signer_unavailable` |
|
||
|
|
|
||
|
|
## Phase 1 Scope
|
||
|
|
|
||
|
|
- Store ist in-memory (`internal/store/memory.go`); Postgres-Impl ist in
|
||
|
|
`internal/store/postgres.go` als Skelett angelegt (Pattern aus
|
||
|
|
`admin-api/internal/store/postgres.go`). Die SQL-Migration liegt bereits
|
||
|
|
in `db/trust/migrations/0004_approval_sessions.*.sql`.
|
||
|
|
- WebAuthn-Verifikation ist eine stdlib-only Shim im Stil von
|
||
|
|
`admin-api/internal/auth/pocket_id.go`: In-Memory-`Credential`-Store,
|
||
|
|
Ed25519-Assertion über `authenticator_data || sha256(client_data_json)`,
|
||
|
|
Challenge-Vergleich konstantzeitig. Die Datenformate sind
|
||
|
|
WebAuthn-kompatibel (b64url-encoded `client_data_json` mit `challenge`,
|
||
|
|
`type`, `origin`), damit das Phase-2-Umstieg auf
|
||
|
|
`github.com/go-webauthn/webauthn` ohne API-Bruch möglich bleibt.
|
||
|
|
- Keine HTML/UI; nur JSON. Browser-Integration landet in einem Folge-Task.
|
||
|
|
|
||
|
|
## Konfiguration
|
||
|
|
|
||
|
|
Alle über Flags oder Env (`APPROVAL_*`):
|
||
|
|
|
||
|
|
| Env / Flag | Default | Zweck |
|
||
|
|
| ------------------------------- | --------------------------- | --------------------------------------------------------- |
|
||
|
|
| `APPROVAL_LISTEN` | `:8080` | HTTP-Listen-Adresse. |
|
||
|
|
| `APPROVAL_SIGNING_URL` | *(required)* | Base URL des signing-service (`/api/v1/sign/dsse`). |
|
||
|
|
| `APPROVAL_SIGNING_KEY_REF` | `trust-api/approval-signer-2026-q4` | DSSE-Signaturschlüssel. |
|
||
|
|
| `APPROVAL_SIGNING_TOKEN` | *(optional)* | Bearer-Token für signing-service (Phase 1 Static). |
|
||
|
|
| `APPROVAL_TLOG_URL` | *(required)* | Base URL des transparency-log. |
|
||
|
|
| `APPROVAL_SESSION_TTL` | `15m` | Lebensdauer einer Approval-Session. |
|
||
|
|
| `APPROVAL_CHALLENGE_TTL` | `2m` | Max. Zeit zwischen Preview und Confirm. |
|
||
|
|
| `APPROVAL_WEBAUTHN_RP_ID` | `trust.basicstack.de` | WebAuthn relying party ID. |
|
||
|
|
| `APPROVAL_WEBAUTHN_RP_ORIGIN` | `https://trust.basicstack.de` | Erlaubter Origin in `client_data_json`. |
|
||
|
|
| `APPROVAL_POCKET_ID_ISSUER` | *(required in prod)* | Pocket-ID OIDC-Issuer. |
|
||
|
|
| `APPROVAL_POCKET_ID_CLIENT` | *(required in prod)* | Client-ID des approval-service. |
|
||
|
|
|
||
|
|
## Tests
|
||
|
|
|
||
|
|
```
|
||
|
|
cd services/approval-service
|
||
|
|
go test ./...
|
||
|
|
```
|
||
|
|
|
||
|
|
Deckt ab:
|
||
|
|
|
||
|
|
- Happy Path (Session → Preview → Confirm → DSSE verifiziert und ins TLog
|
||
|
|
eingetragen).
|
||
|
|
- Tampered `H_content` in der Assertion → 400.
|
||
|
|
- Abgelaufene Challenge → 403.
|
||
|
|
- Signing-Service antwortet nicht (connection refused) → 502.
|
||
|
|
- TLog-Submit scheitert → Approval bleibt `attestation-pending`.
|