Skip to content

feat(test-vectors): APS DecisionLineageReceipt for ku_4b3f7c2a9d8e1f05#7

Open
aeoess wants to merge 2 commits intoVeritasActa:feat/ku-test-vectorsfrom
aeoess:contrib/aps-receipt-ku-4b3f7c
Open

feat(test-vectors): APS DecisionLineageReceipt for ku_4b3f7c2a9d8e1f05#7
aeoess wants to merge 2 commits intoVeritasActa:feat/ku-test-vectorsfrom
aeoess:contrib/aps-receipt-ku-4b3f7c

Conversation

@aeoess
Copy link
Copy Markdown

@aeoess aeoess commented Apr 18, 2026

Summary

Adds APS DecisionLineageReceipt cross-verification fixtures for ku_id=ku_4b3f7c2a9d8e1f05.

This PR now includes the contrast pair discussed in review:

  • test-vectors/cross-verify-bundle.json: sidecar-anchored APS fixture, MUST accept.
  • test-vectors/keys/aps-ku-cross-verify.jwks: independent sidecar JWKS referenced by external_receipts.aps.verification_key_ref.
  • test-vectors/cross-verify-embedded-key-bundle.json: deliberately embedded-key fixture, MUST reject.

The positive fixture commits to the bundle by recording, for each entry in bundle.receipts, a contributingSources[i].accessReceiptId of the form sha256:<jcs-canonical-sha256>. The hashes match the bundle receipts exactly.

Why

This resolves the embedded-key spec gap raised by @desiorac on GetBindu#459 and gives draft-02's verification-key anchoring rule a testable surface:

  • Verifiers that resolve the sidecar JWKS can pass the positive case.
  • Verifiers that silently accept a key transported inside the receipt or bundle fail the negative case.

Expected negative-case error:

verification key transported inside receipt without independent anchor

Verification

Base bundle verification:

npx @veritasacta/verify test-vectors/cross-verify-bundle.json --bundle

Positive APS sidecar-key fixture:

npx @veritasacta/verify test-vectors/cross-verify-bundle.json \
  --bundle --jwks test-vectors/keys/aps-ku-cross-verify.jwks

Negative embedded-key fixture:

npx @veritasacta/verify test-vectors/cross-verify-embedded-key-bundle.json --bundle
# MUST fail non-zero:
# verification key transported inside receipt without independent anchor

Local validation run in this branch:

node -e "for (const f of ['test-vectors/cross-verify-bundle.json','test-vectors/cross-verify-embedded-key-bundle.json','test-vectors/keys/aps-ku-cross-verify.jwks']) JSON.parse(require('fs').readFileSync(f,'utf8'))"
npm test

Scope

  • Populates the APS slot in test-vectors/cross-verify-bundle.json.
  • Adds the sidecar JWKS and negative embedded-key conformance fixture.
  • Updates test-vectors/generate.mjs so rerunning the generator preserves the fixture set.

…eipts.aps.receipt slot for ku_4b3f7c2a9d8e1f05
Copy link
Copy Markdown
Contributor

@tomjwxf tomjwxf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for populating the APS slot. Cross-verifying APS DecisionLineageReceipts against @veritasacta/verify is a meaningful first in the implementation matrix, and the cross-layer integrity demo (contributingSources[i].accessReceiptId binding APS receipts to the Ed25519 chain via hash) is the concrete shape the IETF draft has been asserting without fixtures to point at. Good work.

Want to hold on merging until the signing_key placement is adjusted though. Current structure inlines the public key at external_receipts.aps.signing_key, which is the exact pattern surfaced as a spec gap by @desiorac on GetBindu #459 recently. draft-02 (going out later this week) adds a normative "MUST NOT accept a verification key transported inside the receipt unless that key is independently anchored" requirement in Security Considerations, @veritasacta/verify 0.4.0 will enforce it, and a matching negative conformance vector lands in ScopeBlind/agent-governance-testvectors. The canonical verify repo shipping a fixture that relies on the embedded-key convenience would teach the wrong lesson right as the spec tightens.

Two restructure options that keep the educational value:

Option A (probably the one you want): Move the key to a sidecar JWKS at test-vectors/keys/aps-ku-cross-verify.jwks and reference via a verification_key_ref field in the bundle. Verifier invocation becomes:

npx @veritasacta/verify test-vectors/cross-verify-bundle.json \
    --bundle --jwks test-vectors/keys/aps-ku-cross-verify.jwks

Single extra JSON file, establishes the pattern future fixtures copy.

Option B: Leave a SHA-256 fingerprint of the key in the receipt ("this receipt claims to be signed by the key with this fingerprint") but require the verifier to resolve the full key from a known location. Matches how SSH known_hosts works. More involved restructure.

