|
Some checks failed
build-images / signing-service image (push) Failing after 16s
validate / JSON Schema (ajv) (push) Successful in 19s
validate / Go (vet + test) (push) Failing after 4s
validate / Rego (opa fmt + opa test) (push) Successful in 13s
validate / DB (migrate up/down dry-run) (push) Failing after 21s
Minimal CLI agent process that end-to-end demonstrates the Transform step
of the ProofedMesh trust chain:
1. Fetches an approval attestation from the transparency-log by
attestation_ref (`sha256:<hex>`).
2. DSSE-verifies the fetched envelope against a pinned approval-signer
ed25519 keyring; stops before any signing call on failure.
3. Builds an in-toto v1 Statement with predicateType
`basicstack.de/attestations/transform/v1`, operation
`expense.map_to_sink`, materials[0].attestation_ref = approval_ref,
and subject[0].digest.sha256 = sha256(canonical(handover_payload)).
4. Calls signing-service /api/v1/sign/dsse with BOTH identities:
Authorization: Bearer <pocket-id client_credentials token> AND
X-Workload-Token: <projected SA token from
/var/run/secrets/tokens/trust-api>.
5. Submits the returned DSSE envelope + content_hash to the
transparency-log /api/v1/log/entries.
6. POSTs the canonical handover_payload bytes to the dummy-sink
(schema sink.dummy.v1), carrying the transform content_hash on the
X-Attestation-Ref header.
Tests use httptest for all four backends:
- Happy path (verifies the bytes sent to the sink match the signed
subject digest and that the TLog recorded the same content_hash the
agent returned).
- Invalid approval signature -> stops before sign-call; TLog and sink
untouched.
- Signing-service returns 403 signing_denied -> surfaced as
signer.ErrSigningDenied; TLog and sink untouched.
- Missing Pocket-ID token -> fails before any network call.
Includes PocketID client_credentials TokenSource (OIDC discovery + cache)
and ProjectedSATokenSource (reads kubelet-managed Volume) mirroring
admin-api/approval-service.
Non-scope: no read-side attestations (Phase 2), no K8s/CI image build
wiring (follow-up ticket).
Co-Authored-By: Paperclip <noreply@paperclip.ing>
|
||
|---|---|---|
| .. | ||
| cmd/testagent-transform | ||
| internal | ||
| Dockerfile | ||
| go.mod | ||
| README.md | ||
testagent-transform
Phase-1 end-to-end demonstrator of the ProofedMesh trust chain's Transform
step (DEV-610, parent
DEV-606). It is a one-shot CLI, not an HTTP service:
given an approval attestation and a handover_payload, it produces a signed
transform attestation, logs it to the transparency-log, and hands the payload
to the Phase-1 dummy-sink. The exit code is 0 on success.
Flow
- Fetch approval.
GET {TRANSFORM_TLOG_URL}/api/v1/log/entries/{approval_ref}. The TLog returns the stored DSSE envelope pluslog_index. - Verify. The returned envelope is DSSE-verified against the pinned
approval-signer keyring (
APPROVAL_SIGNER_KEYS). On failure the agent stops before any signing call — nothing new is attested on top of an untrusted approval. - Build transform statement. in-toto v1 Statement with
predicateType = https://basicstack.de/attestations/transform/v1,operation = expense.map_to_sink,materials[0].attestation_ref = approval_ref, andsubject[0].digest.sha256 = sha256(canonical(handover_payload)).canonicalhere is byte-identical to the approval-service's canonical serialisation so a downstream verifier reproduces the same hash. - Sign.
POST {TRANSFORM_SIGNING_URL}/api/v1/sign/dssewith BOTH identities set as headers:Authorization: Bearer <pocket-id-access-token>(client_credentialsgrant againstPOCKET_ID_ISSUER)X-Workload-Token: <projected-sa-token>(read fromTRANSFORM_WORKLOAD_TOKEN_PATH, default/var/run/secrets/tokens/trust-api) The signing-service's structured policy evaluates BOTH identities against the agent registry and OPA; a Pocket-ID bearer alone is not enough. Seeservices/signing-service/internal/httpapi/sign.go.
- Log.
POST {TRANSFORM_TLOG_URL}/api/v1/log/entrieswith the envelope +content_hashfrom step 4. - Handover.
POST {TRANSFORM_SINK_URL}/api/v1/handoverwith the canonicalhandover_payloadbytes. Headers set:X-Payload-Schema: sink.dummy.v1X-Attestation-Ref: <transform content_hash>
Non-scope
- No read-side attestations (Phase 2).
- No persistent store; the agent is stateless and idempotent given the same
(approval_ref, handover_payload)input (the TLog deduplicates oncontent_hash).
Configuration
| Env var | Default | Purpose |
|---|---|---|
TRANSFORM_TLOG_URL |
(required) | Base URL of transparency-log. |
TRANSFORM_SIGNING_URL |
(required) | Base URL of signing-service. |
TRANSFORM_SINK_URL |
(required) | Base URL of dummy-sink. |
TRANSFORM_WORKLOAD_TOKEN_PATH |
/var/run/secrets/tokens/trust-api |
Projected ServiceAccount token path. |
POCKET_ID_ISSUER |
(required) | Pocket-ID OIDC issuer URL. |
POCKET_ID_CLIENT_ID |
(required) | OIDC client id for this agent. |
POCKET_ID_CLIENT_SECRET |
(required) | OIDC client secret for this agent. |
POCKET_ID_AUDIENCE |
trust-api |
OIDC token audience. |
APPROVAL_SIGNER_KEYS |
(required) | Comma-separated kid=base64(public-ed25519) entries. |
TRANSFORM_ACTOR |
agent-testagent-transform |
Registry id written into the attestation. |
TRANSFORM_OPERATION |
expense.map_to_sink |
Transform operation name (must match OPA policy). |
TRANSFORM_SINK_TARGET |
sink:dummy-sink |
Logical sink identifier. |
TRANSFORM_SIGNING_KEY_REF |
trust-api/agent-testagent-transform-2026-q4 |
OpenBao Transit key ref for the transform signer. |
TRANSFORM_INPUT_SCHEMA / _VERSION |
expense.canonical / 1.2.0 |
Input schema name/version carried in the attestation. |
TRANSFORM_OUTPUT_SCHEMA / _VERSION |
sink.db.expense / 1.2.0 |
Output schema name/version carried in the attestation. |
TRANSFORM_VERSION |
0.1.0 |
Agent's own version string, carried in the attestation. |
TRANSFORM_SINK_PAYLOAD_SCHEMA |
sink.dummy.v1 |
Value sent on the sink's X-Payload-Schema header. |
CLI
testagent-transform \
-workflow WF-0042 \
-approval-ref sha256:... \
-handover ./handover.json
handover.json must be a JSON object; the agent canonicalises it before
hashing + sending to the sink.
Tests
cd services/testagent-transform
go vet ./...
go test ./...
The end-to-end transform tests spin up four httptest servers — approval
read, signing-service, transparency-log, dummy-sink — and run the full
pipeline against them:
- Happy path. All four backends respond; the subject digest the agent
signs equals the sha256 of the canonical bytes POSTed to the sink; the
TLog's recorded content_hash equals the agent's returned
TransformRef. - Invalid approval signature. The approval mock returns a DSSE envelope
whose Ed25519 signature does not verify against the pinned public key.
The agent fails with
ErrInvalidApprovaland never calls the signing-service, TLog, or sink. - Signing denied. The signing-service answers
403 signing_denied. The agent surfacessigner.ErrSigningDeniedand never calls the TLog or sink. - Missing Pocket-ID token. The TokenSource returns
"". The signer client rejects before any network traffic.