pub struct Receipt {
pub body: ReceiptBody,
pub signature_b64: String,
}Expand description
A complete receipt: body + detached signature over JCS(body).
The wire shape is intentionally flat (13 body fields + signature_b64
at the top level) — the schema at schemas/receipt-v1.schema.json
commits to it and downstream verifiers (avctl receipt-verify,
external tools) consume it that way.
#[serde(flatten)] normally silently disables deny_unknown_fields
on the inner struct, which would let a hostile issuer add extra
top-level fields (“claim_extra”: “grant admin”) that the human
reviewer sees but verify() accepts (the field never survives the
round-trip to ReceiptBody used inside canonicalize). To close
that gap without breaking the wire shape, the Deserialize impl
is written by hand and rejects any top-level key outside the
declared whitelist. See the
allowed_receipt_top_level_keys_cover_body_fields_exactly
test for the compile-time-ish drift guard.
Fields§
§body: ReceiptBodySigned body.
signature_b64: StringEd25519 signature over the JCS canonicalization of the body, base64.
Implementations§
Source§impl Receipt
impl Receipt
Sourcepub fn from_json_slice(bytes: &[u8]) -> Result<Self, ReceiptError>
pub fn from_json_slice(bytes: &[u8]) -> Result<Self, ReceiptError>
Deserialize a receipt from wire bytes, rejecting any duplicate key at ANY nesting level.
The custom Deserialize impl for Receipt (below) already
rejects duplicate top-level keys, but nested structs
(ai_agent, tool_calls, cost, subject) went through
serde’s default derive which relies on serde_json::Map /
IndexMap — both silently collapse duplicate keys with
last-wins semantics. A hostile issuer could then sign a
receipt whose ai_agent.instance_uid appeared twice
("a" then "b"); JCS canonicalisation saw only "b" so
the signature verified, but a first-wins auditor tool (jq’s
default, some Python configs) displayed "a" — the exact
split-brain the top-level guard was written to prevent, one
level deeper.
Round-15 F4: pre-scan the JSON with a strict duplicate-key
checker (check_no_duplicate_keys) before deserialising into
Receipt. Callers verifying a receipt off the wire should
prefer this over serde_json::from_slice::<Receipt>.
Sourcepub fn from_json_str(s: &str) -> Result<Self, ReceiptError>
pub fn from_json_str(s: &str) -> Result<Self, ReceiptError>
Same as Receipt::from_json_slice for owned/borrowed strings.
Sourcepub fn issue(
body: ReceiptBody,
signer: &dyn Signer,
) -> Result<Self, ReceiptError>
pub fn issue( body: ReceiptBody, signer: &dyn Signer, ) -> Result<Self, ReceiptError>
Issue (sign) a receipt over body with signer.
The key_id and public_key_b64 fields of the body are overwritten
from the signer — a caller can never claim someone else’s key identity.
Sourcepub fn verify(&self, ring: &Keyring) -> Result<(), ReceiptError>
pub fn verify(&self, ring: &Keyring) -> Result<(), ReceiptError>
Verify offline against a keyring. Checks, in execution order:
AtifTrajectory.retroactive == true(round-16 F8 —retroactive: falseon an ATIF-promoted receipt is semantically nonsense; the schema pins it toconst: truebut the Rust type still acceptsfalse; refuse it here so the two agree);- the embedded public key matches the ring’s key for
key_id(anti-substitution); - the signature verifies over
JCS(body).
Sourcepub fn verify_embedded(&self) -> Result<(), ReceiptError>
pub fn verify_embedded(&self) -> Result<(), ReceiptError>
Verify self-contained (trusting the embedded public key). Suitable when
the verifier obtained the receipt over an authenticated channel or
pins key ids separately. Prefer Receipt::verify with a ring.