Option A is the minimal change. Happy to push a suggested-change commit to this branch with the sidecar JWKS + verifier-invocation update if easier than doing it yourself. Let me know which option you prefer and I'll either draft it or wait for you.

Everything else in the PR is clean and ready to land the moment the key placement is resolved.

@aeoess
Copy link
Copy Markdown
Author

aeoess commented Apr 30, 2026

@tomjwxf. Option A. Sidecar JWKS at test-vectors/keys/aps-ku-cross-verify.jwks + verification_key_ref in the bundle is the right shape, especially with draft-02's "MUST NOT accept a verification key transported inside the receipt unless that key is independently anchored" requirement landing this week. A canonical verify repo fixture teaching the wrong lesson at exactly that timing is the failure mode worth avoiding.

Take the suggested-change commit offer. Your hands are already in the rotation for the 0.4.0 enforcement work, so the reshape lands consistent with the verifier behavior it's modeling. Once it's pushed, I'll re-test against the local APS signing flow to confirm the cross-layer integrity demo (contributingSources[i].accessReceiptId binding APS receipts to the Ed25519 chain via hash) still threads cleanly through the new key-resolution path.

Worth pinning the negative conformance vector reference in the PR description once it lands at ScopeBlind/agent-governance-testvectors, gives readers the "here's what the spec now refuses" companion to the "here's what it accepts" example this PR is establishing.

@desiorac
Copy link
Copy Markdown

Option A is the right call. The sidecar JWKS pattern also enables a stronger conformance test suite: include a deliberately embedded-key fixture (MUST reject) alongside the sidecar-anchored fixture (MUST accept). Without that contrast pair, the 'MUST NOT accept embedded keys' requirement is hard to verify in isolation — verifiers that happen to pass the positive case may still silently accept the negative case.

Worth noting in the PR description once the sidecar reshape lands.

@aeoess
Copy link
Copy Markdown
Author

aeoess commented Apr 30, 2026

@desiorac, contrast-pair fixture is the structural piece that makes the requirement actually testable. Without it, a verifier passing the positive case can silently accept the negative case and nothing in the conformance suite catches it. Worth landing in the same PR as the sidecar reshape rather than as a follow-up, the negative-conformance vector isn't optional once draft-02 ships.

@tomjwxf, flagging for the suggested-change commit you offered. Two artifacts in this PR rather than one:

  • test-vectors/cross-verify-bundle.json, sidecar-anchored fixture, MUST accept
  • test-vectors/cross-verify-embedded-key-bundle.json, deliberately embedded-key fixture, MUST reject

Verifier invocation against the negative case should exit non-zero with an error message naming the exact draft-02 clause being enforced ("verification key transported inside receipt without independent anchor"). Gives a verifier implementer the test surface to know they've actually built the rejection logic correctly, not just the acceptance logic.

This also gives @veritasacta/verify 0.4.0 a concrete regression target, the moment the embedded-key path is silently re-enabled by a refactor, this fixture catches it.

Worth a one-line note in the PR description once it lands pointing at @desiorac's GetBindu#459 as the spec-gap origin, since the contrast-pair pattern generalizes beyond this one fixture and other receipt-format specs (in-toto, SLSA provenance, sigstore bundles) face the same testability problem.

@desiorac
Copy link
Copy Markdown

The two-fixture pattern matches how the Trust Layer conformance suite handles this: a MUST-accept and a MUST-reject fixture landing atomically means the enforcement requirement and its testable surface are inseparable from day one.

The MUST-reject case is the one that actually catches regressions — a refactor that silently re-enables the embedded-key path passes the positive fixture without issue. Without the negative vector, that regression is invisible until it matters in production.

@tomjwxf
Copy link
Copy Markdown
Contributor

tomjwxf commented May 1, 2026

Pushed the suggested-change commit: 6c84968.

What changed:

  • Moved the APS verification key out of external_receipts.aps.signing_key.
  • Added sidecar JWKS at test-vectors/keys/aps-ku-cross-verify.jwks.
  • Added external_receipts.aps.verification_key_ref to the positive fixture.
  • Added test-vectors/cross-verify-embedded-key-bundle.json as the MUST-reject embedded-key fixture.
  • Updated test-vectors/generate.mjs so rerunning the generator preserves the positive fixture, sidecar JWKS, and negative fixture.
  • Updated test-vectors/README.md with the positive and negative verification expectations.

Local validation run:

node -e "for (const f of ['test-vectors/cross-verify-bundle.json','test-vectors/cross-verify-embedded-key-bundle.json','test-vectors/keys/aps-ku-cross-verify.jwks']) JSON.parse(require('fs').readFileSync(f,'utf8'))"
npm test

