|
All checks were successful
build-images / signing-service image (push) Successful in 26s
build-images / verification-service image (push) Successful in 27s
build-images / approval-service image (push) Successful in 31s
build-images / dummy-sink image (push) Successful in 29s
validate / JSON Schema (ajv) (push) Successful in 23s
validate / Rego (opa fmt + opa test) (push) Successful in 16s
phase1-smoke / Phase 1 end-to-end smoke (push) Successful in 1m0s
validate / DB (migrate up/down dry-run) (push) Successful in 26s
validate / Go (vet + test) (push) Successful in 1m20s
Phase-1 Subtask 8/10 (parent DEV-606):
- deploy/approval-service/: SA (no auto-mount), Deployment (replicas=2,
distroless-nonroot, read-only rootfs), ClusterIP :8080, NetworkPolicy
(ingress from pangolin namespace only; egress to signing-service,
transparency-log, trust-pg in agent-trust + pocket-id + DNS), README.
- 10-secret-dev.example.yaml template + the real 10-secret-dev.sealed.yaml
carrying the Pocket-ID client id/secret, WebAuthn RP config, and the
dev signing-service bearer; unsealed in-cluster to Secret
approval-service-dev.
- services/approval-service/Dockerfile: BuildKit multi-stage, context-at-
repo-root, matches the signing/verification/dummy-sink pattern.
- .forgejo/workflows/build-images.yml: new approval-service job pushing
harbor.basicstack.de/basicstack/approval-service:{sha,main} on main.
- Pocket-ID: created proofedmesh-admin group + OIDC client
"ProofedMesh Approval" with PKCE enforced, callback
https://approval.proofedmesh.basicstack.de/oidc/callback, group
restriction to proofedmesh-admin. Client secret stored as Paperclip
secret proposal and in the SealedSecret above — never pasted inline.
Deployment applied cluster-side pending the first CI image build:
`kubectl apply -f deploy/approval-service/` is green; pods ErrImagePull
until the Forgejo workflow completes and pushes the :main tag.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
|
||
|---|---|---|
| .. | ||
| cmd/approval-service | ||
| internal | ||
| pkg/httpapi | ||
| Dockerfile | ||
| go.mod | ||
| README.md | ||
approval-service
Phase 1 der ProofedMesh content-gebundenen Approvals (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:
{
"session_id": "sess_…",
"content": { "...application-defined..." }
}
Response:
{
"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:
{
"preview_id": "pv_…",
"assertion": {
"credential_id": "<b64url>",
"client_data_json": "<b64url>",
"authenticator_data": "<b64url>",
"signature": "<b64url>"
}
}
Response:
{
"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 ininternal/store/postgres.goals Skelett angelegt (Pattern ausadmin-api/internal/store/postgres.go). Die SQL-Migration liegt bereits indb/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 überauthenticator_data || sha256(client_data_json), Challenge-Vergleich konstantzeitig. Die Datenformate sind WebAuthn-kompatibel (b64url-encodedclient_data_jsonmitchallenge,type,origin), damit das Phase-2-Umstieg aufgithub.com/go-webauthn/webauthnohne 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_contentin der Assertion → 400. - Abgelaufene Challenge → 403.
- Signing-Service antwortet nicht (connection refused) → 502.
- TLog-Submit scheitert → Approval bleibt
attestation-pending.