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>
Two changes needed to drive the Phase-1 end-to-end smoke test without a
browser and Pocket-ID:
- Add `-dev-auth-sub` / `APPROVAL_DEV_AUTH_SUB` so a dev / CI run can
stamp every request with a fixed principal sub instead of refusing
all callers until Pocket-ID is wired. Mutually exclusive with any
real auth middleware; the deployment path still fails closed.
- Add `-dev-routes` / `APPROVAL_DEV_ROUTES` which mounts
`POST /dev/webauthn/credentials` on the Phase-1 in-memory WebAuthn
store. The smoke test generates an Ed25519 keypair and posts the
public half here before confirming the approval. Both flags log a
WARN at startup so a misconfigured prod stands out.
Separately, promote four chain-control fields —
`allowed_transformations`, `allowed_sinks`, `expires_at`,
`workflow_id` — from the signed rendered content into the approval
predicate. The verification-service already requires these on the
approval predicate to enforce the sink / transform allowlists and the
approval TTL; they live inside H_content so promoting them does not
weaken the WebAuthn-bound signature.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
New services/approval-service with the Session → Preview → Confirm
pipeline required by DEV-606/DEV-608:
* POST /api/v1/sessions starts a Pocket-ID-bound approval session.
* POST /api/v1/approvals/preview renders content server-side, computes
H_content = sha256(canonical JSON), stores the (nonce, salt, canonical,
challenge) tuple and returns the human verification digits plus the
WebAuthn challenge = nonce || H_content.
* POST /api/v1/approvals/confirm verifies the WebAuthn assertion against
the stored challenge, DSSE-signs the Approval statement via the
signing-service (key_ref=trust-api/approval-signer-2026-q4), submits
to the transparency-log, and returns {approval_id, attestation_ref,
log_index}. TLog failures leave the row at attestation-pending for
the reconciler.
* db/trust/migrations/0004_approval_sessions.{up,down}.sql: new tables
approval_sessions, approval_previews, approvals (schema matches the
Phase 1 store contract).
* internal/content: canonical JSON + H_content + 6-digit verification
code (byte-compatible with signing-service/internal/canonical).
* internal/webauthn: stdlib-only Phase-1 verifier; wire shape stays
WebAuthn-compatible for the Phase-2 swap to go-webauthn/webauthn.
* internal/signer, internal/tlog: HTTP clients mapping 5xx / connection
errors to typed ErrSignerUnavailable / ErrTLogUnavailable so the
HTTP layer can return 502 for signing failures and persist
attestation-pending on TLog failure.
* internal/service.Orchestrator ties the pipeline together and persists
the Approval BEFORE the TLog submit (never return a log_index for
an un-persisted approval).
Tests cover: happy path, tampered H_content → 400, expired challenge
→ 403, signer 5xx + conn-refused → 502, TLog failure → row stays
attestation-pending, duplicate confirm → 409, session expired → 403.
go vet clean, go test ./... passes.
Co-Authored-By: Paperclip <noreply@paperclip.ing>