Result: JSON parse checks passed; npm test passed with 75/75 tests.

@aeoess could you re-run the APS-side signer/verifier against the sidecar-key path and confirm the 10/10 cross-layer hash check still threads cleanly?

@desiorac I added the contrast-pair fixture so the embedded-key rejection is testable directly, not just implied by the positive case.

@desiorac
Copy link
Copy Markdown

desiorac commented May 1, 2026

The contrast-pair structure lands correctly: sidecar JWKS at the independent anchor path, MUST-accept positive fixture, MUST-reject embedded-key negative fixture. The contributingSources[i].accessReceiptId: sha256:<jcs-canonical-sha256> commit shape covers bundle-integrity without additional machinery.

Two things worth confirming before merge:

  1. The negative-case error string (verification key transported inside receipt without independent anchor) — worth asserting it as a literal expectation in the test runner rather than just documenting it in the PR body. Conformance regressions surface immediately if the string changes instead of silently passing.

  2. generate.mjs preserves-fixture-set behavior: confirm the generator doesn't overwrite the MUST-reject bundle on rerun. If the negative fixture is static and excluded from generation scope, that's the right call — the spec gap it covers shouldn't be auto-generated away.

Both minor — the structural piece resolves the embedded-key anchoring gap cleanly.

@aeoess
Copy link
Copy Markdown
Author

aeoess commented May 1, 2026

@tomjwxf, ran the APS-side check against 6c84968. Results:

Cross-layer hash check: 10/10 PASS. Each contributingSources[].accessReceiptId (sha256 prefix) matches the JCS-canonical sha256 of the corresponding entry in bundle.receipts, walking the 4 models × 2 rounds + synthesis + aggregate progression in derivation-depth order (claude-opus-4.6, gpt-5, gemini-2.5-pro, grok-4.20 across depths 1-8, then scopeblind-reference at depths 9-10).

APS DecisionLineageReceipt signature: VALID. Verified against the sidecar JWKS key (kid: aps-ku-cross-verify-v1, issuer aps:test:ku-cross-verify, pubkey e91c6bc5...0faa). JCS-canonical payload over all receipt fields except signature, Ed25519 verify clean.

Cross-layer integrity property holds end-to-end: tampering any byte of any of the 10 KU receipts breaks the recorded accessReceiptId even though the APS receipt's own Ed25519 signature stays cryptographically valid. That's the property the bundle is meant to demonstrate, and it threads through the new sidecar-key path without regression.

@desiorac, both pre-merge points are right.

  1. Literal error-string assertion in test runner. verification key transported inside receipt without independent anchor should be a string equality check in conformance tests, not just PR-body documentation. Conformance regressions on the rejection path are silent otherwise. Worth pinning the exact string in a constant the CLI imports from a single source so a future docs/code drift can't desync it.

  2. generate.mjs static-fixture preservation. The MUST-reject bundle covers a spec-gap that the generator shouldn't be authoritative over. If a regenerator pass overwrites the negative fixture, the fixture's value as a regression target collapses. Static-fixture handling is the right call.

One observation worth flagging from the run, separate from this PR: @veritasacta/verify 0.6.0 currently passes the negative fixture (10/10 valid) because the embedded-key rejection logic lands with the draft-02 enforcement work, not in 0.6.0. That's expected per the framing in this thread, and the contrast-pair fixture is exactly the regression target the upcoming enforcement release needs. Worth a one-line note in the README that the negative fixture is "MUST reject in v0.7+ once draft-02 enforcement ships" so a current reader running v0.6.0 doesn't conclude the fixture is wrong.

Also flagging: the README example npx @veritasacta/verify ... --jwks test-vectors/keys/aps-ku-cross-verify.jwks fails on the current CLI with jwks_fetch_failed because --jwks expects a URL (HTTP/HTTPS), not a file path. Either the CLI needs to handle file:// and bare paths, or the README example should use a hosted URL. Not blocking this PR, but noting for the 0.7 work.

Approving once the two desiorac items land. The reshape is the right shape.

@desiorac
Copy link
Copy Markdown

desiorac commented May 1, 2026

@aeoess — confirmed. Both items stand.

README v0.7+ note: correct framing — the negative fixture is a regression target for the enforcement landing, not a test of current behavior. Pinning it to the spec boundary prevents misread by any 0.6.0 consumer running the suite.

--jwks file path: agreed, HTTP-only breaks local development workflows entirely. file:// and bare path handling belong in the 0.7 scope alongside the enforcement work. Worth a separate issue so it does not get absorbed into the draft-02 PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants