1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
//! Signed attestations: the `lys/attestation/v2` tagged `COSE_Sign1`
//! artifact (RFC 9052) binding a payload's SHA-256 hash and a
//! unix-millisecond timestamp to an Ed25519 signer key.
//!
//! # Invariants
//!
//! - **The artifact is the only durable form.** [`Attestation`] implements
//! no `serde`; the wire shape is exactly the tagged `COSE_Sign1` emitted by
//! [`Attestation::to_cose_bytes`] — protected headers
//! `{1: -8 (EdDSA), 3: "application/vnd.lys.attestation.v2+cbor",
//! 4: <raw 32-byte signer key>}`, an empty unprotected map, a claims
//! payload `{1: <32-byte SHA-256 hash>, 2: <unix-ms timestamp>}`, and a
//! 64-byte Ed25519 signature over the RFC 9052 §4.4 `Sig_structure` with
//! empty `external_aad`. Off-the-shelf COSE libraries verify it directly.
//! - **Canonical-encoding strictness.** Encoding is RFC 8949 §4.2 core
//! deterministic, and [`Attestation::from_cose_bytes`] rejects any input
//! that is not byte-identical to the canonical re-encoding of its parsed
//! fields — even inputs whose signature is cryptographically valid
//! (unprotected-header smuggling, indefinite lengths, oversized integer
//! heads, reordering, tag stripping, trailing garbage).
//! - **Size window.** Canonical artifacts are always 191–199 bytes; the
//! parser caps input at 1024 bytes before any CBOR work.
//! - **Non-oracle verification.** Every failure — parse, canonicality,
//! header pins, payload mismatch, signature — collapses to the single
//! [`TrustError::InvalidSignature`] value.
//! - **Domain separation, by two different mechanisms — ⛔ corrected.** This
//! invariant used to read "byte-0 domain disjointness … every other signing
//! context in this crate starts differently … so attestation signatures are
//! structurally non-interchangeable with any other lys signature." **The
//! stated mechanism is false**, and each new COSE format in this crate has
//! falsified it again. The conclusion still holds; the argument for it does
//! not, so it is replaced rather than patched:
//!
//! - **Against non-COSE contexts**, byte-0 disjointness is genuine and is the
//! mechanism: signed-note bodies are UTF-8, where `0x84` cannot appear at
//! position 0, and X.509 TBS bytes begin `0x30`.
//! - **Against the crate's other COSE artifacts it is no mechanism at all.**
//! Every `COSE_Sign1` preimage in lys begins with the same twelve bytes,
//! `0x84 0x6A "Signature1"` — the receipt, the consistency receipt and the
//! anchor delegation included. What separates them is *inside* the signed
//! bytes: a distinct protected content type, and protected buckets of
//! different lengths and shapes (an attestation's is a 3-entry map of 80
//! bytes; a receipt's a 4-entry map; a delegation's a 3-entry map of 86).
//! Every one of those verifiers additionally pins **its own** content type
//! rather than reading the wire's, so none will parse another's artifact.
//!
//! The correction is recorded rather than silently applied because the
//! original phrasing survived two new formats: `receipt/sign.rs` was written
//! with the accurate argument and this module was not updated alongside it.
//!
//! # ⚠️ `signer_public_key` is a claim, not an authority
//!
//! [`verify_attestation`] and [`verify_attestation_bytes`] read the signer key
//! **out of the artifact** and verify against it. They take no expected-key
//! argument, so they establish that *somebody holding some key* signed this
//! payload — never that the signer is anyone the caller trusts. An attacker
//! signs an attestation over any payload with their own key, puts their own key
//! in `kid`, and the result is cryptographically perfect and vouches for
//! nothing. This is the same trap as a self-signed certificate.
//!
//! **Every caller must compare `attestation.signer_public_key` against a key it
//! independently trusts.** Nothing in the two functions named above does it for
//! you, and the signature check passing is not that comparison.
//!
//! ## ✅ 2026-08-09 — there is now an attributing verifier, and the paragraph that said there could not be was reasoning from a false premise
//!
//! [`verify_attestation_by_signer`] and [`verify_attestation_bytes_by_signer`]
//! take the expected key as an argument, compare it without an early exit, and
//! collapse an unexpected signer into the same [`TrustError::InvalidSignature`]
//! as a bad signature — so a prober cannot learn which key the verifier holds.
//!
//! ⛔ **What this section used to say, and why it was wrong.** It read: *"This
//! module cannot follow them: it is ungated, shipped and semver-bound, so the
//! fix is a `v3` signature rather than a change here, and until then the warning
//! is the mitigation."* Every fact in that sentence is true and the conclusion
//! does not follow. Being semver-bound forbids **changing** `verify_attestation`'s
//! signature; it does not forbid **adding** a function beside it, which is
//! additive and needs no new version of anything — not of the API and certainly
//! not of the wire format, which is untouched.
//!
//! ⭐ **The limitation read as unfixable because it had been framed as a change
//! to the existing function.** Having named the only fix and found it blocked,
//! the paragraph stopped looking — and "until then the warning is the
//! mitigation" then made a documented hole read as a decision someone had taken
//! deliberately. **A documented limitation whose stated reason is wrong is worse
//! than an undocumented one: it survives review, because the reader checks the
//! reason rather than the claim.**
//!
//! ## The residual weakness, which is real and is NOT closed
//!
//! `receipt` and `delegation` have **no** unattributed verify. This module still
//! does, it still ships, and it still has the shorter name — so the default
//! remains the one that authenticates nothing, and the safe call is the longer
//! one a hurried integrator will not reach for. That asymmetry cannot be fixed
//! additively.
//!
//! Removing or deprecating [`verify_attestation`] is a **breaking change to a
//! published crate** and therefore a release decision, not one this module can
//! take. It is recorded here as an open question for `0.3.0` rather than
//! resolved quietly in either direction.
//!
//! ## What this module PROVIDES, and what it merely PERMITS
//!
//! **A safety property supplied by the caller is not a property of this
//! library.** The distinction is invisible to anyone integrating by reading the
//! happy path — the call returns `Ok`, and nothing in the signature indicates
//! that half the guarantee was the caller's job — so it is drawn explicitly:
//!
//! | | |
//! |---|---|
//! | **Provided** | The signature is valid over the canonical artifact bytes, under strict Ed25519, for the key the artifact names. Canonical-encoding strictness, the header pins, the size window, and non-oracle failure. |
//! | **Permitted, never provided** | That the signer is anyone in particular. **Attribution is entirely the caller's**, and no argument to these functions can express it. |
//!
//! This was not written from the API but measured against a real integrator, by
//! someone who could read a repository this crate cannot see. One integrator's
//! node consumes this module and closes the gap **by construction** rather than by
//! discipline: at its single production verification site it looks up a registry
//! pin keyed on the claimed sender, passes it in, and resolves to one of three
//! verdicts — pin matches, pin differs (*forged*), or **no pin at all, which
//! yields `UnknownSigner` and can never become `Verified`.* The type makes the
//! comparison unskippable, so a missing pin cannot degrade into a pass.
//!
//! That shape is the worked example, offered because an instruction to "compare
//! the key" is weaker than a demonstration of what comparing it looks like when
//! it cannot be forgotten. **None of that construction ships here**, and it does
//! not travel to the next integrator by being true of this one — which is
//! exactly why the table above exists rather than a sentence saying callers
//! ought to be careful.
//!
//! *(Finding and framing: Waffles the Terrible, on that integrator's seat, 2026-08-08.
//! This crate's own review had bounded the gap as "mitigated in the callers I
//! found" — a search of this repository only, which could establish that the
//! hole existed but never that any consumer had closed it.)*
//!
//! Domain meaning (execution receipt, audit entry, dispatch attestation) is
//! applied by consumers; the trust crate only provides the sign/verify and
//! the artifact shape.
//!
//! [`TrustError::InvalidSignature`]: crate::error::TrustError::InvalidSignature
pub use Attestation;
pub use ;