From e510d0fa42dc9318fcc9f58404e0ad8af3cf423d Mon Sep 17 00:00:00 2001 From: Shreyansh Sancheti <43677304+shreyanshjain7174@users.noreply.github.com> Date: Sun, 18 Oct 2026 05:58:17 +0530 Subject: [PATCH] docs(receipts): freeze v1 receipt contract Pin deterministic vectors to prevent silent v1 changes. Signed-off-by: Shreyansh Sancheti <43677304+shreyanshjain7174@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 2 + docs/spec/RECEIPT-v1.md | 663 ++++++++++++++++++ pkg/receiptspec/DECISION.md | 2 + pkg/receiptspec/receipt.go | 2 + .../testdata/v1/bad-prev-hash.jsonl | 6 + pkg/receiptspec/testdata/v1/empty-chain.jsonl | 2 + .../testdata/v1/encoding-lock.json | 42 ++ pkg/receiptspec/testdata/v1/example.json | 9 + pkg/receiptspec/testdata/v1/gen/main.go | 262 +++++++ .../testdata/v1/inactive-keys.json | 16 + .../testdata/v1/inactive-signer.jsonl | 3 + .../testdata/v1/invalid-fields.jsonl | 6 + .../v1/manifest-endpoint-mismatch.jsonl | 6 + .../v1/manifest-keyset-mismatch.jsonl | 5 + .../testdata/v1/manifest-mismatch.jsonl | 6 + .../testdata/v1/manifest-unknown-signer.jsonl | 6 + .../testdata/v1/missing-middle.jsonl | 5 + .../v1/missing-tail-with-manifest.jsonl | 5 + pkg/receiptspec/testdata/v1/public-keys.json | 16 + .../testdata/v1/reordered-chain.jsonl | 6 + .../testdata/v1/tampered-body.jsonl | 6 + .../testdata/v1/unknown-signer.jsonl | 6 + .../testdata/v1/valid-boundary.jsonl | 4 + pkg/receiptspec/testdata/v1/valid-chain.jsonl | 6 + .../testdata/v1/valid-genesis.jsonl | 3 + .../testdata/v1/valid-partial.jsonl | 5 + .../v1/valid-unchecked-manifest-count.jsonl | 6 + .../testdata/v1/valid-unpinned-prefix.jsonl | 4 + pkg/receiptspec/testdata/v1/vectors.json | 225 ++++++ .../testdata/v1/wrong-key-signature.jsonl | 6 + pkg/receiptspec/vectors_test.go | 235 +++++++ 31 files changed, 1576 insertions(+) create mode 100644 docs/spec/RECEIPT-v1.md create mode 100644 pkg/receiptspec/testdata/v1/bad-prev-hash.jsonl create mode 100644 pkg/receiptspec/testdata/v1/empty-chain.jsonl create mode 100644 pkg/receiptspec/testdata/v1/encoding-lock.json create mode 100644 pkg/receiptspec/testdata/v1/example.json create mode 100644 pkg/receiptspec/testdata/v1/gen/main.go create mode 100644 pkg/receiptspec/testdata/v1/inactive-keys.json create mode 100644 pkg/receiptspec/testdata/v1/inactive-signer.jsonl create mode 100644 pkg/receiptspec/testdata/v1/invalid-fields.jsonl create mode 100644 pkg/receiptspec/testdata/v1/manifest-endpoint-mismatch.jsonl create mode 100644 pkg/receiptspec/testdata/v1/manifest-keyset-mismatch.jsonl create mode 100644 pkg/receiptspec/testdata/v1/manifest-mismatch.jsonl create mode 100644 pkg/receiptspec/testdata/v1/manifest-unknown-signer.jsonl create mode 100644 pkg/receiptspec/testdata/v1/missing-middle.jsonl create mode 100644 pkg/receiptspec/testdata/v1/missing-tail-with-manifest.jsonl create mode 100644 pkg/receiptspec/testdata/v1/public-keys.json create mode 100644 pkg/receiptspec/testdata/v1/reordered-chain.jsonl create mode 100644 pkg/receiptspec/testdata/v1/tampered-body.jsonl create mode 100644 pkg/receiptspec/testdata/v1/unknown-signer.jsonl create mode 100644 pkg/receiptspec/testdata/v1/valid-boundary.jsonl create mode 100644 pkg/receiptspec/testdata/v1/valid-chain.jsonl create mode 100644 pkg/receiptspec/testdata/v1/valid-genesis.jsonl create mode 100644 pkg/receiptspec/testdata/v1/valid-partial.jsonl create mode 100644 pkg/receiptspec/testdata/v1/valid-unchecked-manifest-count.jsonl create mode 100644 pkg/receiptspec/testdata/v1/valid-unpinned-prefix.jsonl create mode 100644 pkg/receiptspec/testdata/v1/vectors.json create mode 100644 pkg/receiptspec/testdata/v1/wrong-key-signature.jsonl create mode 100644 pkg/receiptspec/vectors_test.go diff --git a/README.md b/README.md index d7b41f2..efc4770 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,8 @@ text output to print only the `PASS:` summary on successful verification. ### Verify receipts from other producers +The frozen [receipt v1 specification](docs/spec/RECEIPT-v1.md) defines the signed bytes and verification rules. + The receipt protocol lives in a public, standard-library-only Go package: [`pkg/receiptspec`](pkg/receiptspec). It holds the receipt type, canonical hashing, Ed25519 signing, chain and export verification, and JSONL encoding. diff --git a/docs/spec/RECEIPT-v1.md b/docs/spec/RECEIPT-v1.md new file mode 100644 index 0000000..94c904b --- /dev/null +++ b/docs/spec/RECEIPT-v1.md @@ -0,0 +1,663 @@ +# AgentGate receipt v1 + +Status: frozen from `origin/main` at `f42a5a80b9937a5bbcad9013cb1dbd3baa883b1e`. +This document describes that implementation, including its permissive parsing and verification limits. +It does not change production behavior. + +The protocol implementation is [`pkg/receiptspec`](../../pkg/receiptspec). +Gateway params hashing lives in [`internal/receipt`](../../internal/receipt). +The standalone verifier is [`cmd/agentgate-verify`](../../cmd/agentgate-verify). + +## 1. Values and limits + +Lengths below count bytes, not Unicode characters. +Required strings contain valid UTF-8, contain no NUL byte, and are nonempty. +Strings are not Unicode-normalized. + +| Field | Semantic type | Accepted values | +|---|---|---| +| `seq` | unsigned 64-bit integer | `1` through `18446744073709551615` | +| `timestamp_unix_ns` | unsigned 64-bit integer | `1` through `18446744073709551615` | +| `human_principal` | required string | At most 256 bytes | +| `agent_key_id` | required string | At most 128 bytes | +| `delegation_chain` | ordered string list | At most 32 elements | +| Each delegation element | ASCII string | 0 through 64 bytes; no NUL | +| `service` | required string | At most 64 bytes | +| `action` | required string | At most 128 bytes | +| `params_sha256` | 32 bytes | Any byte value | +| `policy_decision` | string enum | Exactly `allow`, `deny`, or `rate_limited` | +| `status_code` | integer | 100 through 599, inclusive | +| `latency_ms` | signed 64-bit integer | 0 through `9223372036854775807` | +| `error` | string | Empty, or 1 through 64 ASCII bytes matching `[a-z0-9_]+` | +| `prev_hash` | 32 bytes | Any byte value; linkage adds constraints | +| `entry_hash` | 32 bytes | SHA-256 of the receipt preimage | +| `signer_kid` | required string | At most 128 bytes | +| `signature` | 64 bytes | Ed25519 signature | + +`timestamp_unix_ns` represents nanoseconds since the Unix epoch. +Validation requires only a nonzero value. +It imposes no timestamp ordering, clock-skew limit, or calendar range. +`status_code`, `error`, and `policy_decision` have no cross-field consistency checks. +Error codes have a character rule, not a closed vocabulary. +Provider error text does not belong in this field. + +`Validate` returns `ErrInvalidReceipt` for invalid numbers. +It returns `ErrInvalidField` for invalid strings or delegation lists. +It does not authenticate hashes or signatures. +The receipt verifier does not recover params from their hash. + +## 2. Receipt hash preimage + +Define `U32LE(x)` and `U64LE(x)` as unsigned little-endian integers occupying 4 and 8 bytes. +Define `LP(s) = U32LE(len(UTF8(s))) || UTF8(s)`. +`||` means byte concatenation. +There is no padding or alignment. +Only the domain string has a terminating NUL. + +`CanonicalHashInput` validates the receipt, then concatenates these fields in this exact order: + +| Order | Input | Encoding | +|---|---|---| +| 1 | Domain | ASCII `agentgate.receipt.hash.v1`, then `00`; 26 bytes total | +| 2 | `seq` | `U64LE(seq)` | +| 3 | `timestamp_unix_ns` | `U64LE(timestamp_unix_ns)` | +| 4 | `human_principal` | `LP(human_principal)` | +| 5 | `agent_key_id` | `LP(agent_key_id)` | +| 6 | Delegation element count | `U32LE(count)` | +| 7 | Delegation elements | `LP(element)` for each element, in list order | +| 8 | `service` | `LP(service)` | +| 9 | `action` | `LP(action)` | +| 10 | `params_sha256` | 32 raw bytes; no length prefix | +| 11 | `policy_decision` | `LP(policy_decision)` | +| 12 | `status_code` | Convert to `int32`, then `uint32`, then `U32LE` | +| 13 | `latency_ms` | Convert to `uint64`, then `U64LE` | +| 14 | `error` | `LP(error)`; an empty error contributes `00000000` | +| 15 | `prev_hash` | 32 raw bytes; no length prefix | +| 16 | `signer_kid` | `LP(signer_kid)` | + +`entry_hash = SHA256(CanonicalHashInput(receipt))`. +Neither `entry_hash` nor `signature` appears in this preimage. +`signer_kid` and `prev_hash` do appear. +The receipt's JSON `format_version` and `type` do not appear. +The domain selects the binary version. +Valid maximum-length fields produce at most 3106 preimage bytes. +Nil and empty delegation lists both contribute a zero element count. +An empty delegation element contributes its own 4-byte zero length. + +## 3. Params SHA-256 + +### 3.1 Gateway params + +`DigestParams` and `ParseAndDigestParams` implement the gateway rule. +The receipt contains only the resulting 32-byte SHA-256 digest. + +1. Trim leading and trailing whitespace with Go `bytes.TrimSpace`. +2. Normalize empty input and the exact trimmed token `null` to `{}`. +3. Require valid UTF-8 and valid escaped surrogate pairs. +4. Decode exactly 1 JSON object with `json.Decoder.UseNumber`. +5. Reject trailing JSON values or invalid syntax. +6. Reject duplicate decoded object names at every nesting level. +7. Reject excessive nesting and invalid string or number values. +8. Serialize the decoded object with the rules below. +9. Reject canonical output larger than `1048576` bytes. +10. Return SHA-256 of the canonical bytes. + +The top-level object counts as depth 1. +Each nested object or array adds 1. +Depth 32 is accepted. Depth 33 is rejected. +Scalars do not add a nesting level. +There is no raw-input size check inside these functions. +The size check applies after canonicalization. + +Object names sort lexicographically by unsigned UTF-16 code units. +Array order is preserved. +Whitespace outside strings is removed. +`null`, `true`, and `false` retain those lowercase spellings. +Decoded strings retain their Unicode scalar sequence without normalization. +Escape `"` and `\` with a backslash. +Use `\b`, `\t`, `\n`, `\f`, and `\r` for those control characters. +Other U+0000 through U+001F characters use lowercase `\u00xx`. +Other characters remain literal UTF-8. +In particular, `/`, `<`, `>`, `&`, U+2028, and U+2029 are not escaped. + +Both names and string values reject Unicode noncharacters. +These are U+FDD0 through U+FDEF and each plane's U+FFFE and U+FFFF. +Lone or reversed surrogate escapes are rejected. +Proper high-surrogate and low-surrogate pairs are accepted. +Escaped NUL is accepted in params strings. +Names such as `"a"` and `"\u0061"` are duplicates. +Composed `é` and decomposed `e\u0301` remain distinct. + +Numbers follow the implemented RFC 8785 binary64 path. +Each numeric token passes through Go `strconv.ParseFloat(text, 64)`. +Conversion errors, infinities, and NaN are rejected. +Accepted values use `github.com/gowebpki/jcs` v1.0.1 `NumberToJSON`. +This uses ECMAScript's shortest round-trippable binary64 decimal representation. +Both signs of zero become `0`. +Nonzero absolute values from `1e-6` inclusive to `1e21` exclusive use ordinary decimal notation. +Other nonzero values use lowercase `e` notation, without exponent zero padding. +Positive exponents include `+`. +Fractions and exponent notation are accepted. +Integers outside the exact binary64 range can round. +Small values can underflow to zero without a conversion error. +There is no safe-integer restriction for gateway params. + +Failures return `ErrInvalidParams`, except excessive canonical size returns `ErrParamsTooLarge`. +These functions preserve original numeric text in the returned params map. +That preserved text is not the canonical number spelling used for the digest. + +### 3.2 Non-gateway decision records + +`HashRecord` is a separate, narrower canonicalization API. +See [`DECISION.md`](../../pkg/receiptspec/DECISION.md). +Its digest can also occupy `params_sha256`. +It accepts a JSON value, not just an object. +Arrays and scalar roots are accepted by the implementation. +It rejects duplicate names, invalid UTF-8, lone surrogates, and depth greater than 32. +Input and canonical output are each limited to `1048576` bytes. + +Its number tokens must match `^-?(0|[1-9][0-9]*)$`, excluding `-0`. +Their integer values must fall within `[-9007199254740991, 9007199254740991]`. +Fractions and exponent tokens return `ErrUnsupportedNumber`. +For non-`json.RawMessage` inputs, Go `json.Marshal` runs first. +Thus a Go float containing `1.0` can marshal as `1` and pass. +Other invalid records return `ErrInvalidRecord`. +Unlike gateway params, this API does not reject Unicode noncharacters. +It uses the same UTF-16 sorting and minimal string escaping described above. +`VerifyRecordBinding` recomputes this digest and returns `ErrRecordMismatch` on inequality. +It does not check the receipt signature or chain. + +## 4. Signatures and key IDs + +Signatures use plain Ed25519 from Go `crypto/ed25519`. +There is no Ed25519ph mode or Ed25519 context. +The signed message is the raw 32-byte `entry_hash`. +It is not the hexadecimal hash or the full preimage. +`Seal` sets `signer_kid`, computes the hash, then signs that hash. +It requires the signer to return exactly 64 signature bytes. + +The standard key ID is: + +```text +"ed25519:" || lowercase_hex(SHA256(raw_32_byte_public_key)[0:8]) +``` + +The SHA-256 slice contains the first 8 digest bytes, yielding 16 hex characters. +The resulting key ID occupies 24 ASCII bytes. +`internal/signer.ComputeKID` forwards to `receiptspec.ComputeKID`. +Receipt validation treats key IDs as opaque required strings. +Verification looks up their supplied names. It does not recompute key IDs. + +The signer store's `HMAC-SHA256(master_key, "agentgate.signer.v1")` derives an at-rest encryption key. +That storage derivation is not a key ID or receipt signature input. +Rotation uses externally supplied public keys with inclusive sequence windows. +The previous key does not sign the new key. +A nil `valid_until_seq` means no upper bound. +You must obtain key history through a trusted channel. + +## 5. Chain linkage and producer behavior + +Genesis has sequence `1` and a `prev_hash` containing 32 zero bytes. +Each later receipt increments the sequence by exactly 1. +Its `prev_hash` equals the preceding receipt's `entry_hash`. +A partial chain needs a trusted preceding `(seq, entry_hash)` anchor. +The verifier does not sort receipt input. + +`NewChain(signer, 0, zero_hash)` starts at genesis. +`Next` advances the in-memory head only after validation and signing succeed. +A zero requested timestamp uses `time.Now().UnixNano()`, converted to `uint64`. +Other timestamps are used unchanged. +Sequence arithmetic uses Go `uint64` arithmetic. +Advancing after `MaxUint64` produces sequence 0, which validation rejects. + +The gateway ledger reads its committed head inside `BEGIN IMMEDIATE`. +It allocates sequences, sets timestamps, signs, and inserts before `COMMIT`. +Successful append results require that commit. +Rolled-back transactions consume no sequence. +SQLite stores binary hashes and signatures as raw blobs. +Rows store `format_version = 1`; a nil delegation list is stored as `[]`. +SQLite integer storage cannot represent the entire unsigned wire range. +The boundary vectors exercise the public protocol, not SQLite capacity. + +A suffix can disappear without breaking any remaining link. +Without a separately expected endpoint or a manifest, verification cannot detect that deletion. +Even a signed manifest only proves the range selected by its signer. +It does not prove that every external action was recorded. +SSE v1 receipts attest header-time upstream acceptance, not final stream bytes or completion. +The [stream digest proposal](proposals/receipt-stream-digest.md) remains unimplemented. + +## 6. Delegation elements + +The gateway delegation verifier emits 1 digest per appended Biscuit attenuation block. +The authority block is excluded. +Elements follow block append order. +An authority-only grant produces a nil list. +For each block index `i`, the element is: + +```text +lowercase_hex(SHA256(UTF8(biscuit.Code()[i]))) +``` + +`biscuit.Code()` supplies the library's Datalog rendering. +The digest does not cover raw token bytes, signatures, or the original source spelling. +There is no additional domain prefix for these block digests. +The receipt hashes each 64-character hex element as an LP ASCII string, not as 32 decoded bytes. +The receipt verifier does not reconstruct or authorize Biscuit tokens. + +The receipt validator's accepted space is wider than the gateway's output. +It accepts empty elements and nonhex ASCII, including controls other than NUL. +It does not require exactly 64 characters or valid hex. +The receipt limit is 32 elements. +The gateway delegation verifier's default limit is 8 attenuation blocks and 4096 token bytes. +Those configurable gateway limits do not change receipt encoding. + +## 7. JSONL encoding + +`WriteJSONL` emits 1 JSON object per line, followed by LF. +`MarshalJSONLReceipt` returns the object without LF. +It does not validate semantic receipt fields before marshaling. +Whitespace and JSON member order are not hash inputs. + +The writer emits receipt members in this order: + +| Order | Member | JSON type and encoding | +|---|---|---| +| 1 | `type` | String `"receipt"` | +| 2 | `format_version` | Integer `1` | +| 3 | `seq` | Unsigned integer, unquoted | +| 4 | `timestamp_unix_ns` | Unsigned integer, unquoted | +| 5 | `human_principal` | String | +| 6 | `agent_key_id` | String | +| 7 | `delegation_chain` | String array; nil becomes `null`, empty becomes `[]` | +| 8 | `service` | String | +| 9 | `action` | String | +| 10 | `params_sha256` | String, 64 lowercase hex characters | +| 11 | `policy_decision` | String | +| 12 | `status_code` | Integer, unquoted | +| 13 | `latency_ms` | Integer, unquoted | +| 14 | `error` | String, present even when empty | +| 15 | `prev_hash` | String, 64 lowercase hex characters | +| 16 | `entry_hash` | String, 64 lowercase hex characters | +| 17 | `signer_kid` | String | +| 18 | `signature` | String, 128 lowercase hex characters | + +No field uses base64. +Do not parse 64-bit integers through binary64 in another language. +Use exact integer parsing. +JSON strings use Go `encoding/json` escaping, not the canonical params serializer. +The writer escapes `<`, `>`, `&`, U+2028, and U+2029. +Those JSON escape choices do not affect decoded receipt hashes. + +The reader trims surrounding whitespace and skips empty lines. +Its scanner has a `1 << 24` maximum token buffer. +Oversized lines or scanner errors fail parsing. +It accepts uppercase or lowercase hex, but rejects invalid hex and incorrect decoded lengths. +It rejects unsupported or missing receipt `format_version`. +Numeric tokens must fit their destination Go integer types. +Semantic checks happen during verification, not parsing. + +`encoding/json` parsing accepts unknown members and case-insensitive field matches. +Duplicate members follow Go's merge or replacement behavior. They are not rejected. +Invalid UTF-8 and lone surrogate escapes in JSON strings can become U+FFFD. +Unlike params decoding, JSONL parsing does not explicitly reject them. +Subsequent hashing covers the decoded strings. +These are compatibility facts, not recommendations for new serializers. + +Missing or empty `type` defaults to receipt parsing. +`"key"` selects a key line and `"manifest"` selects a manifest line. +All other type strings also take the receipt parser. +Key and manifest parsers do not separately validate `type`. +Keys accumulate in input order. +A later manifest overwrites an earlier manifest. +Manifest and key lines need not precede receipts when parsing. +The export writer emits manifest, keys, then ascending receipts. + +### 7.1 Key lines and trust files + +Key lines contain these members in writer order: + +```text +type, kid, public_key_hex, valid_from_seq, valid_until_seq +``` + +`type` is `"key"`. `kid` and `public_key_hex` are strings. +The public key is 32 raw bytes encoded as 64 lowercase hex characters. +`valid_from_seq` is an unsigned 64-bit JSON integer. +`valid_until_seq` is an unsigned 64-bit JSON integer or `null`. +Missing `valid_until_seq` also decodes as nil. +Key lines have no `format_version`. +The trust-file shape is `{"keys":[{...}]}`, with the same key members except `type`. +`LoadTrustedKeys` rejects empty key lists and malformed public keys. +It does not reject duplicate IDs or inconsistent validity windows. +For receipt lookup, the last matching trust entry wins. +For manifest lookup, the first matching trust entry wins. +Supply distinct key IDs. + +## 8. Export manifests + +### 8.1 Keyset digest + +Copy embedded keys and sort them by `kid`, using Go string byte ordering. +For each key, append: + +```text +LP(kid) +raw_public_key_bytes +U64LE(valid_from_seq) +00 # no upper bound +``` + +For a bounded window, replace that final `00` with: + +```text +01 || U64LE(valid_until_seq) +``` + +SHA-256 the concatenation. +There is no domain prefix or key count. +Public keys have no length prefix. +The empty keyset digest is SHA-256 of empty bytes. +Distinct key IDs make this digest independent of input order. +Equal IDs have no secondary sorting key. + +### 8.2 Manifest preimage and signature + +Concatenate these fields: + +| Order | Input | Encoding | +|---|---|---| +| 1 | Domain | ASCII `agentgate.receipt.export-manifest.v1`, then `00` | +| 2 | `format_version` | `U32LE(uint32(format_version))` | +| 3 | `requested_from` | `U64LE` | +| 4 | `requested_to` | `U64LE`; zero means omitted request bound | +| 5 | `resolved_to` | `U64LE` | +| 6 | `count` | `U64LE(uint64(count))`; Go field type is `int` | +| 7 | `anchor_seq` | `U64LE` | +| 8 | `anchor_hash` | 32 raw bytes | +| 9 | `first_entry_hash` | 32 raw bytes | +| 10 | `last_entry_hash` | 32 raw bytes | +| 11 | `head_seq` | `U64LE` | +| 12 | `head_hash` | 32 raw bytes | +| 13 | `keyset_digest` | 32 raw bytes | +| 14 | `signer_kid` | `LP(signer_kid)` | + +`manifest_hash = SHA256(preimage)`. +The signature is plain Ed25519 over those 32 hash bytes. +The signature itself is excluded from the preimage. +`ComputeManifestHash` does not validate manifest semantics. +Direct calls to `VerifyManifest` do not check the version. +JSONL manifest parsing requires `format_version = 1`. +No dedicated manifest string-length, positive-count, or range-consistency checks exist. + +Manifest JSON members appear in this order: + +```text +type, format_version, requested_from, requested_to, resolved_to, count, +anchor_seq, anchor_hash, first_entry_hash, last_entry_hash, head_seq, +head_hash, keyset_digest, signer_kid, signature +``` + +`type` is `"manifest"`. +`requested_to` is omitted when zero. +All range and sequence fields are unsigned 64-bit JSON integers. +`format_version` and `count` are Go `int` JSON integers. +`signer_kid` is a string. +All hashes use 64 lowercase hex characters. +The signature uses 128 lowercase hex characters. +The reader also accepts uppercase hex and requires exact decoded lengths. + +### 8.3 Gateway export construction + +The export endpoint requires `from >= 1`. +If supplied, `to` must be at least `from`. +It reads head, anchor, rows, and signer keys from 1 read-only SQLite snapshot. +An omitted or excessive `to` resolves to the snapshot head. +An explicit lower `to` resolves to that bound. +`from > head + 1` is rejected. +Ranges exceeding 10000 receipts are rejected, not truncated. + +`anchor_seq = from - 1`. +The anchor hash comes from that sequence, or zero bytes at genesis. +`first_entry_hash` and `last_entry_hash` come from the selected rows. +For an empty selection, both equal the anchor hash. +`head_seq` and `head_hash` describe the snapshot head. +The keyset includes all stored signer keys. +The active signer signs the manifest after the read transaction finishes. +The endpoint can emit an empty selection, but `VerifyChain` rejects an empty chain. + +## 9. Verification algorithm and error classes + +### 9.1 Bundle checks + +`VerifyBundle` performs these steps: + +1. Use explicit trusted keys when nonempty. Otherwise use embedded key lines. +2. Start with a genesis anchor. +3. If a manifest exists, find its `signer_kid` in trusted keys. +4. Reject an unknown manifest signer with `ManifestError` wrapping `ErrUnknownSignerKID`. +5. Reject invalid key length or signature with `ManifestError` wrapping `ErrManifestSignatureInvalid`. +6. Recompute the embedded keyset digest. +7. Reject inequality with `ManifestError` wrapping `ErrKeysetDigestMismatch`. +8. Use the signed manifest's anchor. +9. Unless explicitly overridden, expect `(resolved_to, last_entry_hash)` as the endpoint. +10. Run the chain checks below. + +Manifest signing keys are not checked against sequence validity windows. +Explicit trust replaces embedded keys for lookup, but embedded keys still determine the signed keyset digest. +Embedded keys alone prove internal consistency, not the producer's identity. +Pin an independently obtained trust file when identity matters. + +The verifier does not separately check manifest `count`, `requested_from`, `requested_to`, or `first_entry_hash`. +It does not compare `head_seq` or `head_hash` against receipts. +Those fields are signed but are not independently enforced. +An explicit expected head replaces the manifest-derived endpoint, rather than adding another endpoint comparison. + +### 9.2 Chain checks + +`VerifyChain` returns a Go error if no trusted keys or no receipts exist. +These errors are `ErrNoTrustedKeys` and `ErrEmptyChain`, in that order. +Otherwise, process receipts in supplied order: + +1. Require `seq == anchor.seq + 1 + index`. +2. Require `prev_hash` to equal the anchor hash or preceding verified entry hash. +3. Look up `signer_kid`. An absent key returns `ErrUnknownSignerKID`. +4. Require the receipt sequence inside that key's inclusive validity window. +5. Validate every semantic field from section 1. +6. Recompute the canonical preimage and SHA-256. +7. Require equality with `entry_hash`. +8. Verify Ed25519 over the computed 32-byte hash. +9. Advance the verified count and head only after all checks succeed. +10. If supplied, require the final sequence and hash to match the expected head. + +Except unknown signers, per-receipt failures return `OK = false` without a Go error. +The first failing check determines the result: + +| Check | `VerifyResult.Reason` | +|---|---| +| Unexpected sequence | `sequence_gap_or_not_genesis_anchored` | +| Bad previous hash | `prev_hash_mismatch` | +| Key inactive at sequence | `signer_kid_inactive_at_seq` | +| Invalid semantic field | `invalid_receipt_fields` | +| Recomputed hash differs | `entry_hash_mismatch` | +| Bad key length or Ed25519 signature | `signature_invalid` | +| Final endpoint differs | `expected_head_mismatch` | + +`FailedAtSeq` is the receipt's sequence, except endpoint mismatch uses the expected sequence. +`VerifiedCount` counts only fully checked receipts. +Unknown signer errors return a zero result, discarding earlier progress. +`Complete` is true only when a supplied or manifest-derived endpoint matches. +It means completeness against that endpoint, not against unseen future receipts. +Without an endpoint, a valid prefix returns `OK = true` and `Complete = false`. +`VerifyReceipt` checks fields, entry hash, and signature only. +It does not check key-ID ownership, validity windows, sequence continuity, or previous-hash linkage. + +The CLI exits `0` for success, `1` for mismatches, and `2` for parsing or configuration errors. +All `ManifestError` values exit `1`, including an unknown manifest signer. +An unknown receipt signer exits `2`. +CLI range reporting uses `resolved_to == head_seq` to label an export `full`. +That label does not add a head-hash comparison. + +## 10. Worked bytes + +The committed generator computes these examples through the real receipt implementation. +Run it from the repository root: + +```sh +go run ./pkg/receiptspec/testdata/v1/gen -out pkg/receiptspec/testdata/v1 +go test ./pkg/receiptspec -run 'TestV1(GoldenVectors|VersionLock|WorkedExample)' -count=1 +``` + +[`example.json`](../../pkg/receiptspec/testdata/v1/example.json) contains the complete preimage, key, hash, and signature. +The corresponding input is [`valid-genesis.jsonl`](../../pkg/receiptspec/testdata/v1/valid-genesis.jsonl). +Its receipt has sequence 1, timestamp 1, principal `h`, agent `a`, service `s`, and action `x`. +Its params digest covers `{}`. +Its decision is `allow`, status is 100, latency is 0, and error is empty. +Its previous hash is 32 zero bytes. + +The deterministic Ed25519 seed is: + +```text +000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f +``` + +The public key and derived ID are: + +```text +03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8 +ed25519:56475aa75463474c +``` + +The preimage occupies 183 bytes. +Offsets below are zero-based and inclusive: + +| Offsets | Meaning | Hex bytes | +|---|---|---| +| 0..25 | Domain and NUL | `6167656e74676174652e726563656970742e686173682e763100` | +| 26..33 | Sequence 1 | `0100000000000000` | +| 34..41 | Timestamp 1 | `0100000000000000` | +| 42..46 | Principal | `0100000068` | +| 47..51 | Agent ID | `0100000061` | +| 52..55 | Delegation count 0 | `00000000` | +| 56..60 | Service | `0100000073` | +| 61..65 | Action | `0100000078` | +| 66..97 | Params digest | `44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a` | +| 98..106 | Decision | `05000000616c6c6f77` | +| 107..110 | Status 100 | `64000000` | +| 111..118 | Latency 0 | `0000000000000000` | +| 119..122 | Empty error | `00000000` | +| 123..154 | Genesis previous hash | 32 `00` bytes | +| 155..182 | Signer ID | `18000000656432353531393a35363437356161373534363334373463` | + +SHA-256 of that exact concatenation is: + +```text +b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138 +``` + +The Ed25519 signature over those 32 bytes is: + +```text +9b0b99df7d812a725ed1f810bbf913bf86b15de3152808cf97dc9ecdc45eb6e2f +0481c80490d77d140cc48152c2d337871ebffa343dc69c5f18ee4d294ea1705 +``` + +The signed `valid-chain` manifest hashes to: + +```text +8e8fb7b9943680a29e9ed35e93ad243d59e2b6ddf3c95c003e2c678d99810155 +``` + +Gateway params examples were also computed with `receipt.DigestParams`: + +| Input | Canonical UTF-8 | +|---|---| +| `{"b":2e0,"a":1.0}` | `{"a":1,"b":2}` | +| `{"n":-0}` | `{"n":0}` | +| `{"n":9007199254740993}` | `{"n":9007199254740992}` | +| `{"n":1e-400}` | `{"n":0}` | +| `{"n":1e20}` | `{"n":100000000000000000000}` | +| `{"n":1e21}` | `{"n":1e+21}` | +| `{"n":1e-6}` | `{"n":0.000001}` | +| `{"n":1e-7}` | `{"n":1e-7}` | + +SHA-256 of `{"a":1,"b":2}` is: + +```text +43258cff783fe7036d8a43033f830adfc60ec037382473548ac742b888292777 +``` + +## 11. Golden vectors and version lock + +[`vectors.json`](../../pkg/receiptspec/testdata/v1/vectors.json) lists every input file and expected result. +Each vector includes JSONL, external public keys, and embedded keys. +Bounded exports include their manifest as the first JSONL line. +Tests exercise both explicit and embedded trust. + +| Vector | Expected class | +|---|---| +| `valid-genesis` | `ok`, no completeness claim | +| `valid-chain` | `ok`, 3 receipts, rotation, delegation digests, Unicode, maximum timestamp and latency | +| `valid-partial` | `ok`, signed non-genesis anchor | +| `valid-boundary` | `ok`, maximum sequence and string sizes, 32 delegation elements | +| `valid-unpinned-prefix` | `ok`, deleted suffix cannot be detected without an endpoint | +| `valid-unchecked-manifest-count` | `ok`, signed count is not compared with receipts | +| `tampered-body` | `entry_hash_mismatch` | +| `reordered-chain` | `sequence_gap_or_not_genesis_anchored` | +| `missing-middle` | `sequence_gap_or_not_genesis_anchored` | +| `missing-tail-with-manifest` | `expected_head_mismatch` | +| `wrong-key-signature` | `signature_invalid` | +| `bad-prev-hash` | `prev_hash_mismatch` | +| `invalid-fields` | `invalid_receipt_fields` | +| `inactive-signer` | `signer_kid_inactive_at_seq` | +| `unknown-signer` | `ErrUnknownSignerKID` | +| `empty-chain` | `ErrEmptyChain` | +| `manifest-mismatch` | `ManifestError/ErrManifestSignatureInvalid` | +| `manifest-keyset-mismatch` | `ManifestError/ErrKeysetDigestMismatch` | +| `manifest-unknown-signer` | `ManifestError/ErrUnknownSignerKID` | +| `manifest-endpoint-mismatch` | `expected_head_mismatch` | + +Seeds are public, fixed, test-only values. Never deploy these keys. +Seed A contains bytes `00` through `1f`. Seed B contains `20` through `3f`. +The generator writes deterministic files and does not update `encoding-lock.json`. +That separate file pins SHA-256 digests of canonical receipt and manifest preimages for every accepted vector. +`TestV1VersionLock` compares real encoding against those committed digests. +Do not refresh the v1 lock to hide an encoding change. +The older independent binary fixture remains in `internal/receipt/testdata/v1`. +Its filename includes `genesis`, but its encoded sequence is `MaxUint64`, not 1. + +## 12. Versioning + +Receipt JSONL and SQLite rows declare `format_version = 1`. +Manifest JSONL independently declares `format_version = 1`. +Readers reject other versions with `ErrUnsupportedFormatVersion`. +Missing JSON versions decode to 0 and are rejected. +The receipt binary preimage selects v1 through `agentgate.receipt.hash.v1`. +The manifest preimage includes both its v1 domain and its integer version. + +Keep v1 byte encodings and accepted semantics frozen. +Changes to field order, widths, endianness, prefixes, or domain strings require v2. +Changing signed fields, hash algorithms, signature algorithms, or signature messages requires v2. +Changing params canonicalization or delegation digest meaning requires v2. +Adding policy decisions, changing bounds, or making previously accepted receipts invalid requires v2. +New authenticated payload fields also require v2. +Unsigned JSON metadata can be ignored today, but cannot silently gain signed meaning. +Changing writer formatting alone need not change receipt hashes. +Do not claim that newly emitted values are interoperable unless old v1 readers accept them. +A v2 implementation needs a new format version, separate domain strings, and `RECEIPT-v2.md`. +Preserve v1 vectors and verification support. + +## 13. Existing documentation differences + +Code takes precedence for this freeze. +No discrepancy below changes production behavior. + +- `DECISION.md` calls decision records objects. `CanonicalRecord` also accepts arrays and scalar roots. +- `DECISION.md` names a canonical-size limit. The code also limits raw input to 1 MiB. +- The old `genesis-unicode-max.bin` fixture contains sequence `MaxUint64`, not a genesis chain. +- Manifest comments describe bound metadata. Verification enforces the anchor, endpoint, signature, and keyset digest only. +- The export endpoint can emit an empty range. The verifier rejects it with `ErrEmptyChain`. +- Gateway delegation output is 64-character hex. Receipt validation accepts shorter nonhex ASCII elements. + +The stream-digest proposal correctly describes an unimplemented v2 feature. +Gateway params and decision records intentionally expose different numeric acceptance rules. +They must not be treated as interchangeable validators. diff --git a/pkg/receiptspec/DECISION.md b/pkg/receiptspec/DECISION.md index 2b295da..09a3865 100644 --- a/pkg/receiptspec/DECISION.md +++ b/pkg/receiptspec/DECISION.md @@ -1,5 +1,7 @@ # Decision records from non-gateway producers +See the frozen [receipt v1 specification](../../docs/spec/RECEIPT-v1.md) for bytes, signatures, and verification limits. + A producer that is not the AgentGate gateway (for example the Clawdlinux operator) can record an arbitrary decision as a normal v1 receipt. The wire format does not change. Any existing `agentgate-verify` checks the chain. diff --git a/pkg/receiptspec/receipt.go b/pkg/receiptspec/receipt.go index be35e1e..1828a29 100644 --- a/pkg/receiptspec/receipt.go +++ b/pkg/receiptspec/receipt.go @@ -8,6 +8,8 @@ Licensed under the Apache License, Version 2.0. // canonical hashing, Ed25519 signing, hash-chain verification, JSONL encoding, // signed export manifests, and decision-record binding. // +// The frozen wire contract is documented in docs/spec/RECEIPT-v1.md. +// // Any producer (the AgentGate gateway, an operator, a batch job) can build a // chain with Chain and any verifier can check it with VerifyChain or // VerifyBundle. The on-the-wire format is the same for every producer. diff --git a/pkg/receiptspec/testdata/v1/bad-prev-hash.jsonl b/pkg/receiptspec/testdata/v1/bad-prev-hash.jsonl new file mode 100644 index 0000000..83ecdf4 --- /dev/null +++ b/pkg/receiptspec/testdata/v1/bad-prev-hash.jsonl @@ -0,0 +1,6 @@ +{"type":"manifest","format_version":1,"requested_from":1,"resolved_to":3,"count":3,"anchor_seq":0,"anchor_hash":"0000000000000000000000000000000000000000000000000000000000000000","first_entry_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","last_entry_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","head_seq":3,"head_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","keyset_digest":"9877fcf6988d2245cabc51a00b26fa546ef0c9209e17a5fd66c6cda80fe075eb","signer_kid":"ed25519:24f6ed6acbfe1009","signature":"38a522420dd49b670936d9c003c596bbec6c7ea036856e1e6430f320da92de8225b45cd43af5b863e6b6889620bebbc42b8cb450660a7e2136654cd61c23a20e"} +{"type":"key","kid":"ed25519:56475aa75463474c","public_key_hex":"03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8","valid_from_seq":1,"valid_until_seq":2} +{"type":"key","kid":"ed25519:24f6ed6acbfe1009","public_key_hex":"29acbae141bccaf0b22e1a94d34d0bc7361e526d0bfe12c89794bc9322966dd7","valid_from_seq":3,"valid_until_seq":null} +{"type":"receipt","format_version":1,"seq":1,"timestamp_unix_ns":1,"human_principal":"h","agent_key_id":"a","delegation_chain":null,"service":"s","action":"x","params_sha256":"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","policy_decision":"allow","status_code":100,"latency_ms":0,"error":"","prev_hash":"0000000000000000000000000000000000000000000000000000000000000000","entry_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","signer_kid":"ed25519:56475aa75463474c","signature":"9b0b99df7d812a725ed1f810bbf913bf86b15de3152808cf97dc9ecdc45eb6e2f0481c80490d77d140cc48152c2d337871ebffa343dc69c5f18ee4d294ea1705"} +{"type":"receipt","format_version":1,"seq":2,"timestamp_unix_ns":1700000000000000001,"human_principal":"human-é/é/😀","agent_key_id":"agent-2","delegation_chain":["d2d8e9aea4f592e1ca94318065976dbe9147e57eaf6eb56e14ca9ae6004c6bae","482d488bd94eee647aa10471442b576f78c4646767b16616929cdef236d9eeb6"],"service":"operator","action":"decision:scale","params_sha256":"d110a9af75f1fb6bdecaf9590b2fa2b99df60f007e5ed4b37c72d57edbf11186","policy_decision":"deny","status_code":403,"latency_ms":17,"error":"scope_denied","prev_hash":"b8647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","entry_hash":"b2a27707d518092a9fa6b069a0ef1907489ed66ad9ae3196d4bffafe3affcfe2","signer_kid":"ed25519:56475aa75463474c","signature":"d070daaf2edf8115f713d5749e4d8894ed4dfd3191d74d0d0f1d13082247bdb54055fe1dd818607edd9f8b26f86f4eed16cbcac784f509c68b1dcf3f672fe60e"} +{"type":"receipt","format_version":1,"seq":3,"timestamp_unix_ns":18446744073709551615,"human_principal":"h","agent_key_id":"a","delegation_chain":null,"service":"s","action":"x","params_sha256":"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","policy_decision":"rate_limited","status_code":599,"latency_ms":9223372036854775807,"error":"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","prev_hash":"b2a27707d518092a9fa6b069a0ef1907489ed66ad9ae3196d4bffafe3affcfe2","entry_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","signer_kid":"ed25519:24f6ed6acbfe1009","signature":"628208d560ad848a6635343e2d0c0ab1b8f2bc07f2e6ff69c8f9f8bb11a54f5a70f8a26e42dafb797146ac252f6df58fa2d976d306fd17a9ce336bf84692f600"} diff --git a/pkg/receiptspec/testdata/v1/empty-chain.jsonl b/pkg/receiptspec/testdata/v1/empty-chain.jsonl new file mode 100644 index 0000000..2f015a6 --- /dev/null +++ b/pkg/receiptspec/testdata/v1/empty-chain.jsonl @@ -0,0 +1,2 @@ +{"type":"key","kid":"ed25519:56475aa75463474c","public_key_hex":"03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8","valid_from_seq":1,"valid_until_seq":2} +{"type":"key","kid":"ed25519:24f6ed6acbfe1009","public_key_hex":"29acbae141bccaf0b22e1a94d34d0bc7361e526d0bfe12c89794bc9322966dd7","valid_from_seq":3,"valid_until_seq":null} diff --git a/pkg/receiptspec/testdata/v1/encoding-lock.json b/pkg/receiptspec/testdata/v1/encoding-lock.json new file mode 100644 index 0000000..ef32eb3 --- /dev/null +++ b/pkg/receiptspec/testdata/v1/encoding-lock.json @@ -0,0 +1,42 @@ +{ + "valid-boundary": { + "receipts": [ + "393f5d597c088647185b507af322f38730372ae5f722581f98996a1d69c65ade" + ], + "manifest": "9f159bca2f5d724207008e5e997a4931b68d8fd4f3077358034f24dab74a50f5" + }, + "valid-chain": { + "receipts": [ + "b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138", + "b2a27707d518092a9fa6b069a0ef1907489ed66ad9ae3196d4bffafe3affcfe2", + "559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695" + ], + "manifest": "8e8fb7b9943680a29e9ed35e93ad243d59e2b6ddf3c95c003e2c678d99810155" + }, + "valid-genesis": { + "receipts": [ + "b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138" + ] + }, + "valid-partial": { + "receipts": [ + "b2a27707d518092a9fa6b069a0ef1907489ed66ad9ae3196d4bffafe3affcfe2", + "559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695" + ], + "manifest": "74a034547f23baccf6b021c8e44bbcc72bd16c032ae649393bbbbc6ef1c3dc40" + }, + "valid-unchecked-manifest-count": { + "receipts": [ + "b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138", + "b2a27707d518092a9fa6b069a0ef1907489ed66ad9ae3196d4bffafe3affcfe2", + "559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695" + ], + "manifest": "965801b7579b6d543114d768ea871ced95867776f73f612bdb3133855b9597e8" + }, + "valid-unpinned-prefix": { + "receipts": [ + "b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138", + "b2a27707d518092a9fa6b069a0ef1907489ed66ad9ae3196d4bffafe3affcfe2" + ] + } +} diff --git a/pkg/receiptspec/testdata/v1/example.json b/pkg/receiptspec/testdata/v1/example.json new file mode 100644 index 0000000..0c7a06f --- /dev/null +++ b/pkg/receiptspec/testdata/v1/example.json @@ -0,0 +1,9 @@ +{ + "seed_hex": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + "public_key_hex": "03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8", + "kid": "ed25519:56475aa75463474c", + "preimage_hex": "6167656e74676174652e726563656970742e686173682e7631000100000000000000010000000000000001000000680100000061000000000100000073010000007844136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a05000000616c6c6f7764000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018000000656432353531393a35363437356161373534363334373463", + "preimage_bytes": 183, + "entry_hash": "b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138", + "signature": "9b0b99df7d812a725ed1f810bbf913bf86b15de3152808cf97dc9ecdc45eb6e2f0481c80490d77d140cc48152c2d337871ebffa343dc69c5f18ee4d294ea1705" +} diff --git a/pkg/receiptspec/testdata/v1/gen/main.go b/pkg/receiptspec/testdata/v1/gen/main.go new file mode 100644 index 0000000..2ddd03c --- /dev/null +++ b/pkg/receiptspec/testdata/v1/gen/main.go @@ -0,0 +1,262 @@ +/* +Copyright 2026 Clawdlinux. +Licensed under the Apache License, Version 2.0. +*/ + +// Command gen writes deterministic v1 receipt fixtures. Keys are test-only. +package main + +import ( + "bytes" + "crypto/ed25519" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "flag" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/Clawdlinux/agentgate/pkg/receiptspec" +) + +type outcome struct { + Class string `json:"class"` + VerifiedCount int `json:"verified_count"` + FailedAtSeq uint64 `json:"failed_at_seq"` + Complete bool `json:"complete"` +} + +type vector struct { + Name string `json:"name"` + JSONL string `json:"jsonl"` + Keys string `json:"keys"` + Expected outcome `json:"expected"` +} + +type catalog struct { + FormatVersion int `json:"format_version"` + Vectors []vector `json:"vectors"` +} + +func main() { + out := flag.String("out", "", "output directory (required)") + flag.Parse() + if *out == "" { + fatal(fmt.Errorf("-out is required")) + } + if err := os.MkdirAll(*out, 0o755); err != nil { + fatal(err) + } + generate(*out) +} + +func generate(out string) { + var seedA, seedB [ed25519.SeedSize]byte + for i := range seedA { + seedA[i], seedB[i] = byte(i), byte(i+32) + } + privA := ed25519.NewKeyFromSeed(seedA[:]) + privB := ed25519.NewKeyFromSeed(seedB[:]) + signerA := receiptspec.NewEd25519Signer(privA) + signerB := receiptspec.NewEd25519Signer(privB) + until := uint64(2) + keys := []receiptspec.TrustedKey{ + {KID: signerA.KID(), PublicKey: privA.Public().(ed25519.PublicKey), ValidFromSeq: 1, ValidUntilSeq: &until}, + {KID: signerB.KID(), PublicKey: privB.Public().(ed25519.PublicKey), ValidFromSeq: 3}, + } + writeTrust(out, "public-keys.json", keys) + + base := receiptspec.Fields{ + TimestampUnixNS: 1, HumanPrincipal: "h", AgentKeyID: "a", + Service: "s", Action: "x", ParamsSHA256: sha256.Sum256([]byte("{}")), + PolicyDecision: receiptspec.DecisionAllow, StatusCode: 100, + } + chainA := receiptspec.NewChain(signerA, 0, [32]byte{}) + first, err := chainA.Next(base) + check(err) + secondFields := base + secondFields.TimestampUnixNS = 1_700_000_000_000_000_001 + secondFields.HumanPrincipal = "human-é/e\u0301/😀" + secondFields.AgentKeyID = "agent-2" + secondFields.Service, secondFields.Action = "operator", "decision:scale" + secondFields.PolicyDecision, secondFields.StatusCode = receiptspec.DecisionDeny, 403 + secondFields.Error, secondFields.LatencyMS = "scope_denied", 17 + for _, code := range []string{`check if true`, `check if "github" == "github"`} { + digest := sha256.Sum256([]byte(code)) + secondFields.DelegationChain = append(secondFields.DelegationChain, hex.EncodeToString(digest[:])) + } + secondFields.ParamsSHA256, err = receiptspec.HashRecord(json.RawMessage(`{"replicas":3,"note":"é/e\u0301/😀","confidence":"0.875"}`)) + check(err) + second, err := chainA.Next(secondFields) + check(err) + chainB := receiptspec.NewChain(signerB, second.Seq, second.EntryHash) + thirdFields := base + thirdFields.TimestampUnixNS = ^uint64(0) + thirdFields.LatencyMS = int64(^uint64(0) >> 1) + thirdFields.StatusCode, thirdFields.PolicyDecision = 599, receiptspec.DecisionRateLimited + thirdFields.Error = strings.Repeat("e", 64) + third, err := chainB.Next(thirdFields) + check(err) + receipts := []receiptspec.Receipt{first, second, third} + + manifest := func(rs []receiptspec.Receipt, anchor receiptspec.Anchor, head receiptspec.Receipt) *receiptspec.ExportManifest { + m := receiptspec.ExportManifest{ + FormatVersion: 1, RequestedFrom: rs[0].Seq, ResolvedTo: rs[len(rs)-1].Seq, + Count: len(rs), AnchorSeq: anchor.Seq, AnchorHash: anchor.EntryHash, + FirstEntryHash: rs[0].EntryHash, LastEntryHash: rs[len(rs)-1].EntryHash, + HeadSeq: head.Seq, HeadHash: head.EntryHash, KeysetDigest: receiptspec.ComputeKeysetDigest(keys), + } + m = receiptspec.SignManifest(m, signerB.KID(), privB) + return &m + } + full := manifest(receipts, receiptspec.Anchor{}, third) + c := catalog{FormatVersion: 1} + add := func(name string, rs []receiptspec.Receipt, embedded []receiptspec.TrustedKey, m *receiptspec.ExportManifest, trust string, want outcome) { + var buf bytes.Buffer + if m != nil { + line, err := receiptspec.MarshalManifestLine(*m) + check(err) + buf.Write(line) + buf.WriteByte('\n') + } + for _, key := range embedded { + line, err := receiptspec.MarshalKeyLine(key) + check(err) + buf.Write(line) + buf.WriteByte('\n') + } + check(receiptspec.WriteJSONL(&buf, rs)) + write(out, name+".jsonl", buf.Bytes()) + c.Vectors = append(c.Vectors, vector{Name: name, JSONL: name + ".jsonl", Keys: trust, Expected: want}) + } + addCommon := func(name string, rs []receiptspec.Receipt, m *receiptspec.ExportManifest, want outcome) { + add(name, rs, keys, m, "public-keys.json", want) + } + addCommon("valid-genesis", receipts[:1], nil, outcome{Class: "ok", VerifiedCount: 1}) + addCommon("valid-chain", receipts, full, outcome{Class: "ok", VerifiedCount: 3, Complete: true}) + addCommon("valid-partial", receipts[1:], manifest(receipts[1:], receiptspec.Anchor{Seq: 1, EntryHash: first.EntryHash}, third), + outcome{Class: "ok", VerifiedCount: 2, Complete: true}) + addCommon("valid-unpinned-prefix", receipts[:2], nil, outcome{Class: "ok", VerifiedCount: 2}) + + maxFields := thirdFields + maxFields.HumanPrincipal = strings.Repeat("é", 128) + maxFields.AgentKeyID, maxFields.Service, maxFields.Action = strings.Repeat("a", 128), strings.Repeat("s", 64), strings.Repeat("x", 128) + maxFields.DelegationChain = make([]string, 32) + for i := range maxFields.DelegationChain { + digest := sha256.Sum256([]byte(fmt.Sprintf("block-%d", i))) + maxFields.DelegationChain[i] = hex.EncodeToString(digest[:]) + } + maxAnchor := receiptspec.Anchor{Seq: ^uint64(0) - 1, EntryHash: sha256.Sum256([]byte("boundary anchor"))} + maxChain := receiptspec.NewChain(signerB, maxAnchor.Seq, maxAnchor.EntryHash) + maxReceipt, err := maxChain.Next(maxFields) + check(err) + maxReceipts := []receiptspec.Receipt{maxReceipt} + addCommon("valid-boundary", maxReceipts, manifest(maxReceipts, maxAnchor, maxReceipt), + outcome{Class: "ok", VerifiedCount: 1, Complete: true}) + + modified := func(change func(*receiptspec.Receipt)) []receiptspec.Receipt { + copyOfReceipts := append([]receiptspec.Receipt(nil), receipts...) + change(©OfReceipts[1]) + return copyOfReceipts + } + addCommon("tampered-body", modified(func(r *receiptspec.Receipt) { r.Action = "decision:delete" }), full, + outcome{Class: receiptspec.ReasonEntryHashMismatch, VerifiedCount: 1, FailedAtSeq: 2}) + addCommon("reordered-chain", []receiptspec.Receipt{second, first, third}, full, + outcome{Class: receiptspec.ReasonSequenceGap, FailedAtSeq: 2}) + addCommon("missing-middle", []receiptspec.Receipt{first, third}, full, + outcome{Class: receiptspec.ReasonSequenceGap, VerifiedCount: 1, FailedAtSeq: 3}) + addCommon("missing-tail-with-manifest", receipts[:2], full, + outcome{Class: receiptspec.ReasonExpectedHeadMismatch, VerifiedCount: 2, FailedAtSeq: 3}) + addCommon("wrong-key-signature", modified(func(r *receiptspec.Receipt) { + copy(r.Signature[:], ed25519.Sign(privB, r.EntryHash[:])) + }), full, outcome{Class: receiptspec.ReasonSignatureInvalid, VerifiedCount: 1, FailedAtSeq: 2}) + addCommon("bad-prev-hash", modified(func(r *receiptspec.Receipt) { r.PrevHash[0] ^= 1 }), full, + outcome{Class: receiptspec.ReasonPrevHashMismatch, VerifiedCount: 1, FailedAtSeq: 2}) + addCommon("invalid-fields", modified(func(r *receiptspec.Receipt) { r.StatusCode = 99 }), full, + outcome{Class: receiptspec.ReasonInvalidReceiptFields, VerifiedCount: 1, FailedAtSeq: 2}) + addCommon("unknown-signer", modified(func(r *receiptspec.Receipt) { r.SignerKID = "unknown" }), full, + outcome{Class: "ErrUnknownSignerKID"}) + addCommon("empty-chain", nil, nil, outcome{Class: "ErrEmptyChain"}) + + inactive := append([]receiptspec.TrustedKey(nil), keys...) + inactive[0].ValidFromSeq = 2 + writeTrust(out, "inactive-keys.json", inactive) + add("inactive-signer", receipts[:1], inactive, nil, "inactive-keys.json", + outcome{Class: receiptspec.ReasonSignerInactiveAtSeq, FailedAtSeq: 1}) + + badManifest := *full + badManifest.HeadHash[0] ^= 1 + addCommon("manifest-mismatch", receipts, &badManifest, outcome{Class: "ManifestError/ErrManifestSignatureInvalid"}) + add("manifest-keyset-mismatch", receipts, keys[1:], full, "public-keys.json", + outcome{Class: "ManifestError/ErrKeysetDigestMismatch"}) + badManifest = *full + badManifest.SignerKID = "unknown" + addCommon("manifest-unknown-signer", receipts, &badManifest, outcome{Class: "ManifestError/ErrUnknownSignerKID"}) + + badManifest = *full + badManifest.ResolvedTo++ + badManifest = receiptspec.SignManifest(badManifest, signerB.KID(), privB) + addCommon("manifest-endpoint-mismatch", receipts, &badManifest, + outcome{Class: receiptspec.ReasonExpectedHeadMismatch, VerifiedCount: 3, FailedAtSeq: 4}) + badManifest = *full + badManifest.Count = 99 + badManifest = receiptspec.SignManifest(badManifest, signerB.KID(), privB) + addCommon("valid-unchecked-manifest-count", receipts, &badManifest, + outcome{Class: "ok", VerifiedCount: 3, Complete: true}) + + preimage, err := receiptspec.CanonicalHashInput(first) + check(err) + writeJSON(out, "example.json", struct { + SeedHex string `json:"seed_hex"` + PublicKeyHex string `json:"public_key_hex"` + KID string `json:"kid"` + PreimageHex string `json:"preimage_hex"` + PreimageBytes int `json:"preimage_bytes"` + EntryHash string `json:"entry_hash"` + Signature string `json:"signature"` + }{ + hex.EncodeToString(seedA[:]), hex.EncodeToString(keys[0].PublicKey), signerA.KID(), + hex.EncodeToString(preimage), len(preimage), hex.EncodeToString(first.EntryHash[:]), hex.EncodeToString(first.Signature[:]), + }) + writeJSON(out, "vectors.json", c) +} + +func writeTrust(out, name string, keys []receiptspec.TrustedKey) { + var lines []json.RawMessage + for _, key := range keys { + line, err := receiptspec.MarshalKeyLine(key) + check(err) + var fields map[string]json.RawMessage + check(json.Unmarshal(line, &fields)) + delete(fields, "type") + line, err = json.Marshal(fields) + check(err) + lines = append(lines, line) + } + writeJSON(out, name, struct { + Keys []json.RawMessage `json:"keys"` + }{lines}) +} + +func writeJSON(out, name string, value any) { + data, err := json.MarshalIndent(value, "", " ") + check(err) + write(out, name, append(data, '\n')) +} + +func write(out, name string, contents []byte) { + check(os.WriteFile(filepath.Join(out, name), contents, 0o644)) +} + +func check(err error) { + if err != nil { + fatal(err) + } +} + +func fatal(err error) { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) +} diff --git a/pkg/receiptspec/testdata/v1/inactive-keys.json b/pkg/receiptspec/testdata/v1/inactive-keys.json new file mode 100644 index 0000000..15ee76f --- /dev/null +++ b/pkg/receiptspec/testdata/v1/inactive-keys.json @@ -0,0 +1,16 @@ +{ + "keys": [ + { + "kid": "ed25519:56475aa75463474c", + "public_key_hex": "03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8", + "valid_from_seq": 2, + "valid_until_seq": 2 + }, + { + "kid": "ed25519:24f6ed6acbfe1009", + "public_key_hex": "29acbae141bccaf0b22e1a94d34d0bc7361e526d0bfe12c89794bc9322966dd7", + "valid_from_seq": 3, + "valid_until_seq": null + } + ] +} diff --git a/pkg/receiptspec/testdata/v1/inactive-signer.jsonl b/pkg/receiptspec/testdata/v1/inactive-signer.jsonl new file mode 100644 index 0000000..1795280 --- /dev/null +++ b/pkg/receiptspec/testdata/v1/inactive-signer.jsonl @@ -0,0 +1,3 @@ +{"type":"key","kid":"ed25519:56475aa75463474c","public_key_hex":"03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8","valid_from_seq":2,"valid_until_seq":2} +{"type":"key","kid":"ed25519:24f6ed6acbfe1009","public_key_hex":"29acbae141bccaf0b22e1a94d34d0bc7361e526d0bfe12c89794bc9322966dd7","valid_from_seq":3,"valid_until_seq":null} +{"type":"receipt","format_version":1,"seq":1,"timestamp_unix_ns":1,"human_principal":"h","agent_key_id":"a","delegation_chain":null,"service":"s","action":"x","params_sha256":"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","policy_decision":"allow","status_code":100,"latency_ms":0,"error":"","prev_hash":"0000000000000000000000000000000000000000000000000000000000000000","entry_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","signer_kid":"ed25519:56475aa75463474c","signature":"9b0b99df7d812a725ed1f810bbf913bf86b15de3152808cf97dc9ecdc45eb6e2f0481c80490d77d140cc48152c2d337871ebffa343dc69c5f18ee4d294ea1705"} diff --git a/pkg/receiptspec/testdata/v1/invalid-fields.jsonl b/pkg/receiptspec/testdata/v1/invalid-fields.jsonl new file mode 100644 index 0000000..fd75b1e --- /dev/null +++ b/pkg/receiptspec/testdata/v1/invalid-fields.jsonl @@ -0,0 +1,6 @@ +{"type":"manifest","format_version":1,"requested_from":1,"resolved_to":3,"count":3,"anchor_seq":0,"anchor_hash":"0000000000000000000000000000000000000000000000000000000000000000","first_entry_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","last_entry_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","head_seq":3,"head_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","keyset_digest":"9877fcf6988d2245cabc51a00b26fa546ef0c9209e17a5fd66c6cda80fe075eb","signer_kid":"ed25519:24f6ed6acbfe1009","signature":"38a522420dd49b670936d9c003c596bbec6c7ea036856e1e6430f320da92de8225b45cd43af5b863e6b6889620bebbc42b8cb450660a7e2136654cd61c23a20e"} +{"type":"key","kid":"ed25519:56475aa75463474c","public_key_hex":"03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8","valid_from_seq":1,"valid_until_seq":2} +{"type":"key","kid":"ed25519:24f6ed6acbfe1009","public_key_hex":"29acbae141bccaf0b22e1a94d34d0bc7361e526d0bfe12c89794bc9322966dd7","valid_from_seq":3,"valid_until_seq":null} +{"type":"receipt","format_version":1,"seq":1,"timestamp_unix_ns":1,"human_principal":"h","agent_key_id":"a","delegation_chain":null,"service":"s","action":"x","params_sha256":"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","policy_decision":"allow","status_code":100,"latency_ms":0,"error":"","prev_hash":"0000000000000000000000000000000000000000000000000000000000000000","entry_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","signer_kid":"ed25519:56475aa75463474c","signature":"9b0b99df7d812a725ed1f810bbf913bf86b15de3152808cf97dc9ecdc45eb6e2f0481c80490d77d140cc48152c2d337871ebffa343dc69c5f18ee4d294ea1705"} +{"type":"receipt","format_version":1,"seq":2,"timestamp_unix_ns":1700000000000000001,"human_principal":"human-é/é/😀","agent_key_id":"agent-2","delegation_chain":["d2d8e9aea4f592e1ca94318065976dbe9147e57eaf6eb56e14ca9ae6004c6bae","482d488bd94eee647aa10471442b576f78c4646767b16616929cdef236d9eeb6"],"service":"operator","action":"decision:scale","params_sha256":"d110a9af75f1fb6bdecaf9590b2fa2b99df60f007e5ed4b37c72d57edbf11186","policy_decision":"deny","status_code":99,"latency_ms":17,"error":"scope_denied","prev_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","entry_hash":"b2a27707d518092a9fa6b069a0ef1907489ed66ad9ae3196d4bffafe3affcfe2","signer_kid":"ed25519:56475aa75463474c","signature":"d070daaf2edf8115f713d5749e4d8894ed4dfd3191d74d0d0f1d13082247bdb54055fe1dd818607edd9f8b26f86f4eed16cbcac784f509c68b1dcf3f672fe60e"} +{"type":"receipt","format_version":1,"seq":3,"timestamp_unix_ns":18446744073709551615,"human_principal":"h","agent_key_id":"a","delegation_chain":null,"service":"s","action":"x","params_sha256":"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","policy_decision":"rate_limited","status_code":599,"latency_ms":9223372036854775807,"error":"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","prev_hash":"b2a27707d518092a9fa6b069a0ef1907489ed66ad9ae3196d4bffafe3affcfe2","entry_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","signer_kid":"ed25519:24f6ed6acbfe1009","signature":"628208d560ad848a6635343e2d0c0ab1b8f2bc07f2e6ff69c8f9f8bb11a54f5a70f8a26e42dafb797146ac252f6df58fa2d976d306fd17a9ce336bf84692f600"} diff --git a/pkg/receiptspec/testdata/v1/manifest-endpoint-mismatch.jsonl b/pkg/receiptspec/testdata/v1/manifest-endpoint-mismatch.jsonl new file mode 100644 index 0000000..ad6f930 --- /dev/null +++ b/pkg/receiptspec/testdata/v1/manifest-endpoint-mismatch.jsonl @@ -0,0 +1,6 @@ +{"type":"manifest","format_version":1,"requested_from":1,"resolved_to":4,"count":3,"anchor_seq":0,"anchor_hash":"0000000000000000000000000000000000000000000000000000000000000000","first_entry_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","last_entry_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","head_seq":3,"head_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","keyset_digest":"9877fcf6988d2245cabc51a00b26fa546ef0c9209e17a5fd66c6cda80fe075eb","signer_kid":"ed25519:24f6ed6acbfe1009","signature":"e72add0974982d05fdaf034b1004d98da7edd7ad155cdc707adf2577a59c93383c1a8eda11a286f92267fb389e57264530d589fdfb1c141dc6efe4e216b02707"} +{"type":"key","kid":"ed25519:56475aa75463474c","public_key_hex":"03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8","valid_from_seq":1,"valid_until_seq":2} +{"type":"key","kid":"ed25519:24f6ed6acbfe1009","public_key_hex":"29acbae141bccaf0b22e1a94d34d0bc7361e526d0bfe12c89794bc9322966dd7","valid_from_seq":3,"valid_until_seq":null} +{"type":"receipt","format_version":1,"seq":1,"timestamp_unix_ns":1,"human_principal":"h","agent_key_id":"a","delegation_chain":null,"service":"s","action":"x","params_sha256":"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","policy_decision":"allow","status_code":100,"latency_ms":0,"error":"","prev_hash":"0000000000000000000000000000000000000000000000000000000000000000","entry_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","signer_kid":"ed25519:56475aa75463474c","signature":"9b0b99df7d812a725ed1f810bbf913bf86b15de3152808cf97dc9ecdc45eb6e2f0481c80490d77d140cc48152c2d337871ebffa343dc69c5f18ee4d294ea1705"} +{"type":"receipt","format_version":1,"seq":2,"timestamp_unix_ns":1700000000000000001,"human_principal":"human-é/é/😀","agent_key_id":"agent-2","delegation_chain":["d2d8e9aea4f592e1ca94318065976dbe9147e57eaf6eb56e14ca9ae6004c6bae","482d488bd94eee647aa10471442b576f78c4646767b16616929cdef236d9eeb6"],"service":"operator","action":"decision:scale","params_sha256":"d110a9af75f1fb6bdecaf9590b2fa2b99df60f007e5ed4b37c72d57edbf11186","policy_decision":"deny","status_code":403,"latency_ms":17,"error":"scope_denied","prev_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","entry_hash":"b2a27707d518092a9fa6b069a0ef1907489ed66ad9ae3196d4bffafe3affcfe2","signer_kid":"ed25519:56475aa75463474c","signature":"d070daaf2edf8115f713d5749e4d8894ed4dfd3191d74d0d0f1d13082247bdb54055fe1dd818607edd9f8b26f86f4eed16cbcac784f509c68b1dcf3f672fe60e"} +{"type":"receipt","format_version":1,"seq":3,"timestamp_unix_ns":18446744073709551615,"human_principal":"h","agent_key_id":"a","delegation_chain":null,"service":"s","action":"x","params_sha256":"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","policy_decision":"rate_limited","status_code":599,"latency_ms":9223372036854775807,"error":"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","prev_hash":"b2a27707d518092a9fa6b069a0ef1907489ed66ad9ae3196d4bffafe3affcfe2","entry_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","signer_kid":"ed25519:24f6ed6acbfe1009","signature":"628208d560ad848a6635343e2d0c0ab1b8f2bc07f2e6ff69c8f9f8bb11a54f5a70f8a26e42dafb797146ac252f6df58fa2d976d306fd17a9ce336bf84692f600"} diff --git a/pkg/receiptspec/testdata/v1/manifest-keyset-mismatch.jsonl b/pkg/receiptspec/testdata/v1/manifest-keyset-mismatch.jsonl new file mode 100644 index 0000000..03c8728 --- /dev/null +++ b/pkg/receiptspec/testdata/v1/manifest-keyset-mismatch.jsonl @@ -0,0 +1,5 @@ +{"type":"manifest","format_version":1,"requested_from":1,"resolved_to":3,"count":3,"anchor_seq":0,"anchor_hash":"0000000000000000000000000000000000000000000000000000000000000000","first_entry_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","last_entry_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","head_seq":3,"head_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","keyset_digest":"9877fcf6988d2245cabc51a00b26fa546ef0c9209e17a5fd66c6cda80fe075eb","signer_kid":"ed25519:24f6ed6acbfe1009","signature":"38a522420dd49b670936d9c003c596bbec6c7ea036856e1e6430f320da92de8225b45cd43af5b863e6b6889620bebbc42b8cb450660a7e2136654cd61c23a20e"} +{"type":"key","kid":"ed25519:24f6ed6acbfe1009","public_key_hex":"29acbae141bccaf0b22e1a94d34d0bc7361e526d0bfe12c89794bc9322966dd7","valid_from_seq":3,"valid_until_seq":null} +{"type":"receipt","format_version":1,"seq":1,"timestamp_unix_ns":1,"human_principal":"h","agent_key_id":"a","delegation_chain":null,"service":"s","action":"x","params_sha256":"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","policy_decision":"allow","status_code":100,"latency_ms":0,"error":"","prev_hash":"0000000000000000000000000000000000000000000000000000000000000000","entry_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","signer_kid":"ed25519:56475aa75463474c","signature":"9b0b99df7d812a725ed1f810bbf913bf86b15de3152808cf97dc9ecdc45eb6e2f0481c80490d77d140cc48152c2d337871ebffa343dc69c5f18ee4d294ea1705"} +{"type":"receipt","format_version":1,"seq":2,"timestamp_unix_ns":1700000000000000001,"human_principal":"human-é/é/😀","agent_key_id":"agent-2","delegation_chain":["d2d8e9aea4f592e1ca94318065976dbe9147e57eaf6eb56e14ca9ae6004c6bae","482d488bd94eee647aa10471442b576f78c4646767b16616929cdef236d9eeb6"],"service":"operator","action":"decision:scale","params_sha256":"d110a9af75f1fb6bdecaf9590b2fa2b99df60f007e5ed4b37c72d57edbf11186","policy_decision":"deny","status_code":403,"latency_ms":17,"error":"scope_denied","prev_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","entry_hash":"b2a27707d518092a9fa6b069a0ef1907489ed66ad9ae3196d4bffafe3affcfe2","signer_kid":"ed25519:56475aa75463474c","signature":"d070daaf2edf8115f713d5749e4d8894ed4dfd3191d74d0d0f1d13082247bdb54055fe1dd818607edd9f8b26f86f4eed16cbcac784f509c68b1dcf3f672fe60e"} +{"type":"receipt","format_version":1,"seq":3,"timestamp_unix_ns":18446744073709551615,"human_principal":"h","agent_key_id":"a","delegation_chain":null,"service":"s","action":"x","params_sha256":"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","policy_decision":"rate_limited","status_code":599,"latency_ms":9223372036854775807,"error":"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","prev_hash":"b2a27707d518092a9fa6b069a0ef1907489ed66ad9ae3196d4bffafe3affcfe2","entry_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","signer_kid":"ed25519:24f6ed6acbfe1009","signature":"628208d560ad848a6635343e2d0c0ab1b8f2bc07f2e6ff69c8f9f8bb11a54f5a70f8a26e42dafb797146ac252f6df58fa2d976d306fd17a9ce336bf84692f600"} diff --git a/pkg/receiptspec/testdata/v1/manifest-mismatch.jsonl b/pkg/receiptspec/testdata/v1/manifest-mismatch.jsonl new file mode 100644 index 0000000..5c99638 --- /dev/null +++ b/pkg/receiptspec/testdata/v1/manifest-mismatch.jsonl @@ -0,0 +1,6 @@ +{"type":"manifest","format_version":1,"requested_from":1,"resolved_to":3,"count":3,"anchor_seq":0,"anchor_hash":"0000000000000000000000000000000000000000000000000000000000000000","first_entry_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","last_entry_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","head_seq":3,"head_hash":"549b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","keyset_digest":"9877fcf6988d2245cabc51a00b26fa546ef0c9209e17a5fd66c6cda80fe075eb","signer_kid":"ed25519:24f6ed6acbfe1009","signature":"38a522420dd49b670936d9c003c596bbec6c7ea036856e1e6430f320da92de8225b45cd43af5b863e6b6889620bebbc42b8cb450660a7e2136654cd61c23a20e"} +{"type":"key","kid":"ed25519:56475aa75463474c","public_key_hex":"03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8","valid_from_seq":1,"valid_until_seq":2} +{"type":"key","kid":"ed25519:24f6ed6acbfe1009","public_key_hex":"29acbae141bccaf0b22e1a94d34d0bc7361e526d0bfe12c89794bc9322966dd7","valid_from_seq":3,"valid_until_seq":null} +{"type":"receipt","format_version":1,"seq":1,"timestamp_unix_ns":1,"human_principal":"h","agent_key_id":"a","delegation_chain":null,"service":"s","action":"x","params_sha256":"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","policy_decision":"allow","status_code":100,"latency_ms":0,"error":"","prev_hash":"0000000000000000000000000000000000000000000000000000000000000000","entry_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","signer_kid":"ed25519:56475aa75463474c","signature":"9b0b99df7d812a725ed1f810bbf913bf86b15de3152808cf97dc9ecdc45eb6e2f0481c80490d77d140cc48152c2d337871ebffa343dc69c5f18ee4d294ea1705"} +{"type":"receipt","format_version":1,"seq":2,"timestamp_unix_ns":1700000000000000001,"human_principal":"human-é/é/😀","agent_key_id":"agent-2","delegation_chain":["d2d8e9aea4f592e1ca94318065976dbe9147e57eaf6eb56e14ca9ae6004c6bae","482d488bd94eee647aa10471442b576f78c4646767b16616929cdef236d9eeb6"],"service":"operator","action":"decision:scale","params_sha256":"d110a9af75f1fb6bdecaf9590b2fa2b99df60f007e5ed4b37c72d57edbf11186","policy_decision":"deny","status_code":403,"latency_ms":17,"error":"scope_denied","prev_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","entry_hash":"b2a27707d518092a9fa6b069a0ef1907489ed66ad9ae3196d4bffafe3affcfe2","signer_kid":"ed25519:56475aa75463474c","signature":"d070daaf2edf8115f713d5749e4d8894ed4dfd3191d74d0d0f1d13082247bdb54055fe1dd818607edd9f8b26f86f4eed16cbcac784f509c68b1dcf3f672fe60e"} +{"type":"receipt","format_version":1,"seq":3,"timestamp_unix_ns":18446744073709551615,"human_principal":"h","agent_key_id":"a","delegation_chain":null,"service":"s","action":"x","params_sha256":"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","policy_decision":"rate_limited","status_code":599,"latency_ms":9223372036854775807,"error":"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","prev_hash":"b2a27707d518092a9fa6b069a0ef1907489ed66ad9ae3196d4bffafe3affcfe2","entry_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","signer_kid":"ed25519:24f6ed6acbfe1009","signature":"628208d560ad848a6635343e2d0c0ab1b8f2bc07f2e6ff69c8f9f8bb11a54f5a70f8a26e42dafb797146ac252f6df58fa2d976d306fd17a9ce336bf84692f600"} diff --git a/pkg/receiptspec/testdata/v1/manifest-unknown-signer.jsonl b/pkg/receiptspec/testdata/v1/manifest-unknown-signer.jsonl new file mode 100644 index 0000000..1470a14 --- /dev/null +++ b/pkg/receiptspec/testdata/v1/manifest-unknown-signer.jsonl @@ -0,0 +1,6 @@ +{"type":"manifest","format_version":1,"requested_from":1,"resolved_to":3,"count":3,"anchor_seq":0,"anchor_hash":"0000000000000000000000000000000000000000000000000000000000000000","first_entry_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","last_entry_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","head_seq":3,"head_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","keyset_digest":"9877fcf6988d2245cabc51a00b26fa546ef0c9209e17a5fd66c6cda80fe075eb","signer_kid":"unknown","signature":"38a522420dd49b670936d9c003c596bbec6c7ea036856e1e6430f320da92de8225b45cd43af5b863e6b6889620bebbc42b8cb450660a7e2136654cd61c23a20e"} +{"type":"key","kid":"ed25519:56475aa75463474c","public_key_hex":"03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8","valid_from_seq":1,"valid_until_seq":2} +{"type":"key","kid":"ed25519:24f6ed6acbfe1009","public_key_hex":"29acbae141bccaf0b22e1a94d34d0bc7361e526d0bfe12c89794bc9322966dd7","valid_from_seq":3,"valid_until_seq":null} +{"type":"receipt","format_version":1,"seq":1,"timestamp_unix_ns":1,"human_principal":"h","agent_key_id":"a","delegation_chain":null,"service":"s","action":"x","params_sha256":"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","policy_decision":"allow","status_code":100,"latency_ms":0,"error":"","prev_hash":"0000000000000000000000000000000000000000000000000000000000000000","entry_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","signer_kid":"ed25519:56475aa75463474c","signature":"9b0b99df7d812a725ed1f810bbf913bf86b15de3152808cf97dc9ecdc45eb6e2f0481c80490d77d140cc48152c2d337871ebffa343dc69c5f18ee4d294ea1705"} +{"type":"receipt","format_version":1,"seq":2,"timestamp_unix_ns":1700000000000000001,"human_principal":"human-é/é/😀","agent_key_id":"agent-2","delegation_chain":["d2d8e9aea4f592e1ca94318065976dbe9147e57eaf6eb56e14ca9ae6004c6bae","482d488bd94eee647aa10471442b576f78c4646767b16616929cdef236d9eeb6"],"service":"operator","action":"decision:scale","params_sha256":"d110a9af75f1fb6bdecaf9590b2fa2b99df60f007e5ed4b37c72d57edbf11186","policy_decision":"deny","status_code":403,"latency_ms":17,"error":"scope_denied","prev_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","entry_hash":"b2a27707d518092a9fa6b069a0ef1907489ed66ad9ae3196d4bffafe3affcfe2","signer_kid":"ed25519:56475aa75463474c","signature":"d070daaf2edf8115f713d5749e4d8894ed4dfd3191d74d0d0f1d13082247bdb54055fe1dd818607edd9f8b26f86f4eed16cbcac784f509c68b1dcf3f672fe60e"} +{"type":"receipt","format_version":1,"seq":3,"timestamp_unix_ns":18446744073709551615,"human_principal":"h","agent_key_id":"a","delegation_chain":null,"service":"s","action":"x","params_sha256":"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","policy_decision":"rate_limited","status_code":599,"latency_ms":9223372036854775807,"error":"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","prev_hash":"b2a27707d518092a9fa6b069a0ef1907489ed66ad9ae3196d4bffafe3affcfe2","entry_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","signer_kid":"ed25519:24f6ed6acbfe1009","signature":"628208d560ad848a6635343e2d0c0ab1b8f2bc07f2e6ff69c8f9f8bb11a54f5a70f8a26e42dafb797146ac252f6df58fa2d976d306fd17a9ce336bf84692f600"} diff --git a/pkg/receiptspec/testdata/v1/missing-middle.jsonl b/pkg/receiptspec/testdata/v1/missing-middle.jsonl new file mode 100644 index 0000000..b5d1b6c --- /dev/null +++ b/pkg/receiptspec/testdata/v1/missing-middle.jsonl @@ -0,0 +1,5 @@ +{"type":"manifest","format_version":1,"requested_from":1,"resolved_to":3,"count":3,"anchor_seq":0,"anchor_hash":"0000000000000000000000000000000000000000000000000000000000000000","first_entry_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","last_entry_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","head_seq":3,"head_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","keyset_digest":"9877fcf6988d2245cabc51a00b26fa546ef0c9209e17a5fd66c6cda80fe075eb","signer_kid":"ed25519:24f6ed6acbfe1009","signature":"38a522420dd49b670936d9c003c596bbec6c7ea036856e1e6430f320da92de8225b45cd43af5b863e6b6889620bebbc42b8cb450660a7e2136654cd61c23a20e"} +{"type":"key","kid":"ed25519:56475aa75463474c","public_key_hex":"03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8","valid_from_seq":1,"valid_until_seq":2} +{"type":"key","kid":"ed25519:24f6ed6acbfe1009","public_key_hex":"29acbae141bccaf0b22e1a94d34d0bc7361e526d0bfe12c89794bc9322966dd7","valid_from_seq":3,"valid_until_seq":null} +{"type":"receipt","format_version":1,"seq":1,"timestamp_unix_ns":1,"human_principal":"h","agent_key_id":"a","delegation_chain":null,"service":"s","action":"x","params_sha256":"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","policy_decision":"allow","status_code":100,"latency_ms":0,"error":"","prev_hash":"0000000000000000000000000000000000000000000000000000000000000000","entry_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","signer_kid":"ed25519:56475aa75463474c","signature":"9b0b99df7d812a725ed1f810bbf913bf86b15de3152808cf97dc9ecdc45eb6e2f0481c80490d77d140cc48152c2d337871ebffa343dc69c5f18ee4d294ea1705"} +{"type":"receipt","format_version":1,"seq":3,"timestamp_unix_ns":18446744073709551615,"human_principal":"h","agent_key_id":"a","delegation_chain":null,"service":"s","action":"x","params_sha256":"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","policy_decision":"rate_limited","status_code":599,"latency_ms":9223372036854775807,"error":"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","prev_hash":"b2a27707d518092a9fa6b069a0ef1907489ed66ad9ae3196d4bffafe3affcfe2","entry_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","signer_kid":"ed25519:24f6ed6acbfe1009","signature":"628208d560ad848a6635343e2d0c0ab1b8f2bc07f2e6ff69c8f9f8bb11a54f5a70f8a26e42dafb797146ac252f6df58fa2d976d306fd17a9ce336bf84692f600"} diff --git a/pkg/receiptspec/testdata/v1/missing-tail-with-manifest.jsonl b/pkg/receiptspec/testdata/v1/missing-tail-with-manifest.jsonl new file mode 100644 index 0000000..4ea022c --- /dev/null +++ b/pkg/receiptspec/testdata/v1/missing-tail-with-manifest.jsonl @@ -0,0 +1,5 @@ +{"type":"manifest","format_version":1,"requested_from":1,"resolved_to":3,"count":3,"anchor_seq":0,"anchor_hash":"0000000000000000000000000000000000000000000000000000000000000000","first_entry_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","last_entry_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","head_seq":3,"head_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","keyset_digest":"9877fcf6988d2245cabc51a00b26fa546ef0c9209e17a5fd66c6cda80fe075eb","signer_kid":"ed25519:24f6ed6acbfe1009","signature":"38a522420dd49b670936d9c003c596bbec6c7ea036856e1e6430f320da92de8225b45cd43af5b863e6b6889620bebbc42b8cb450660a7e2136654cd61c23a20e"} +{"type":"key","kid":"ed25519:56475aa75463474c","public_key_hex":"03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8","valid_from_seq":1,"valid_until_seq":2} +{"type":"key","kid":"ed25519:24f6ed6acbfe1009","public_key_hex":"29acbae141bccaf0b22e1a94d34d0bc7361e526d0bfe12c89794bc9322966dd7","valid_from_seq":3,"valid_until_seq":null} +{"type":"receipt","format_version":1,"seq":1,"timestamp_unix_ns":1,"human_principal":"h","agent_key_id":"a","delegation_chain":null,"service":"s","action":"x","params_sha256":"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","policy_decision":"allow","status_code":100,"latency_ms":0,"error":"","prev_hash":"0000000000000000000000000000000000000000000000000000000000000000","entry_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","signer_kid":"ed25519:56475aa75463474c","signature":"9b0b99df7d812a725ed1f810bbf913bf86b15de3152808cf97dc9ecdc45eb6e2f0481c80490d77d140cc48152c2d337871ebffa343dc69c5f18ee4d294ea1705"} +{"type":"receipt","format_version":1,"seq":2,"timestamp_unix_ns":1700000000000000001,"human_principal":"human-é/é/😀","agent_key_id":"agent-2","delegation_chain":["d2d8e9aea4f592e1ca94318065976dbe9147e57eaf6eb56e14ca9ae6004c6bae","482d488bd94eee647aa10471442b576f78c4646767b16616929cdef236d9eeb6"],"service":"operator","action":"decision:scale","params_sha256":"d110a9af75f1fb6bdecaf9590b2fa2b99df60f007e5ed4b37c72d57edbf11186","policy_decision":"deny","status_code":403,"latency_ms":17,"error":"scope_denied","prev_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","entry_hash":"b2a27707d518092a9fa6b069a0ef1907489ed66ad9ae3196d4bffafe3affcfe2","signer_kid":"ed25519:56475aa75463474c","signature":"d070daaf2edf8115f713d5749e4d8894ed4dfd3191d74d0d0f1d13082247bdb54055fe1dd818607edd9f8b26f86f4eed16cbcac784f509c68b1dcf3f672fe60e"} diff --git a/pkg/receiptspec/testdata/v1/public-keys.json b/pkg/receiptspec/testdata/v1/public-keys.json new file mode 100644 index 0000000..2e5678b --- /dev/null +++ b/pkg/receiptspec/testdata/v1/public-keys.json @@ -0,0 +1,16 @@ +{ + "keys": [ + { + "kid": "ed25519:56475aa75463474c", + "public_key_hex": "03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8", + "valid_from_seq": 1, + "valid_until_seq": 2 + }, + { + "kid": "ed25519:24f6ed6acbfe1009", + "public_key_hex": "29acbae141bccaf0b22e1a94d34d0bc7361e526d0bfe12c89794bc9322966dd7", + "valid_from_seq": 3, + "valid_until_seq": null + } + ] +} diff --git a/pkg/receiptspec/testdata/v1/reordered-chain.jsonl b/pkg/receiptspec/testdata/v1/reordered-chain.jsonl new file mode 100644 index 0000000..fc7d14c --- /dev/null +++ b/pkg/receiptspec/testdata/v1/reordered-chain.jsonl @@ -0,0 +1,6 @@ +{"type":"manifest","format_version":1,"requested_from":1,"resolved_to":3,"count":3,"anchor_seq":0,"anchor_hash":"0000000000000000000000000000000000000000000000000000000000000000","first_entry_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","last_entry_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","head_seq":3,"head_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","keyset_digest":"9877fcf6988d2245cabc51a00b26fa546ef0c9209e17a5fd66c6cda80fe075eb","signer_kid":"ed25519:24f6ed6acbfe1009","signature":"38a522420dd49b670936d9c003c596bbec6c7ea036856e1e6430f320da92de8225b45cd43af5b863e6b6889620bebbc42b8cb450660a7e2136654cd61c23a20e"} +{"type":"key","kid":"ed25519:56475aa75463474c","public_key_hex":"03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8","valid_from_seq":1,"valid_until_seq":2} +{"type":"key","kid":"ed25519:24f6ed6acbfe1009","public_key_hex":"29acbae141bccaf0b22e1a94d34d0bc7361e526d0bfe12c89794bc9322966dd7","valid_from_seq":3,"valid_until_seq":null} +{"type":"receipt","format_version":1,"seq":2,"timestamp_unix_ns":1700000000000000001,"human_principal":"human-é/é/😀","agent_key_id":"agent-2","delegation_chain":["d2d8e9aea4f592e1ca94318065976dbe9147e57eaf6eb56e14ca9ae6004c6bae","482d488bd94eee647aa10471442b576f78c4646767b16616929cdef236d9eeb6"],"service":"operator","action":"decision:scale","params_sha256":"d110a9af75f1fb6bdecaf9590b2fa2b99df60f007e5ed4b37c72d57edbf11186","policy_decision":"deny","status_code":403,"latency_ms":17,"error":"scope_denied","prev_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","entry_hash":"b2a27707d518092a9fa6b069a0ef1907489ed66ad9ae3196d4bffafe3affcfe2","signer_kid":"ed25519:56475aa75463474c","signature":"d070daaf2edf8115f713d5749e4d8894ed4dfd3191d74d0d0f1d13082247bdb54055fe1dd818607edd9f8b26f86f4eed16cbcac784f509c68b1dcf3f672fe60e"} +{"type":"receipt","format_version":1,"seq":1,"timestamp_unix_ns":1,"human_principal":"h","agent_key_id":"a","delegation_chain":null,"service":"s","action":"x","params_sha256":"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","policy_decision":"allow","status_code":100,"latency_ms":0,"error":"","prev_hash":"0000000000000000000000000000000000000000000000000000000000000000","entry_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","signer_kid":"ed25519:56475aa75463474c","signature":"9b0b99df7d812a725ed1f810bbf913bf86b15de3152808cf97dc9ecdc45eb6e2f0481c80490d77d140cc48152c2d337871ebffa343dc69c5f18ee4d294ea1705"} +{"type":"receipt","format_version":1,"seq":3,"timestamp_unix_ns":18446744073709551615,"human_principal":"h","agent_key_id":"a","delegation_chain":null,"service":"s","action":"x","params_sha256":"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","policy_decision":"rate_limited","status_code":599,"latency_ms":9223372036854775807,"error":"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","prev_hash":"b2a27707d518092a9fa6b069a0ef1907489ed66ad9ae3196d4bffafe3affcfe2","entry_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","signer_kid":"ed25519:24f6ed6acbfe1009","signature":"628208d560ad848a6635343e2d0c0ab1b8f2bc07f2e6ff69c8f9f8bb11a54f5a70f8a26e42dafb797146ac252f6df58fa2d976d306fd17a9ce336bf84692f600"} diff --git a/pkg/receiptspec/testdata/v1/tampered-body.jsonl b/pkg/receiptspec/testdata/v1/tampered-body.jsonl new file mode 100644 index 0000000..7e13198 --- /dev/null +++ b/pkg/receiptspec/testdata/v1/tampered-body.jsonl @@ -0,0 +1,6 @@ +{"type":"manifest","format_version":1,"requested_from":1,"resolved_to":3,"count":3,"anchor_seq":0,"anchor_hash":"0000000000000000000000000000000000000000000000000000000000000000","first_entry_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","last_entry_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","head_seq":3,"head_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","keyset_digest":"9877fcf6988d2245cabc51a00b26fa546ef0c9209e17a5fd66c6cda80fe075eb","signer_kid":"ed25519:24f6ed6acbfe1009","signature":"38a522420dd49b670936d9c003c596bbec6c7ea036856e1e6430f320da92de8225b45cd43af5b863e6b6889620bebbc42b8cb450660a7e2136654cd61c23a20e"} +{"type":"key","kid":"ed25519:56475aa75463474c","public_key_hex":"03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8","valid_from_seq":1,"valid_until_seq":2} +{"type":"key","kid":"ed25519:24f6ed6acbfe1009","public_key_hex":"29acbae141bccaf0b22e1a94d34d0bc7361e526d0bfe12c89794bc9322966dd7","valid_from_seq":3,"valid_until_seq":null} +{"type":"receipt","format_version":1,"seq":1,"timestamp_unix_ns":1,"human_principal":"h","agent_key_id":"a","delegation_chain":null,"service":"s","action":"x","params_sha256":"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","policy_decision":"allow","status_code":100,"latency_ms":0,"error":"","prev_hash":"0000000000000000000000000000000000000000000000000000000000000000","entry_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","signer_kid":"ed25519:56475aa75463474c","signature":"9b0b99df7d812a725ed1f810bbf913bf86b15de3152808cf97dc9ecdc45eb6e2f0481c80490d77d140cc48152c2d337871ebffa343dc69c5f18ee4d294ea1705"} +{"type":"receipt","format_version":1,"seq":2,"timestamp_unix_ns":1700000000000000001,"human_principal":"human-é/é/😀","agent_key_id":"agent-2","delegation_chain":["d2d8e9aea4f592e1ca94318065976dbe9147e57eaf6eb56e14ca9ae6004c6bae","482d488bd94eee647aa10471442b576f78c4646767b16616929cdef236d9eeb6"],"service":"operator","action":"decision:delete","params_sha256":"d110a9af75f1fb6bdecaf9590b2fa2b99df60f007e5ed4b37c72d57edbf11186","policy_decision":"deny","status_code":403,"latency_ms":17,"error":"scope_denied","prev_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","entry_hash":"b2a27707d518092a9fa6b069a0ef1907489ed66ad9ae3196d4bffafe3affcfe2","signer_kid":"ed25519:56475aa75463474c","signature":"d070daaf2edf8115f713d5749e4d8894ed4dfd3191d74d0d0f1d13082247bdb54055fe1dd818607edd9f8b26f86f4eed16cbcac784f509c68b1dcf3f672fe60e"} +{"type":"receipt","format_version":1,"seq":3,"timestamp_unix_ns":18446744073709551615,"human_principal":"h","agent_key_id":"a","delegation_chain":null,"service":"s","action":"x","params_sha256":"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","policy_decision":"rate_limited","status_code":599,"latency_ms":9223372036854775807,"error":"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","prev_hash":"b2a27707d518092a9fa6b069a0ef1907489ed66ad9ae3196d4bffafe3affcfe2","entry_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","signer_kid":"ed25519:24f6ed6acbfe1009","signature":"628208d560ad848a6635343e2d0c0ab1b8f2bc07f2e6ff69c8f9f8bb11a54f5a70f8a26e42dafb797146ac252f6df58fa2d976d306fd17a9ce336bf84692f600"} diff --git a/pkg/receiptspec/testdata/v1/unknown-signer.jsonl b/pkg/receiptspec/testdata/v1/unknown-signer.jsonl new file mode 100644 index 0000000..41c3e6e --- /dev/null +++ b/pkg/receiptspec/testdata/v1/unknown-signer.jsonl @@ -0,0 +1,6 @@ +{"type":"manifest","format_version":1,"requested_from":1,"resolved_to":3,"count":3,"anchor_seq":0,"anchor_hash":"0000000000000000000000000000000000000000000000000000000000000000","first_entry_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","last_entry_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","head_seq":3,"head_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","keyset_digest":"9877fcf6988d2245cabc51a00b26fa546ef0c9209e17a5fd66c6cda80fe075eb","signer_kid":"ed25519:24f6ed6acbfe1009","signature":"38a522420dd49b670936d9c003c596bbec6c7ea036856e1e6430f320da92de8225b45cd43af5b863e6b6889620bebbc42b8cb450660a7e2136654cd61c23a20e"} +{"type":"key","kid":"ed25519:56475aa75463474c","public_key_hex":"03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8","valid_from_seq":1,"valid_until_seq":2} +{"type":"key","kid":"ed25519:24f6ed6acbfe1009","public_key_hex":"29acbae141bccaf0b22e1a94d34d0bc7361e526d0bfe12c89794bc9322966dd7","valid_from_seq":3,"valid_until_seq":null} +{"type":"receipt","format_version":1,"seq":1,"timestamp_unix_ns":1,"human_principal":"h","agent_key_id":"a","delegation_chain":null,"service":"s","action":"x","params_sha256":"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","policy_decision":"allow","status_code":100,"latency_ms":0,"error":"","prev_hash":"0000000000000000000000000000000000000000000000000000000000000000","entry_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","signer_kid":"ed25519:56475aa75463474c","signature":"9b0b99df7d812a725ed1f810bbf913bf86b15de3152808cf97dc9ecdc45eb6e2f0481c80490d77d140cc48152c2d337871ebffa343dc69c5f18ee4d294ea1705"} +{"type":"receipt","format_version":1,"seq":2,"timestamp_unix_ns":1700000000000000001,"human_principal":"human-é/é/😀","agent_key_id":"agent-2","delegation_chain":["d2d8e9aea4f592e1ca94318065976dbe9147e57eaf6eb56e14ca9ae6004c6bae","482d488bd94eee647aa10471442b576f78c4646767b16616929cdef236d9eeb6"],"service":"operator","action":"decision:scale","params_sha256":"d110a9af75f1fb6bdecaf9590b2fa2b99df60f007e5ed4b37c72d57edbf11186","policy_decision":"deny","status_code":403,"latency_ms":17,"error":"scope_denied","prev_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","entry_hash":"b2a27707d518092a9fa6b069a0ef1907489ed66ad9ae3196d4bffafe3affcfe2","signer_kid":"unknown","signature":"d070daaf2edf8115f713d5749e4d8894ed4dfd3191d74d0d0f1d13082247bdb54055fe1dd818607edd9f8b26f86f4eed16cbcac784f509c68b1dcf3f672fe60e"} +{"type":"receipt","format_version":1,"seq":3,"timestamp_unix_ns":18446744073709551615,"human_principal":"h","agent_key_id":"a","delegation_chain":null,"service":"s","action":"x","params_sha256":"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","policy_decision":"rate_limited","status_code":599,"latency_ms":9223372036854775807,"error":"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","prev_hash":"b2a27707d518092a9fa6b069a0ef1907489ed66ad9ae3196d4bffafe3affcfe2","entry_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","signer_kid":"ed25519:24f6ed6acbfe1009","signature":"628208d560ad848a6635343e2d0c0ab1b8f2bc07f2e6ff69c8f9f8bb11a54f5a70f8a26e42dafb797146ac252f6df58fa2d976d306fd17a9ce336bf84692f600"} diff --git a/pkg/receiptspec/testdata/v1/valid-boundary.jsonl b/pkg/receiptspec/testdata/v1/valid-boundary.jsonl new file mode 100644 index 0000000..e929c1b --- /dev/null +++ b/pkg/receiptspec/testdata/v1/valid-boundary.jsonl @@ -0,0 +1,4 @@ +{"type":"manifest","format_version":1,"requested_from":18446744073709551615,"resolved_to":18446744073709551615,"count":1,"anchor_seq":18446744073709551614,"anchor_hash":"58b6d394047af8eaee4f1b9cf445bb34e648f7096cb40105af180d54755ec8fe","first_entry_hash":"393f5d597c088647185b507af322f38730372ae5f722581f98996a1d69c65ade","last_entry_hash":"393f5d597c088647185b507af322f38730372ae5f722581f98996a1d69c65ade","head_seq":18446744073709551615,"head_hash":"393f5d597c088647185b507af322f38730372ae5f722581f98996a1d69c65ade","keyset_digest":"9877fcf6988d2245cabc51a00b26fa546ef0c9209e17a5fd66c6cda80fe075eb","signer_kid":"ed25519:24f6ed6acbfe1009","signature":"31ee1c172f4e9517b6596d46d049787d4e874debc2ddf28796fa40eb62fc1126e203e03aa26011725834354e71ca0d121b2a7a3ed7b623475d826d124eb2990b"} +{"type":"key","kid":"ed25519:56475aa75463474c","public_key_hex":"03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8","valid_from_seq":1,"valid_until_seq":2} +{"type":"key","kid":"ed25519:24f6ed6acbfe1009","public_key_hex":"29acbae141bccaf0b22e1a94d34d0bc7361e526d0bfe12c89794bc9322966dd7","valid_from_seq":3,"valid_until_seq":null} +{"type":"receipt","format_version":1,"seq":18446744073709551615,"timestamp_unix_ns":18446744073709551615,"human_principal":"éééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééééé","agent_key_id":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","delegation_chain":["b8c6f33f1780d30977c5e964f62e7959102a3694f1c28ae0834ab12f98a3dcb0","89a1a98e709fa672374b463bbd8d5946ff4f530c5e65be07bf17ef8473ec96e9","2453695514ac2ba4f06e40a20e20cbc76b7a6c6d9a438c4a30e2acea3be39f57","0a4f88e04b578e42ff125845c9b27ba749498ed4b68a20c94cf0e195aae9cbc4","51b0b105bb16ebcd60cfee5ea698e21a60300ce6aee537045cb725bbb5b308a1","473ae6f80e73bd717a6c1afa74ede04c86236d6c1c8a6faaf5f33b80ca640d55","276ebaa0da80347dd44fb336e667fa673db8d019bc1f03a041f271573785c510","bf60053ae6caed9fc3ebb46abd5f29ff37265b7b080592ea930ed46072e8f4b3","373657ae16270347e7e2100556fc83da3ff7ef39e7821d1b81d1e7a8cc7f8c13","5d703eb2bcf96496bff435d54f27d2ebb164f88439b5b6d14123f01b4de30d71","487679bee7a9126eb48976fc1907d8152029de98fc38dd442cd9b3cdc552b75c","542948eee0541274f9689705b19f8f7e20b9202edbc76dc0dd45629e973f5f5e","92f05ed70b173aa2149ae309836ef0dd2427d958ecc9e1707f6c2c35683c591a","7b036d2149c2537d804fcfef6d728ebcbfb5bba0708f91389ce862e79f26cbfa","6385d5d674bcd41431061f0649d048d9887dc4de4e270836db0dd785ecf21936","1c323ba446689f5d69c85a3d4582d4f5ab494e4211ced74c3f8e868eae0d3c5a","d2ebd69bc371acfec30afca0c53d383cf9e597c0965c9cf2e61547c679a6c894","24a4f4793d81f6d335f994f1efabd5b48eb926be6ee5d7b110cc8405535252d8","7825d9c08f81bdcb5ab12d48e0cffedda4c7149955ff698e0f9005edcd0c7cfb","4622dff578451d8bfedc9485a8cd9ec43780397567dae7075dfb3da28aa64db5","7416e732cd4e9a45eb13d2e5a95dc137272ebff34a262664a1dfd01f897393d1","3c31ebf024b0b0b18b95a36e8994dd53dc1a6c7612501b0f569173bd871d0b0b","6694b4e5fef76ffc836253094f8243bf93c313f64eab53531e2beba349d0fbd1","5b7965c01917a187566d33139d53a4a914c92b40cbd7b1187ce1b224ddacb5da","c58fa5541a462700434aa3c1a06212397ffb0ae2049959fdb2b7f9d2dde817d6","2f9d75c4f99bef58b3e457bcc03030bf4a4932b87c5a7dc3b71544b0c46ad2d4","1417ccc33a5adac505d59d70cb47d4f3b7a73f7519d4a6b387c819ba4634d393","970f75ea382d4a03c2b4d200980a1beab8e3cf6f6c6df1002f50bf43dca3e278","cd9f35ab7e8b05806236675e84d5685154ff82fbca33676067588df49d11b0b7","180e151301a43a03596f7eeaaf94189eb97282a833dfb2ba1a7485288b162b3d","5513127a0d39eb9a9a5f2a3a92aba2bf93d8c55634bd4ee13064e5ef0437904a","f1564775d74b7f3f8bfa503f9c67b7e244ecf8a05b801e25068d089acc89252e"],"service":"ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss","action":"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","params_sha256":"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","policy_decision":"rate_limited","status_code":599,"latency_ms":9223372036854775807,"error":"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","prev_hash":"58b6d394047af8eaee4f1b9cf445bb34e648f7096cb40105af180d54755ec8fe","entry_hash":"393f5d597c088647185b507af322f38730372ae5f722581f98996a1d69c65ade","signer_kid":"ed25519:24f6ed6acbfe1009","signature":"3504ad0580109adafab2dba41209d04d69c181c26c0011b2d364ee5820b968a5ee2361f92d34adc9a556e565ba349b6238fcadb06b1cc797a894364377e71805"} diff --git a/pkg/receiptspec/testdata/v1/valid-chain.jsonl b/pkg/receiptspec/testdata/v1/valid-chain.jsonl new file mode 100644 index 0000000..d69419d --- /dev/null +++ b/pkg/receiptspec/testdata/v1/valid-chain.jsonl @@ -0,0 +1,6 @@ +{"type":"manifest","format_version":1,"requested_from":1,"resolved_to":3,"count":3,"anchor_seq":0,"anchor_hash":"0000000000000000000000000000000000000000000000000000000000000000","first_entry_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","last_entry_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","head_seq":3,"head_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","keyset_digest":"9877fcf6988d2245cabc51a00b26fa546ef0c9209e17a5fd66c6cda80fe075eb","signer_kid":"ed25519:24f6ed6acbfe1009","signature":"38a522420dd49b670936d9c003c596bbec6c7ea036856e1e6430f320da92de8225b45cd43af5b863e6b6889620bebbc42b8cb450660a7e2136654cd61c23a20e"} +{"type":"key","kid":"ed25519:56475aa75463474c","public_key_hex":"03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8","valid_from_seq":1,"valid_until_seq":2} +{"type":"key","kid":"ed25519:24f6ed6acbfe1009","public_key_hex":"29acbae141bccaf0b22e1a94d34d0bc7361e526d0bfe12c89794bc9322966dd7","valid_from_seq":3,"valid_until_seq":null} +{"type":"receipt","format_version":1,"seq":1,"timestamp_unix_ns":1,"human_principal":"h","agent_key_id":"a","delegation_chain":null,"service":"s","action":"x","params_sha256":"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","policy_decision":"allow","status_code":100,"latency_ms":0,"error":"","prev_hash":"0000000000000000000000000000000000000000000000000000000000000000","entry_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","signer_kid":"ed25519:56475aa75463474c","signature":"9b0b99df7d812a725ed1f810bbf913bf86b15de3152808cf97dc9ecdc45eb6e2f0481c80490d77d140cc48152c2d337871ebffa343dc69c5f18ee4d294ea1705"} +{"type":"receipt","format_version":1,"seq":2,"timestamp_unix_ns":1700000000000000001,"human_principal":"human-é/é/😀","agent_key_id":"agent-2","delegation_chain":["d2d8e9aea4f592e1ca94318065976dbe9147e57eaf6eb56e14ca9ae6004c6bae","482d488bd94eee647aa10471442b576f78c4646767b16616929cdef236d9eeb6"],"service":"operator","action":"decision:scale","params_sha256":"d110a9af75f1fb6bdecaf9590b2fa2b99df60f007e5ed4b37c72d57edbf11186","policy_decision":"deny","status_code":403,"latency_ms":17,"error":"scope_denied","prev_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","entry_hash":"b2a27707d518092a9fa6b069a0ef1907489ed66ad9ae3196d4bffafe3affcfe2","signer_kid":"ed25519:56475aa75463474c","signature":"d070daaf2edf8115f713d5749e4d8894ed4dfd3191d74d0d0f1d13082247bdb54055fe1dd818607edd9f8b26f86f4eed16cbcac784f509c68b1dcf3f672fe60e"} +{"type":"receipt","format_version":1,"seq":3,"timestamp_unix_ns":18446744073709551615,"human_principal":"h","agent_key_id":"a","delegation_chain":null,"service":"s","action":"x","params_sha256":"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","policy_decision":"rate_limited","status_code":599,"latency_ms":9223372036854775807,"error":"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","prev_hash":"b2a27707d518092a9fa6b069a0ef1907489ed66ad9ae3196d4bffafe3affcfe2","entry_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","signer_kid":"ed25519:24f6ed6acbfe1009","signature":"628208d560ad848a6635343e2d0c0ab1b8f2bc07f2e6ff69c8f9f8bb11a54f5a70f8a26e42dafb797146ac252f6df58fa2d976d306fd17a9ce336bf84692f600"} diff --git a/pkg/receiptspec/testdata/v1/valid-genesis.jsonl b/pkg/receiptspec/testdata/v1/valid-genesis.jsonl new file mode 100644 index 0000000..9b92702 --- /dev/null +++ b/pkg/receiptspec/testdata/v1/valid-genesis.jsonl @@ -0,0 +1,3 @@ +{"type":"key","kid":"ed25519:56475aa75463474c","public_key_hex":"03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8","valid_from_seq":1,"valid_until_seq":2} +{"type":"key","kid":"ed25519:24f6ed6acbfe1009","public_key_hex":"29acbae141bccaf0b22e1a94d34d0bc7361e526d0bfe12c89794bc9322966dd7","valid_from_seq":3,"valid_until_seq":null} +{"type":"receipt","format_version":1,"seq":1,"timestamp_unix_ns":1,"human_principal":"h","agent_key_id":"a","delegation_chain":null,"service":"s","action":"x","params_sha256":"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","policy_decision":"allow","status_code":100,"latency_ms":0,"error":"","prev_hash":"0000000000000000000000000000000000000000000000000000000000000000","entry_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","signer_kid":"ed25519:56475aa75463474c","signature":"9b0b99df7d812a725ed1f810bbf913bf86b15de3152808cf97dc9ecdc45eb6e2f0481c80490d77d140cc48152c2d337871ebffa343dc69c5f18ee4d294ea1705"} diff --git a/pkg/receiptspec/testdata/v1/valid-partial.jsonl b/pkg/receiptspec/testdata/v1/valid-partial.jsonl new file mode 100644 index 0000000..0347a27 --- /dev/null +++ b/pkg/receiptspec/testdata/v1/valid-partial.jsonl @@ -0,0 +1,5 @@ +{"type":"manifest","format_version":1,"requested_from":2,"resolved_to":3,"count":2,"anchor_seq":1,"anchor_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","first_entry_hash":"b2a27707d518092a9fa6b069a0ef1907489ed66ad9ae3196d4bffafe3affcfe2","last_entry_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","head_seq":3,"head_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","keyset_digest":"9877fcf6988d2245cabc51a00b26fa546ef0c9209e17a5fd66c6cda80fe075eb","signer_kid":"ed25519:24f6ed6acbfe1009","signature":"9c158453c7490a091ff0823c4c285022172fc2647377c4147be31bb9b03a64982f924739514a00b2b57b62273fee31d18fb28adce89a637305076990b0f52a04"} +{"type":"key","kid":"ed25519:56475aa75463474c","public_key_hex":"03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8","valid_from_seq":1,"valid_until_seq":2} +{"type":"key","kid":"ed25519:24f6ed6acbfe1009","public_key_hex":"29acbae141bccaf0b22e1a94d34d0bc7361e526d0bfe12c89794bc9322966dd7","valid_from_seq":3,"valid_until_seq":null} +{"type":"receipt","format_version":1,"seq":2,"timestamp_unix_ns":1700000000000000001,"human_principal":"human-é/é/😀","agent_key_id":"agent-2","delegation_chain":["d2d8e9aea4f592e1ca94318065976dbe9147e57eaf6eb56e14ca9ae6004c6bae","482d488bd94eee647aa10471442b576f78c4646767b16616929cdef236d9eeb6"],"service":"operator","action":"decision:scale","params_sha256":"d110a9af75f1fb6bdecaf9590b2fa2b99df60f007e5ed4b37c72d57edbf11186","policy_decision":"deny","status_code":403,"latency_ms":17,"error":"scope_denied","prev_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","entry_hash":"b2a27707d518092a9fa6b069a0ef1907489ed66ad9ae3196d4bffafe3affcfe2","signer_kid":"ed25519:56475aa75463474c","signature":"d070daaf2edf8115f713d5749e4d8894ed4dfd3191d74d0d0f1d13082247bdb54055fe1dd818607edd9f8b26f86f4eed16cbcac784f509c68b1dcf3f672fe60e"} +{"type":"receipt","format_version":1,"seq":3,"timestamp_unix_ns":18446744073709551615,"human_principal":"h","agent_key_id":"a","delegation_chain":null,"service":"s","action":"x","params_sha256":"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","policy_decision":"rate_limited","status_code":599,"latency_ms":9223372036854775807,"error":"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","prev_hash":"b2a27707d518092a9fa6b069a0ef1907489ed66ad9ae3196d4bffafe3affcfe2","entry_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","signer_kid":"ed25519:24f6ed6acbfe1009","signature":"628208d560ad848a6635343e2d0c0ab1b8f2bc07f2e6ff69c8f9f8bb11a54f5a70f8a26e42dafb797146ac252f6df58fa2d976d306fd17a9ce336bf84692f600"} diff --git a/pkg/receiptspec/testdata/v1/valid-unchecked-manifest-count.jsonl b/pkg/receiptspec/testdata/v1/valid-unchecked-manifest-count.jsonl new file mode 100644 index 0000000..36369eb --- /dev/null +++ b/pkg/receiptspec/testdata/v1/valid-unchecked-manifest-count.jsonl @@ -0,0 +1,6 @@ +{"type":"manifest","format_version":1,"requested_from":1,"resolved_to":3,"count":99,"anchor_seq":0,"anchor_hash":"0000000000000000000000000000000000000000000000000000000000000000","first_entry_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","last_entry_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","head_seq":3,"head_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","keyset_digest":"9877fcf6988d2245cabc51a00b26fa546ef0c9209e17a5fd66c6cda80fe075eb","signer_kid":"ed25519:24f6ed6acbfe1009","signature":"0a1e0ac8be59319ba581b5b61159f5a1502b9b9b6fa8e0eee3cb231c34b5c43f759d21ef4b789e4c6eb15b0b2046f6e70f1ba8a9f34ab0f1143752015ea76c0f"} +{"type":"key","kid":"ed25519:56475aa75463474c","public_key_hex":"03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8","valid_from_seq":1,"valid_until_seq":2} +{"type":"key","kid":"ed25519:24f6ed6acbfe1009","public_key_hex":"29acbae141bccaf0b22e1a94d34d0bc7361e526d0bfe12c89794bc9322966dd7","valid_from_seq":3,"valid_until_seq":null} +{"type":"receipt","format_version":1,"seq":1,"timestamp_unix_ns":1,"human_principal":"h","agent_key_id":"a","delegation_chain":null,"service":"s","action":"x","params_sha256":"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","policy_decision":"allow","status_code":100,"latency_ms":0,"error":"","prev_hash":"0000000000000000000000000000000000000000000000000000000000000000","entry_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","signer_kid":"ed25519:56475aa75463474c","signature":"9b0b99df7d812a725ed1f810bbf913bf86b15de3152808cf97dc9ecdc45eb6e2f0481c80490d77d140cc48152c2d337871ebffa343dc69c5f18ee4d294ea1705"} +{"type":"receipt","format_version":1,"seq":2,"timestamp_unix_ns":1700000000000000001,"human_principal":"human-é/é/😀","agent_key_id":"agent-2","delegation_chain":["d2d8e9aea4f592e1ca94318065976dbe9147e57eaf6eb56e14ca9ae6004c6bae","482d488bd94eee647aa10471442b576f78c4646767b16616929cdef236d9eeb6"],"service":"operator","action":"decision:scale","params_sha256":"d110a9af75f1fb6bdecaf9590b2fa2b99df60f007e5ed4b37c72d57edbf11186","policy_decision":"deny","status_code":403,"latency_ms":17,"error":"scope_denied","prev_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","entry_hash":"b2a27707d518092a9fa6b069a0ef1907489ed66ad9ae3196d4bffafe3affcfe2","signer_kid":"ed25519:56475aa75463474c","signature":"d070daaf2edf8115f713d5749e4d8894ed4dfd3191d74d0d0f1d13082247bdb54055fe1dd818607edd9f8b26f86f4eed16cbcac784f509c68b1dcf3f672fe60e"} +{"type":"receipt","format_version":1,"seq":3,"timestamp_unix_ns":18446744073709551615,"human_principal":"h","agent_key_id":"a","delegation_chain":null,"service":"s","action":"x","params_sha256":"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","policy_decision":"rate_limited","status_code":599,"latency_ms":9223372036854775807,"error":"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","prev_hash":"b2a27707d518092a9fa6b069a0ef1907489ed66ad9ae3196d4bffafe3affcfe2","entry_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","signer_kid":"ed25519:24f6ed6acbfe1009","signature":"628208d560ad848a6635343e2d0c0ab1b8f2bc07f2e6ff69c8f9f8bb11a54f5a70f8a26e42dafb797146ac252f6df58fa2d976d306fd17a9ce336bf84692f600"} diff --git a/pkg/receiptspec/testdata/v1/valid-unpinned-prefix.jsonl b/pkg/receiptspec/testdata/v1/valid-unpinned-prefix.jsonl new file mode 100644 index 0000000..060c5a2 --- /dev/null +++ b/pkg/receiptspec/testdata/v1/valid-unpinned-prefix.jsonl @@ -0,0 +1,4 @@ +{"type":"key","kid":"ed25519:56475aa75463474c","public_key_hex":"03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8","valid_from_seq":1,"valid_until_seq":2} +{"type":"key","kid":"ed25519:24f6ed6acbfe1009","public_key_hex":"29acbae141bccaf0b22e1a94d34d0bc7361e526d0bfe12c89794bc9322966dd7","valid_from_seq":3,"valid_until_seq":null} +{"type":"receipt","format_version":1,"seq":1,"timestamp_unix_ns":1,"human_principal":"h","agent_key_id":"a","delegation_chain":null,"service":"s","action":"x","params_sha256":"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","policy_decision":"allow","status_code":100,"latency_ms":0,"error":"","prev_hash":"0000000000000000000000000000000000000000000000000000000000000000","entry_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","signer_kid":"ed25519:56475aa75463474c","signature":"9b0b99df7d812a725ed1f810bbf913bf86b15de3152808cf97dc9ecdc45eb6e2f0481c80490d77d140cc48152c2d337871ebffa343dc69c5f18ee4d294ea1705"} +{"type":"receipt","format_version":1,"seq":2,"timestamp_unix_ns":1700000000000000001,"human_principal":"human-é/é/😀","agent_key_id":"agent-2","delegation_chain":["d2d8e9aea4f592e1ca94318065976dbe9147e57eaf6eb56e14ca9ae6004c6bae","482d488bd94eee647aa10471442b576f78c4646767b16616929cdef236d9eeb6"],"service":"operator","action":"decision:scale","params_sha256":"d110a9af75f1fb6bdecaf9590b2fa2b99df60f007e5ed4b37c72d57edbf11186","policy_decision":"deny","status_code":403,"latency_ms":17,"error":"scope_denied","prev_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","entry_hash":"b2a27707d518092a9fa6b069a0ef1907489ed66ad9ae3196d4bffafe3affcfe2","signer_kid":"ed25519:56475aa75463474c","signature":"d070daaf2edf8115f713d5749e4d8894ed4dfd3191d74d0d0f1d13082247bdb54055fe1dd818607edd9f8b26f86f4eed16cbcac784f509c68b1dcf3f672fe60e"} diff --git a/pkg/receiptspec/testdata/v1/vectors.json b/pkg/receiptspec/testdata/v1/vectors.json new file mode 100644 index 0000000..9b99559 --- /dev/null +++ b/pkg/receiptspec/testdata/v1/vectors.json @@ -0,0 +1,225 @@ +{ + "format_version": 1, + "vectors": [ + { + "name": "valid-genesis", + "jsonl": "valid-genesis.jsonl", + "keys": "public-keys.json", + "expected": { + "class": "ok", + "verified_count": 1, + "failed_at_seq": 0, + "complete": false + } + }, + { + "name": "valid-chain", + "jsonl": "valid-chain.jsonl", + "keys": "public-keys.json", + "expected": { + "class": "ok", + "verified_count": 3, + "failed_at_seq": 0, + "complete": true + } + }, + { + "name": "valid-partial", + "jsonl": "valid-partial.jsonl", + "keys": "public-keys.json", + "expected": { + "class": "ok", + "verified_count": 2, + "failed_at_seq": 0, + "complete": true + } + }, + { + "name": "valid-unpinned-prefix", + "jsonl": "valid-unpinned-prefix.jsonl", + "keys": "public-keys.json", + "expected": { + "class": "ok", + "verified_count": 2, + "failed_at_seq": 0, + "complete": false + } + }, + { + "name": "valid-boundary", + "jsonl": "valid-boundary.jsonl", + "keys": "public-keys.json", + "expected": { + "class": "ok", + "verified_count": 1, + "failed_at_seq": 0, + "complete": true + } + }, + { + "name": "tampered-body", + "jsonl": "tampered-body.jsonl", + "keys": "public-keys.json", + "expected": { + "class": "entry_hash_mismatch", + "verified_count": 1, + "failed_at_seq": 2, + "complete": false + } + }, + { + "name": "reordered-chain", + "jsonl": "reordered-chain.jsonl", + "keys": "public-keys.json", + "expected": { + "class": "sequence_gap_or_not_genesis_anchored", + "verified_count": 0, + "failed_at_seq": 2, + "complete": false + } + }, + { + "name": "missing-middle", + "jsonl": "missing-middle.jsonl", + "keys": "public-keys.json", + "expected": { + "class": "sequence_gap_or_not_genesis_anchored", + "verified_count": 1, + "failed_at_seq": 3, + "complete": false + } + }, + { + "name": "missing-tail-with-manifest", + "jsonl": "missing-tail-with-manifest.jsonl", + "keys": "public-keys.json", + "expected": { + "class": "expected_head_mismatch", + "verified_count": 2, + "failed_at_seq": 3, + "complete": false + } + }, + { + "name": "wrong-key-signature", + "jsonl": "wrong-key-signature.jsonl", + "keys": "public-keys.json", + "expected": { + "class": "signature_invalid", + "verified_count": 1, + "failed_at_seq": 2, + "complete": false + } + }, + { + "name": "bad-prev-hash", + "jsonl": "bad-prev-hash.jsonl", + "keys": "public-keys.json", + "expected": { + "class": "prev_hash_mismatch", + "verified_count": 1, + "failed_at_seq": 2, + "complete": false + } + }, + { + "name": "invalid-fields", + "jsonl": "invalid-fields.jsonl", + "keys": "public-keys.json", + "expected": { + "class": "invalid_receipt_fields", + "verified_count": 1, + "failed_at_seq": 2, + "complete": false + } + }, + { + "name": "unknown-signer", + "jsonl": "unknown-signer.jsonl", + "keys": "public-keys.json", + "expected": { + "class": "ErrUnknownSignerKID", + "verified_count": 0, + "failed_at_seq": 0, + "complete": false + } + }, + { + "name": "empty-chain", + "jsonl": "empty-chain.jsonl", + "keys": "public-keys.json", + "expected": { + "class": "ErrEmptyChain", + "verified_count": 0, + "failed_at_seq": 0, + "complete": false + } + }, + { + "name": "inactive-signer", + "jsonl": "inactive-signer.jsonl", + "keys": "inactive-keys.json", + "expected": { + "class": "signer_kid_inactive_at_seq", + "verified_count": 0, + "failed_at_seq": 1, + "complete": false + } + }, + { + "name": "manifest-mismatch", + "jsonl": "manifest-mismatch.jsonl", + "keys": "public-keys.json", + "expected": { + "class": "ManifestError/ErrManifestSignatureInvalid", + "verified_count": 0, + "failed_at_seq": 0, + "complete": false + } + }, + { + "name": "manifest-keyset-mismatch", + "jsonl": "manifest-keyset-mismatch.jsonl", + "keys": "public-keys.json", + "expected": { + "class": "ManifestError/ErrKeysetDigestMismatch", + "verified_count": 0, + "failed_at_seq": 0, + "complete": false + } + }, + { + "name": "manifest-unknown-signer", + "jsonl": "manifest-unknown-signer.jsonl", + "keys": "public-keys.json", + "expected": { + "class": "ManifestError/ErrUnknownSignerKID", + "verified_count": 0, + "failed_at_seq": 0, + "complete": false + } + }, + { + "name": "manifest-endpoint-mismatch", + "jsonl": "manifest-endpoint-mismatch.jsonl", + "keys": "public-keys.json", + "expected": { + "class": "expected_head_mismatch", + "verified_count": 3, + "failed_at_seq": 4, + "complete": false + } + }, + { + "name": "valid-unchecked-manifest-count", + "jsonl": "valid-unchecked-manifest-count.jsonl", + "keys": "public-keys.json", + "expected": { + "class": "ok", + "verified_count": 3, + "failed_at_seq": 0, + "complete": true + } + } + ] +} diff --git a/pkg/receiptspec/testdata/v1/wrong-key-signature.jsonl b/pkg/receiptspec/testdata/v1/wrong-key-signature.jsonl new file mode 100644 index 0000000..0c980fd --- /dev/null +++ b/pkg/receiptspec/testdata/v1/wrong-key-signature.jsonl @@ -0,0 +1,6 @@ +{"type":"manifest","format_version":1,"requested_from":1,"resolved_to":3,"count":3,"anchor_seq":0,"anchor_hash":"0000000000000000000000000000000000000000000000000000000000000000","first_entry_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","last_entry_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","head_seq":3,"head_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","keyset_digest":"9877fcf6988d2245cabc51a00b26fa546ef0c9209e17a5fd66c6cda80fe075eb","signer_kid":"ed25519:24f6ed6acbfe1009","signature":"38a522420dd49b670936d9c003c596bbec6c7ea036856e1e6430f320da92de8225b45cd43af5b863e6b6889620bebbc42b8cb450660a7e2136654cd61c23a20e"} +{"type":"key","kid":"ed25519:56475aa75463474c","public_key_hex":"03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8","valid_from_seq":1,"valid_until_seq":2} +{"type":"key","kid":"ed25519:24f6ed6acbfe1009","public_key_hex":"29acbae141bccaf0b22e1a94d34d0bc7361e526d0bfe12c89794bc9322966dd7","valid_from_seq":3,"valid_until_seq":null} +{"type":"receipt","format_version":1,"seq":1,"timestamp_unix_ns":1,"human_principal":"h","agent_key_id":"a","delegation_chain":null,"service":"s","action":"x","params_sha256":"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","policy_decision":"allow","status_code":100,"latency_ms":0,"error":"","prev_hash":"0000000000000000000000000000000000000000000000000000000000000000","entry_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","signer_kid":"ed25519:56475aa75463474c","signature":"9b0b99df7d812a725ed1f810bbf913bf86b15de3152808cf97dc9ecdc45eb6e2f0481c80490d77d140cc48152c2d337871ebffa343dc69c5f18ee4d294ea1705"} +{"type":"receipt","format_version":1,"seq":2,"timestamp_unix_ns":1700000000000000001,"human_principal":"human-é/é/😀","agent_key_id":"agent-2","delegation_chain":["d2d8e9aea4f592e1ca94318065976dbe9147e57eaf6eb56e14ca9ae6004c6bae","482d488bd94eee647aa10471442b576f78c4646767b16616929cdef236d9eeb6"],"service":"operator","action":"decision:scale","params_sha256":"d110a9af75f1fb6bdecaf9590b2fa2b99df60f007e5ed4b37c72d57edbf11186","policy_decision":"deny","status_code":403,"latency_ms":17,"error":"scope_denied","prev_hash":"b9647a242fa29e0bbb32d5d3e1157165b4a5ebc24a1b04c71d6f4b4d03cba138","entry_hash":"b2a27707d518092a9fa6b069a0ef1907489ed66ad9ae3196d4bffafe3affcfe2","signer_kid":"ed25519:56475aa75463474c","signature":"6d34de46a57e31b8feb8cbb565e8e1374fef2d7a4e0d47d849d1e4afd47195aeee4ab040a175cce5ef6f41ee6fc1f72347df07000deba8ed8ccbf51ea4722d0a"} +{"type":"receipt","format_version":1,"seq":3,"timestamp_unix_ns":18446744073709551615,"human_principal":"h","agent_key_id":"a","delegation_chain":null,"service":"s","action":"x","params_sha256":"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","policy_decision":"rate_limited","status_code":599,"latency_ms":9223372036854775807,"error":"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","prev_hash":"b2a27707d518092a9fa6b069a0ef1907489ed66ad9ae3196d4bffafe3affcfe2","entry_hash":"559b87ae267e480b80e34aacf5111f94d200c94ef8fa111296965cad3d43b695","signer_kid":"ed25519:24f6ed6acbfe1009","signature":"628208d560ad848a6635343e2d0c0ab1b8f2bc07f2e6ff69c8f9f8bb11a54f5a70f8a26e42dafb797146ac252f6df58fa2d976d306fd17a9ce336bf84692f600"} diff --git a/pkg/receiptspec/vectors_test.go b/pkg/receiptspec/vectors_test.go new file mode 100644 index 0000000..6d9fce9 --- /dev/null +++ b/pkg/receiptspec/vectors_test.go @@ -0,0 +1,235 @@ +/* +Copyright 2026 Clawdlinux. +Licensed under the Apache License, Version 2.0. +*/ + +package receiptspec + +import ( + "crypto/ed25519" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "os" + "path/filepath" + "reflect" + "strings" + "testing" +) + +type vectorOutcome struct { + Class string `json:"class"` + VerifiedCount int `json:"verified_count"` + FailedAtSeq uint64 `json:"failed_at_seq"` + Complete bool `json:"complete"` +} + +type goldenVector struct { + Name string `json:"name"` + JSONL string `json:"jsonl"` + Keys string `json:"keys"` + Expected vectorOutcome `json:"expected"` +} + +func loadVectors(t *testing.T) []goldenVector { + t.Helper() + var catalog struct { + FormatVersion int `json:"format_version"` + Vectors []goldenVector `json:"vectors"` + } + readVectorJSON(t, "vectors.json", &catalog) + if catalog.FormatVersion != 1 || len(catalog.Vectors) == 0 { + t.Fatal("expected a nonempty v1 vector catalog") + } + files, err := filepath.Glob("testdata/v1/*.jsonl") + if err != nil || len(files) != len(catalog.Vectors) { + t.Fatalf("every JSONL fixture must be listed: files=%d vectors=%d err=%v", len(files), len(catalog.Vectors), err) + } + seen := make(map[string]bool) + for _, v := range catalog.Vectors { + if seen[v.JSONL] { + t.Fatalf("duplicate vector file %q", v.JSONL) + } + seen[v.JSONL] = true + } + return catalog.Vectors +} + +func readVectorJSON(t *testing.T, name string, out any) { + t.Helper() + if err := json.Unmarshal(readVectorFile(t, name), out); err != nil { + t.Fatal(err) + } +} + +func readVectorFile(t *testing.T, name string) []byte { + t.Helper() + data, err := os.ReadFile(filepath.Join("testdata/v1", name)) + if err != nil { + t.Fatal(err) + } + return data +} + +func TestV1GoldenVectors(t *testing.T) { + for _, v := range loadVectors(t) { + t.Run(v.Name, func(t *testing.T) { + bundle, err := ReadJSONL(strings.NewReader(string(readVectorFile(t, v.JSONL)))) + if err != nil { + t.Fatal(err) + } + trust, err := LoadTrustedKeys(readVectorFile(t, v.Keys)) + if err != nil { + t.Fatal(err) + } + for _, source := range []struct { + name string + keys []TrustedKey + }{{"external-trust", trust}, {"embedded-trust", nil}} { + t.Run(source.name, func(t *testing.T) { + result, err := VerifyBundle(bundle, source.keys, nil) + got := vectorOutcome{ + Class: vectorResultClass(t, result, err), VerifiedCount: result.VerifiedCount, + FailedAtSeq: result.FailedAtSeq, Complete: result.Complete, + } + if got != v.Expected { + t.Fatalf("got %+v (error %v), want %+v", got, err, v.Expected) + } + }) + } + }) + } +} + +func TestV1WorkedExample(t *testing.T) { + var example struct { + SeedHex string `json:"seed_hex"` + PublicKeyHex string `json:"public_key_hex"` + KID string `json:"kid"` + PreimageHex string `json:"preimage_hex"` + PreimageBytes int `json:"preimage_bytes"` + EntryHash string `json:"entry_hash"` + Signature string `json:"signature"` + } + readVectorJSON(t, "example.json", &example) + seed, err := hex.DecodeString(example.SeedHex) + if err != nil || len(seed) != ed25519.SeedSize { + t.Fatalf("invalid example seed: %v", err) + } + private := ed25519.NewKeyFromSeed(seed) + public := private.Public().(ed25519.PublicKey) + if hex.EncodeToString(public) != example.PublicKeyHex || ComputeKID(public) != example.KID { + t.Fatal("worked example key derivation differs") + } + bundle, err := ReadJSONL(strings.NewReader(string(readVectorFile(t, "valid-genesis.jsonl")))) + if err != nil || len(bundle.Receipts) != 1 { + t.Fatalf("invalid worked example: %v", err) + } + r := bundle.Receipts[0] + preimage, err := CanonicalHashInput(r) + if err != nil { + t.Fatal(err) + } + if len(preimage) != example.PreimageBytes || hex.EncodeToString(preimage) != example.PreimageHex { + t.Fatal("worked example preimage differs") + } + hash := sha256.Sum256(preimage) + if hex.EncodeToString(hash[:]) != example.EntryHash || r.EntryHash != hash { + t.Fatal("worked example entry hash differs") + } + signature := ed25519.Sign(private, hash[:]) + if hex.EncodeToString(signature) != example.Signature || hex.EncodeToString(r.Signature[:]) != example.Signature { + t.Fatal("worked example signature differs") + } + if err := VerifyReceipt(r, public); err != nil { + t.Fatal(err) + } +} + +func vectorResultClass(t *testing.T, result VerifyResult, err error) string { + t.Helper() + if err == nil { + if result.OK { + return "ok" + } + if result.Reason == "" { + t.Fatal("failed verification without a reason") + } + return result.Reason + } + prefix := "" + var manifestError *ManifestError + if errors.As(err, &manifestError) { + prefix = "ManifestError/" + } + for _, candidate := range []struct { + err error + class string + }{ + {ErrUnknownSignerKID, "ErrUnknownSignerKID"}, + {ErrEmptyChain, "ErrEmptyChain"}, + {ErrNoTrustedKeys, "ErrNoTrustedKeys"}, + {ErrManifestSignatureInvalid, "ErrManifestSignatureInvalid"}, + {ErrKeysetDigestMismatch, "ErrKeysetDigestMismatch"}, + } { + if errors.Is(err, candidate.err) { + return prefix + candidate.class + } + } + t.Fatalf("unclassified verifier error: %v", err) + return "" +} + +type encodingLock struct { + Receipts []string `json:"receipts"` + Manifest string `json:"manifest,omitempty"` +} + +func TestV1VersionLock(t *testing.T) { + const guidance = "v1 encoding changed: bump format_version and write docs/spec/RECEIPT-v2.md; preserve the v1 lock" + if JSONLFormatVersion != 1 || ExportFormatVersion != 1 || + HashDomainV1 != "agentgate.receipt.hash.v1" || + ExportManifestDomainV1 != "agentgate.receipt.export-manifest.v1" { + t.Fatal(guidance) + } + // This file is deliberately outside the generator's outputs. + var locks map[string]encodingLock + readVectorJSON(t, "encoding-lock.json", &locks) + checked := 0 + for _, v := range loadVectors(t) { + if v.Expected.Class != "ok" { + continue + } + t.Run(v.Name, func(t *testing.T) { + want, ok := locks[v.Name] + if !ok { + t.Fatalf("missing committed lock for %s", v.Name) + } + bundle, err := ReadJSONL(strings.NewReader(string(readVectorFile(t, v.JSONL)))) + if err != nil { + t.Fatal(err) + } + var got encodingLock + for _, r := range bundle.Receipts { + preimage, err := CanonicalHashInput(r) + if err != nil { + t.Fatalf("%s: %v", guidance, err) + } + hash := sha256.Sum256(preimage) + got.Receipts = append(got.Receipts, hex.EncodeToString(hash[:])) + } + if bundle.Manifest != nil { + hash := sha256.Sum256(canonicalManifestInput(*bundle.Manifest)) + got.Manifest = hex.EncodeToString(hash[:]) + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("%s\ngot: %+v\nwant: %+v", guidance, got, want) + } + }) + checked++ + } + if checked != len(locks) { + t.Fatalf("lock coverage differs: checked=%d committed=%d", checked, len(locks)) + } +}