# signing-service HTTP service that implements `POST /api/v1/sign/dsse` per [ADR-0005](../../docs/adr/0005-signing-service-and-transparency-log-api.md). It canonicalises an in-toto v1 Statement, DSSE-signs it with the key named by `key_ref`, and returns the envelope plus a stable `content_hash` that the admin-api and other clients use as the single attestation reference. ## Status - **Signing is real**: Ed25519 PAE + envelope + content_hash. Verified end-to-end against the admin-api `attest.HTTPSigner` client via a Go interop test that re-verifies the signature with the server's public key. - **In-memory keyring**: `-backend=memory`. Ephemeral keys are generated at startup unless `-keys-from-env` is set; stable dev keys can be supplied as hex-encoded 64-byte Ed25519 private keys via `KEY_` env vars. - **OpenBao Transit backend**: stub (`-backend=openbao` returns `ErrNotWired`). Separate subtask. - **Pocket ID JWKS verifier**: stub. Dev uses the static `-bearers` table. Separate subtask. - **OPA policy decider**: stub. Dev uses AllowAll. Separate subtask. - **Deployment manifest**: separate subtask. ## Layout ``` services/signing-service/ ├── README.md ├── go.mod ├── cmd/signing-service/main.go — HTTP entry (bind + router + graceful shutdown) └── internal/ ├── canonical/canonical.go — stable JSON serialisation (byte-for-byte │ compatible with admin-api/internal/attest/hash.go) ├── signer/signer.go — Signer interface, InMemory (Ed25519), OpenBao stub, │ DSSE PAE and SignStatement orchestration ├── signer/signer_test.go — signature verification, PAE format exactness, errors ├── policy/policy.go — Decider interface, AllowAll, OPA stub ├── token/validator.go — bearer validator, Static + PocketID stub └── httpapi/ ├── sign.go — POST /api/v1/sign/dsse handler + /healthz └── sign_test.go — happy path, auth, policy, unknown key, bad statement ``` ## Local dev ```bash cd services/signing-service go vet ./... && go test ./... go run ./cmd/signing-service \ -listen=:8081 \ -backend=memory \ -keys=trust-api/rate-change-signer-2026-q4 \ -bearers=dev-token=agent-admin-api ``` Smoke-test it with curl: ```bash curl -sS -X POST http://localhost:8081/api/v1/sign/dsse \ -H "Authorization: Bearer dev-token" \ -H "Content-Type: application/json" \ -d '{ "key_ref":"trust-api/rate-change-signer-2026-q4", "statement":{ "_type":"https://in-toto.io/Statement/v1", "subject":[{"name":"per_diem_rate/PDR-1","digest":{"sha256":"aaaa"}}], "predicateType":"https://basicstack.de/attestations/rate_change/v1", "predicate":{"change_kind":"create","rate_id":"PDR-1"} } }' | jq . ``` Pointing the admin-api at this instance: ```bash # Terminal 1 cd services/signing-service && go run ./cmd/signing-service -listen=:8081 # Terminal 2 (admin-api still needs a tlog, so this will log "attestation failed" # until services/transparency-log is up — rows will still be created and marked # attestation-pending as designed): cd services/admin-api && go run ./cmd/admin-api -store=memory \ -signer-url=http://localhost:8081 \ -tlog-url=http://localhost:9099 \ -bearer=dev-token \ -signer-key=trust-api/rate-change-signer-2026-q4 ``` ## Keys ### Generate one hex-encoded Ed25519 private key for dev ```bash go run ./cmd/signing-service -gen-dev-key # → prints 128 hex chars (64 bytes raw private key) ``` Supply it via env: ```bash # For key_ref "trust-api/rate-change-signer-2026-q4" the env name is # KEY_TRUST_API_RATE_CHANGE_SIGNER_2026_Q4 (upper-case, / - . → _) export KEY_TRUST_API_RATE_CHANGE_SIGNER_2026_Q4= go run ./cmd/signing-service -backend=memory -keys-from-env \ -keys=trust-api/rate-change-signer-2026-q4 ``` ### Prod Prod wiring in the OpenBao-Transit subtask: Signer backend becomes `signer.OpenBao` with HTTP client to the cluster-internal OpenBao, policy backend becomes `policy.OPA` with the Rego bundle under `policies/signing/` (not yet created), token validator becomes `token.PocketID` with JWKS from `https://auth.basicstack.de/.well-known/jwks.json`.