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 |
||
|---|---|---|
| .. | ||
| decision_destroy_dual_control.json | ||
| decision_rotate.json | ||
| decision_rotate_signed.json | ||
| decision_rotate_signed_tampered.json | ||
| decision_wrong_tenant_deny.json | ||
| keys.json | ||
| README.md | ||
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.