- Dockerfile: multi-stage (golang:1.23-alpine3.21 → distroless/static:nonroot), CGO off, trimpath, stripped, UID/GID 65532, read-only rootfs friendly. - deploy/signing-service/: ServiceAccount, Deployment (2 replicas, probes, tight SecurityContext), ClusterIP Service on :8081, NetworkPolicy (ingress from agent-trust, DNS egress), example Secret, and README with the full dev + prod secret convention. - .forgejo/workflows/build-images.yml: builds image on push + PR, pushes :<git-sha> + :main to harbor.basicstack.de on main pushes only. Clone pattern matches validate.yml (sha256 repo, manual git clone). - README updates: service README gains Deployment section; repo README layout now mentions services/signing-service, services/transparency-log, deploy/, and the new workflow. Scope matches DEV-605. OpenBao, OPA and Pocket-ID JWKS stay stubs and move in their own follow-up subtasks; the prod secret shape is documented so those subtasks can plug in without reshaping the Deployment. Co-Authored-By: Paperclip <noreply@paperclip.ing>
169 lines
6.2 KiB
Markdown
169 lines
6.2 KiB
Markdown
# 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_<slug>` 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**: Kubernetes manifests under
|
|
[`deploy/signing-service/`](../../deploy/signing-service/) + Dockerfile
|
|
next to this README. See **Deployment** below.
|
|
|
|
## 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=<hex>
|
|
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`.
|
|
|
|
## Deployment
|
|
|
|
### Container image
|
|
|
|
Multi-stage build in `services/signing-service/Dockerfile`:
|
|
|
|
- build stage: `golang:1.23-alpine3.21`, runs `go vet` + `go test` before `go build`
|
|
- runtime stage: `gcr.io/distroless/static-debian12:nonroot` (UID/GID 65532, no shell)
|
|
- the final binary is static (`CGO_ENABLED=0`), stripped (`-ldflags="-s -w"`),
|
|
reproducible (`-trimpath`)
|
|
|
|
Build locally from the repo root:
|
|
|
|
```bash
|
|
docker build -f services/signing-service/Dockerfile \
|
|
-t harbor.basicstack.de/basicstack/signing-service:dev .
|
|
```
|
|
|
|
CI (`.forgejo/workflows/build-images.yml`) builds on every push + PR to
|
|
`main` and pushes `:<git-sha>` + `:main` to Harbor on main pushes only.
|
|
|
|
### Kubernetes
|
|
|
|
Manifests live under
|
|
[`deploy/signing-service/`](../../deploy/signing-service/) and target the
|
|
`agent-trust` namespace. See
|
|
[`deploy/signing-service/README.md`](../../deploy/signing-service/README.md)
|
|
for the full secret convention (dev + prod layout), the SealedSecret
|
|
recipe, and the apply order.
|
|
|
|
Minimal example (dev mode, in-memory key, static bearer):
|
|
|
|
```bash
|
|
BEARER_TOKEN="$(openssl rand -hex 32)"
|
|
HEX_KEY="$(go run ./cmd/signing-service -gen-dev-key)"
|
|
|
|
kubectl apply -f ../../deploy/trillian/00-namespace.yaml
|
|
kubectl apply -f ../../deploy/signing-service/00-serviceaccount.yaml
|
|
|
|
kubectl create secret generic signing-service-dev -n agent-trust \
|
|
--from-literal="SIGNING_BEARERS=${BEARER_TOKEN}=agent-admin-api" \
|
|
--from-literal="KEY_TRUST_API_RATE_CHANGE_SIGNER_2026_Q4=${HEX_KEY}"
|
|
|
|
kubectl apply -f ../../deploy/signing-service/20-deployment.yaml
|
|
kubectl apply -f ../../deploy/signing-service/30-service.yaml
|
|
kubectl apply -f ../../deploy/signing-service/40-networkpolicy.yaml
|
|
|
|
kubectl -n agent-trust rollout status deploy/signing-service
|
|
```
|
|
|
|
In-cluster the service is reachable at
|
|
`http://signing-service.agent-trust.svc.cluster.local:8081` (health check
|
|
`/healthz`).
|