flex-auth/examples/secrets-engine/replay
tegwick 127f83da4d
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s
Build and Publish Container Image / build-and-push (push) Successful in 53s
Sign decision envelopes and close FLEX-WP-0024.
Detached Ed25519 over the canonical envelope with signature omitted.
Unsigned is stated, not implied. Testdata fixtures prove verify and
tamper failure without minting a production key. FLEX-WP-0025 is
finished with the validate check from the previous commit.

Assistant: grok
Assistant-Session: 01a09dc1-b21e-77e1-919e-fcad2f82b267
2026-09-14 09:57:50 +02:00
..
decision_destroy_dual_control.json fix(decision): registry facts win over caller-supplied attributes 2026-09-07 13:43:33 +02:00
decision_rotate.json fix(decision): registry facts win over caller-supplied attributes 2026-09-07 13:43:33 +02:00
decision_rotate_signed.json Sign decision envelopes and close FLEX-WP-0024. 2026-09-14 09:57:50 +02:00
decision_rotate_signed_tampered.json Sign decision envelopes and close FLEX-WP-0024. 2026-09-14 09:57:50 +02:00
decision_wrong_tenant_deny.json fix(decision): registry facts win over caller-supplied attributes 2026-09-07 13:43:33 +02:00
keys.json Sign decision envelopes and close FLEX-WP-0024. 2026-09-14 09:57:50 +02:00
README.md Sign decision envelopes and close FLEX-WP-0024. 2026-09-14 09:57:50 +02:00

T03 replay fixtures

Real DecisionEnvelopes emitted from the published package, for secrets-engine to verify its digest join (627810b) unchanged. FLEX-WP-0021-T03.

File From
decision_rotate.json ../check_request_allow_rotate.json — plain allow, empty context
decision_destroy_dual_control.json ../check_request_allow_destroy_dual_control.json — dual control, valid approval-claim
decision_wrong_tenant_deny.json ../check_request_deny_wrong_tenant.json — foreign tenant, denied wrong_tenant
decision_rotate_signed.json same rotate envelope, Ed25519-signed with the testdata key (FLEX-WP-0024-T03)
decision_rotate_signed_tampered.json the signed rotate envelope with effect/reason altered after signing
keys.json public half of the testdata key (kid=testdata-ed25519). Not a production key

Regenerate either with:

go run ./cmd/flex-auth check \
  -policy   examples/secrets-engine/policy_package.md \
  -registry examples/secrets-engine/registry_snapshot.json \
  -request  examples/secrets-engine/check_request_allow_rotate.json

Envelope signature (FLEX-WP-0024)

A consumer verifier is untested until it has seen both a valid signature and an invalid one. These two files are that pair:

# genuine — must verify
# tampered — must fail
# public key: keys.json kid testdata-ed25519

The signed material is json.Marshal of the envelope with signature omitted (docs/decision-envelope-signature.md). request_digest is the same on the unsigned rotate fixture, the signed fixture, and the tampered fixture, because the signature is not binding material.

The testdata private seed is 0x42 repeated 32 times. It is not the OpenBao custody path.

What is stable and what is not

Stable across runs — these are the fields to pin a contract test against:

Field rotate destroy
binding.request_digest sha256:de67324f…4345 sha256:c749ee2…091a
provenance.policy_package_digest sha256:bd11c5fe…c643 same
provenance.registry_snapshot_digest sha256:f5a309bc…40bb same
provenance.input_claim_digests.context absent (empty context) sha256:8b73d29…2800

Verified identical across two runs.

Regenerated 2026-09-07 — submitted_request_digest added; every existing digest unchanged. From FLEX-DEC-2026-012, which secrets-engine found by running against the live pin.

request_digest is computed over the enriched request — after the evaluator overlays registry facts onto subject and resource — so it is not consumer-computable and never was. Pin submitted_request_digest instead: it is the digest over the request exactly as sent, and it is the §6.4.2 replay test. request_digest keeps both its meaning as flex-auth's own audit-replay identity and its exact value here — de67324f…, c749ee2d…, c9c6e6f8… are unchanged, as is approval_binding_digest at fa07bec…. The enrichment precedence changed in the same decision but these three requests contradict no registry fact, so nothing they hash moved. Nothing you have pinned needs re-pinning; there is a new field to add, not an old one to correct.

Field rotate destroy wrong-tenant
binding.submitted_request_digest sha256:41c8fc08… sha256:c605a9ec… sha256:9aab6de9…

A consumer that pinned request_digest from these files was pinning a value it could not have computed itself — it matched because it was copied from our output, not derived. That is the defect submitted_request_digest closes: the same assertion now has a value you can recompute from the request you sent.

Regenerated at v2, 2026-09-06. The package gained the tenant rule it had been missing, so provenance.policy_version is now v2 and policy_package_digest moved from sha256:fe0070b7…bd8c to sha256:bd11c5fe…c643. Both request_digest values are unchanged — the canonical digest covers tenant/subject/action/resource/context and not the package, so a consumer that pinned the digest join has nothing to re-pin. Only the two provenance fields moved, and they moved because the policy did.

decision_wrong_tenant_deny.json is the denial evidence for the tenant question: the same rotate request as decision_rotate.json with tenant: tenant:coulomb substituted. Its request_digest differs from the allow's, which is the replay identity working — the tenant is hashed material, so the two requests are not the same request. A deny carries no lifetime.

| binding.approval_binding_digest | absent (no claim) | sha256:fa07bec…cd56 |

That last row is the value an approval's pdp_digest must equal — never request_digest, which a claim can never match on the request that carries it. It equals the request_digest of the same request with context.approval removed, which is verifiable from this directory: strip the context from ../check_request_allow_destroy_dual_control.json, re-run check, and the claim-free request digest is the same value (it denies dual_control_required, which is the point).

This fixture verifies itself. The claim inside the request carries binding.pdp_digest equal to the envelope's approval_binding_digest, and binding.pdp_path: true. Compare the two values in the two files. Note that request_digest moved when the claim's contents changed while approval_binding_digest did not — that is the stability property, demonstrated in the artifact rather than asserted in prose.

Not stable: id, provenance.decision_time, and lifetime.not_before / lifetime.expires_at move with the clock. lifetime.ttl is 15m from the package's allow_ttl. Do not pin the record as a whole.

input_claim_digests.context appears only when the request carries a non-empty context — which is why both fixtures are here rather than just one. A consumer asserting the field is always present would pass on destroy and fail on rotate.

The destroy request's context.approval is a complete approval-claim, valid against approval-engine/schemas/approval_claim.schema.json including the now-required binding.pdp_digest. It was regenerated on 2026-09-06 when that claim was completed, so its digests differ from the first emission — a partial claim in a fixture is how a consumer learns the wrong shape.

Not a deployment

These come from flex-auth/local in standalone mode (provenance.evaluator / mode), not from a cluster pin. No flex-auth-secrets-engine pin exists yet (FLEX-WP-0021-T04), and the consumer policy pin stays unset until T05.