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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
//! Byte-exact CBOR/COSE encoding for the `lys/anchor-receipt/v1` artifact.
//!
//! # Invariants
//!
//! - **Encoding is hand-assembled and infallible.** Every emitted byte comes
//! from this module's fixed-shape writers over [`crate::cbor`]'s canonical
//! heads — RFC 8949 §4.2 core deterministic by construction, immune to any
//! serializer dependency's encoding choices across upgrades.
//! - **Decoding of untrusted input is never hand-rolled.** [`decode_fields`]
//! parses with `ciborium` and then enforces the exact artifact shape; the
//! caller ([`super::artifact::AnchorReceipt::from_cose_bytes`]) additionally
//! re-encodes the extracted fields and requires byte-identity with the
//! input (canonical-encoding strictness).
//! - The protected header bucket is
//! `{1: -8 (EdDSA), 3: <content type>,
//! 4: <raw 32-byte Ed25519 anchor key>, 395: 1 (RFC9162_SHA256)}` in
//! RFC 8949 §4.2 key order — exactly 80 bytes for an inclusion receipt and
//! 92 for a consistency one, the difference being the media-type string
//! alone. Ascending numeric label order and ascending bytewise-encoded-key
//! order coincide for `{1, 3, 4, 395}`, because all four are non-negative
//! and shorter heads sort first.
//! - **The content type is the caller's declaration, never the wire's.** The
//! two receipt kinds are otherwise the same COSE shape signed by the same key
//! over a 32-byte detached root, so the content type inside the signed bytes
//! is the only thing that stops one being re-labelled as the other.
//! - **The payload is `nil`.** The signature covers the anchor's 32-byte
//! Merkle root as a *detached* payload, which never appears in the artifact
//! — the verifier recomputes it. See [`super`] for why that is the point.
//! - Every decode failure collapses to
//! [`TrustError::ReceiptVerification`](crate::error::TrustError::ReceiptVerification)
//! (non-oracle; see the [`super`] module docs).
//!
//! # The vdp shape follows RFC 9942 exactly, including its wrappers
//!
//! The verifiable data proof is **not** a bare `[tree_size, leaf_index, path]`
//! array. RFC 9942's CDDL nests it twice:
//!
//! ```text
//! 396 => { -1 => [ + inclusion-proof ] }
//! inclusion-proof = bstr .cbor [ tree-size, leaf-index, inclusion-path ]
//! ```
//!
//! Both wrappers are reproduced here — the array-of-proofs at `-1`, and the
//! `bstr` that wraps each proof's CBOR. They are easy to drop by accident and
//! dropping either would make our receipts unparseable by every conforming
//! RFC 9942 implementation, which is the one outcome this format exists to
//! avoid. lys issues and accepts **exactly one** proof in that array; see
//! [`decode_fields`].
use crate;
pub use ;
/// The `lys/anchor-receipt/v1` domain discriminator: the protected content
/// type (COSE header label 3). Signature-covered. This string is a frozen wire
/// contract — evolving the artifact means a new `v2` media type, never a
/// mutation of this one.
///
/// It is also what separates a receipt from a `lys/attestation/v2`
/// attestation, which is the same COSE shape signed by the same kind of key.
pub const CONTENT_TYPE: &str = "application/vnd.lys.receipt.v1+cbor";
/// The `lys/consistency-receipt/v1` domain discriminator (COSE header label 3).
///
/// **It must differ from [`CONTENT_TYPE`], and that is a security property
/// rather than a naming convention.** The two receipt kinds are the same COSE
/// shape signed by the same key over a 32-byte detached root; with a shared
/// content type their protected headers would be byte-identical, so an inclusion
/// receipt over root `R` at size `S` could be re-labelled as a consistency
/// receipt claiming `R` is the newer root at `tree_size_2 = S`, and the
/// anchor's real signature would verify over it. The proof lives in the
/// *unprotected* header, so nothing there is covered; the content type inside
/// the signed bytes is what refuses the re-label before any proof is examined.
///
/// Frozen wire contract, as for [`CONTENT_TYPE`]: evolving the artifact means a
/// new `v2` media type, never a mutation of this one.
pub const CONSISTENCY_CONTENT_TYPE: &str = "application/vnd.lys.consistency-receipt.v1+cbor";
/// Length of a SHA-256 digest, and so of every node in an inclusion path.
pub const DIGEST_LEN: usize = 32;
/// Hard cap on inclusion-path elements. A path over a tree of at most
/// `u64::MAX` leaves can never exceed 64 nodes, so anything longer is
/// structurally impossible rather than merely large — rejected before any
/// hashing work.
pub const MAX_PATH_ELEMENTS: usize = 64;
/// Hard input cap for [`decode_fields`]. A canonical receipt is at most
/// roughly 2.4 KiB (80-byte protected bucket, a 64-node path at 34 bytes per
/// node, a 64-byte signature); this bound is comfortably above that and
/// rejects oversize input before parsing.
pub const MAX_ARTIFACT_LEN: usize = 4096;
/// CBOR tag number for `COSE_Sign1` (RFC 9052 §2). The artifact is always
/// tagged, and the verifier requires the tag.
const COSE_SIGN1_TAG: u64 = 18;
/// COSE header label `alg`.
const HEADER_LABEL_ALG: u64 = 1;
/// COSE header label `content type`.
const HEADER_LABEL_CONTENT_TYPE: u64 = 3;
/// COSE header label `kid`.
const HEADER_LABEL_KID: u64 = 4;
/// COSE header label `vds` — verifiable data structure (RFC 9942).
const HEADER_LABEL_VDS: u64 = 395;
/// COSE header label `vdp` — verifiable data proofs (RFC 9942).
const HEADER_LABEL_VDP: u64 = 396;
/// The `alg` value: `EdDSA`.
///
/// `-8` rather than RFC 9864's preferred `-19`, deliberately and for the same
/// reason as the shipped attestation: `go-cose` ships only `-8`, and a receipt
/// no off-the-shelf library verifies is worthless. A move to `-19` is a `v2`
/// matter, triggered when the Go and Python COSE ecosystems both accept it.
const ALG_EDDSA: i64 = -8;
/// The `vds` value: `RFC9162_SHA256 = 1` — the same RFC 6962 SHA-256 tree lys
/// already implements and conformance-tests, so this is a re-encoding of
/// identical semantics rather than a new proof system.
const VDS_RFC9162_SHA256: u64 = 1;
/// The `vdp` proof-type key for an inclusion proof (RFC 9942).
const PROOF_TYPE_INCLUSION: i64 = -1;
/// The `vdp` proof-type key for a consistency proof (RFC 9942 §5.3.1).
///
/// **This label is in the *unprotected* header and therefore not
/// signature-covered.** It says which proof the artifact carries; it does not
/// and cannot say which kind of receipt it is. That is the content type's job —
/// see [`CONSISTENCY_CONTENT_TYPE`].
const PROOF_TYPE_CONSISTENCY: i64 = -2;
/// Build the protected header map
/// `{1: -8, 3: content_type, 4: anchor_public_key, 395: 1}` in canonical key
/// order — 80 bytes for [`CONTENT_TYPE`], 92 for [`CONSISTENCY_CONTENT_TYPE`].
///
/// **`content_type` is the caller's declaration of which artifact kind it is
/// building, and is never read from an artifact.** At verification the header is
/// re-derived through this function from the constant belonging to the code path
/// doing the verifying, so the discriminator that separates an inclusion receipt
/// from a consistency one is not attacker-supplied at all. That property is
/// exactly as strong as each call site passing *its own* constant: pass the
/// wrong one and the two signing preimages coincide, which is the whole of the
/// re-labelling attack. `the_two_receipt_kinds_sign_different_bytes` is the
/// gate on that, and it compares the buckets byte-for-byte rather than merely
/// asserting they differ — two paths drifting together would still be unequal
/// to a third.
pub
/// Build the inner inclusion-proof CBOR — the value the RFC 9942
/// `bstr .cbor` wrapper carries: `[tree_size, leaf_index, [path...]]`.
/// Build the unprotected header map `{396: {-1: [<bstr .cbor proof>]}}`.
///
/// This bucket is **not** signature-covered, and is safe there because the
/// verifier recomputes the root from it and checks the signature against that
/// recomputed value — see the [`super`] module docs.
/// Build the inner consistency-proof CBOR — the value the RFC 9942
/// `bstr .cbor` wrapper carries: `[tree_size_1, tree_size_2, [path...]]`.
///
/// Structurally the inclusion proof's twin, and deliberately written out rather
/// than shared with it: the two arrays hold three fields of the same CBOR shape
/// but *different meaning*, and a shared writer distinguished only by its
/// caller's intent is how the wrong pair of numbers gets encoded under the
/// right label.
/// Build the unprotected header map `{396: {-2: [<bstr .cbor proof>]}}`.
///
/// Not signature-covered, and safe there for the same reason as the inclusion
/// bucket **plus one specific to `-2`**: the verifier derives the newer root
/// from this proof *and* from an older root it supplied itself, then checks the
/// signature against the derived value. The type label `-2` is not what makes
/// the verifier run the consistency procedure — the content type in the
/// protected bucket is.
/// Build the complete tagged `COSE_Sign1` consistency receipt.
///
/// Identical envelope to [`artifact_bytes`] but with
/// [`CONSISTENCY_CONTENT_TYPE`] in the protected bucket — which is the entire
/// separation between the two kinds, since both carry a detached `nil` payload
/// over a 32-byte root.
pub
/// Build the complete tagged `COSE_Sign1` receipt:
/// `18([protected, {396: {...}}, nil, signature])` with all-definite lengths
/// and a detached payload.
pub