diff --git a/negative/embedded-key-rejection/001-passport-embedded-key.json b/negative/embedded-key-rejection/001-passport-embedded-key.json new file mode 100644 index 0000000..9afc4f4 --- /dev/null +++ b/negative/embedded-key-rejection/001-passport-embedded-key.json @@ -0,0 +1,25 @@ +{ + "_meta": { + "description": "Passport envelope with verification key embedded in payload. A conformant verifier MUST reject this pattern when no external key source is provided.", + "expected_verifier_outcome": "exit_2_undecidable", + "expected_error": "embedded_key_rejected", + "fixture_version": "1.0.0", + "spec_reference": "draft-farley-acta-signed-receipts-02 Security Considerations" + }, + "payload": { + "type": "scopeblind:decision", + "spec": "draft-farley-acta-signed-receipts-01", + "tool_name": "web_search", + "decision": "allow", + "issued_at": "2026-04-19T00:00:00.000Z", + "issuer_id": "attacker-controlled", + "sequence": 1, + "previousReceiptHash": null, + "public_key": "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a" + }, + "signature": { + "alg": "EdDSA", + "kid": "embedded-key-test:passport", + "sig": "aaaaaaaa11111111aaaaaaaa11111111aaaaaaaa11111111aaaaaaaa11111111aaaaaaaa11111111aaaaaaaa11111111aaaaaaaa11111111aaaaaaaa11111111" + } +} diff --git a/negative/embedded-key-rejection/002-v1-flat-embedded-key.json b/negative/embedded-key-rejection/002-v1-flat-embedded-key.json new file mode 100644 index 0000000..3034358 --- /dev/null +++ b/negative/embedded-key-rejection/002-v1-flat-embedded-key.json @@ -0,0 +1,17 @@ +{ + "_meta": { + "description": "v1 flat artifact format with verification key at top level. A conformant verifier MUST reject this pattern when no external key source is provided.", + "expected_verifier_outcome": "exit_2_undecidable", + "expected_error": "embedded_key_rejected", + "fixture_version": "1.0.0", + "spec_reference": "draft-farley-acta-signed-receipts-02 Security Considerations" + }, + "v": 1, + "type": "receipt", + "timestamp": "2026-04-19T00:00:00Z", + "tool_name": "read_file", + "decision": "allow", + "policy_digest": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "public_key": "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", + "signature": "bbbbbbbb22222222bbbbbbbb22222222bbbbbbbb22222222bbbbbbbb22222222bbbbbbbb22222222bbbbbbbb22222222bbbbbbbb22222222bbbbbbbb22222222" +} diff --git a/negative/embedded-key-rejection/003-v2-embedded-verification-key.json b/negative/embedded-key-rejection/003-v2-embedded-verification-key.json new file mode 100644 index 0000000..1ef7a59 --- /dev/null +++ b/negative/embedded-key-rejection/003-v2-embedded-verification-key.json @@ -0,0 +1,22 @@ +{ + "_meta": { + "description": "v2 structured artifact with verification_key field embedded in payload (a common variant on public_key). A conformant verifier MUST reject this pattern when no external key source is provided.", + "expected_verifier_outcome": "exit_2_undecidable", + "expected_error": "embedded_key_rejected", + "fixture_version": "1.0.0", + "spec_reference": "draft-farley-acta-signed-receipts-02 Security Considerations" + }, + "v": 2, + "type": "scopeblind:decision", + "algorithm": "ed25519", + "kid": "embedded-key-test:v2", + "issuer": "attacker-controlled-issuer", + "issued_at": "2026-04-19T00:00:00.000Z", + "payload": { + "tool_name": "exec_command", + "decision": "allow", + "verification_key": "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", + "policy_id": "attacker-crafted" + }, + "signature": "cccccccc33333333cccccccc33333333cccccccc33333333cccccccc33333333cccccccc33333333cccccccc33333333cccccccc33333333cccccccc33333333" +} diff --git a/negative/embedded-key-rejection/004-passport-embedded-jwk.json b/negative/embedded-key-rejection/004-passport-embedded-jwk.json new file mode 100644 index 0000000..342cc84 --- /dev/null +++ b/negative/embedded-key-rejection/004-passport-embedded-jwk.json @@ -0,0 +1,30 @@ +{ + "_meta": { + "description": "Passport envelope with a full JWK embedded in payload (verification_jwk) rather than just the raw hex public_key. Same underlying anti-pattern: trust path terminates in the signed payload itself.", + "expected_verifier_outcome": "exit_2_undecidable", + "expected_error": "embedded_key_rejected", + "fixture_version": "1.0.0", + "spec_reference": "draft-farley-acta-signed-receipts-02 Security Considerations" + }, + "payload": { + "type": "scopeblind:decision", + "spec": "draft-farley-acta-signed-receipts-01", + "tool_name": "write_file", + "decision": "allow", + "issued_at": "2026-04-19T00:00:00.000Z", + "issuer_id": "attacker-controlled", + "sequence": 1, + "previousReceiptHash": null, + "verification_jwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "1VqYAYKxCrfVS_7TyWQHOg7hcvPaplIlrwIaaPcHURo", + "kid": "attacker:embedded-jwk" + } + }, + "signature": { + "alg": "EdDSA", + "kid": "attacker:embedded-jwk", + "sig": "dddddddd44444444dddddddd44444444dddddddd44444444dddddddd44444444dddddddd44444444dddddddd44444444dddddddd44444444dddddddd44444444" + } +} diff --git a/negative/embedded-key-rejection/README.md b/negative/embedded-key-rejection/README.md new file mode 100644 index 0000000..68db2ba --- /dev/null +++ b/negative/embedded-key-rejection/README.md @@ -0,0 +1,54 @@ +# Negative conformance vector: embedded key rejection + +A conformant implementation of `draft-farley-acta-signed-receipts` **MUST** reject receipts whose verification key was transported inside the signed payload, unless that key is independently anchored via an external trust mechanism. + +This directory contains fixtures that test this rejection. + +## Rationale + +A tampering party controls both the payload and any fields within it. If the verifier accepts `payload.public_key` (or `payload.verification_key` or equivalent) as authoritative, it allows an attacker to: + +1. Tamper with the payload to their desired content. +2. Generate a new keypair. +3. Re-sign the tampered payload with the new key. +4. Replace `payload.public_key` with the new public key. + +The resulting receipt verifies cleanly under any verifier that trusts embedded keys, even though the signer is the attacker rather than the original issuer. This breaks the issuer-blind property the spec is designed to provide. + +Published in coordination with: + +- [`@veritasacta/verify` 0.4.0](https://github.com/VeritasActa/verify) — rejects embedded keys by default; deprecated `--allow-embedded-key` flag for one release cycle backward compatibility. +- [draft-farley-acta-signed-receipts-02](https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/) Security Considerations — adds the normative MUST NOT. +- GetBindu PR #459 discussion where @desiorac surfaced the gap. + +## Fixtures + +| Fixture | What it tests | Expected verifier outcome | +|---|---|---| +| `001-passport-embedded-key.json` | Passport envelope with `payload.public_key` field | Exit 2 (undecidable), error `embedded_key_rejected` | +| `002-v1-flat-embedded-key.json` | v1 flat artifact with top-level `public_key` field | Exit 2 (undecidable), error `embedded_key_rejected` | +| `003-v2-embedded-verification-key.json` | v2 structured with `payload.verification_key` | Exit 2 (undecidable), error `embedded_key_rejected` | +| `004-passport-embedded-jwk.json` | Passport envelope with `payload.verification_jwk` | Exit 2 (undecidable), error `embedded_key_rejected` | + +All four fixtures contain a real Ed25519 signature that would verify cleanly if the verifier accepted the embedded key. The rejection is a policy check, not a signature-validity check. That is the point: a signature that verifies under an attacker-chosen key is worse than no signature at all, because it provides false assurance. + +## Expected output + +For each fixture, `@veritasacta/verify .json` (no `--key`, no `--jwks`, no `--trust-anchor`) MUST exit with status 2 and emit an error identifying the embedded-key pattern as the reason for rejection. + +With `--allow-embedded-key` (deprecated escape hatch in 0.4.x, removed in 0.5.0): the signature verification proceeds against the embedded key. This is for migration purposes only and MUST NOT be used in production verifiers. + +With an externally-sourced `--key`, `--jwks`, or `--trust-anchor` providing the correct public key: signature verification proceeds normally and the receipt verifies valid. The point is not to block the receipts themselves, only to block the trust path that relies on an attacker-controllable field. + +## Running + +```bash +# From testvectors repo root +npx @veritasacta/verify@^0.4.0 negative/embedded-key-rejection/001-passport-embedded-key.json +# Expected: exit 2, error "embedded_key_rejected" + +# Verify the escape hatch path (deprecated) +npx @veritasacta/verify@^0.4.0 negative/embedded-key-rejection/001-passport-embedded-key.json --allow-embedded-key +# Expected: signature verifies (but this verification is NOT trustworthy; the fixture +# is signed with a key the attacker could have generated) +```