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
//! Sign and verify `lys/attestation/v2` `COSE_Sign1` attestations over
//! arbitrary payload bytes.
//!
//! [`sign_attestation`] hashes the payload with SHA-256, captures the
//! current unix-millisecond timestamp, and signs the COSE `Sig_structure`
//! (RFC 9052 §4.4, empty `external_aad`) with the supplied
//! [`Ed25519Identity`]:
//!
//! ```text
//! Sig_structure = ["Signature1", protected, h'', claims]
//! protected = {1: -8 (EdDSA), 3: <v2 content type>, 4: <signer key>}
//! claims = {1: <SHA-256 payload hash>, 2: <unix-ms timestamp>}
//! ```
//!
//! [`verify_attestation`] recomputes the digest of the candidate payload,
//! compares it against `attestation.payload_hash`, rebuilds the
//! `Sig_structure` from the attestation's own fields, and verifies the
//! Ed25519 signature against `attestation.signer_public_key` (strict
//! verification). Any mismatch — wrong payload, tampered signature,
//! tampered timestamp, or wrong signer key — collapses to
//! [`TrustError::InvalidSignature`].
//!
//! Verification is v2-only: the COSE `Sig_structure` above is the sole
//! accepted signing scheme. A signature over anything else — the deleted v1
//! preimage (`lys/attestation/v1 ‖ timestamp_le ‖ hash`), a bare payload
//! hash, raw payload bytes — is rejected. There is no fallback path.
//!
//! Two properties fall out of the construction:
//!
//! - **Every field is authenticated.** The signer key (protected `kid`),
//! the payload hash, and the timestamp all ride inside the signed bytes;
//! none can be altered after signing without invalidating the signature.
//! - **Domain separation.** The signed message always begins
//! `0x84 0x6A "Signature1"`, which is byte-0 disjoint from every other
//! signing context in this crate: the old v1 preimage began `0x6C`
//! (deleted), signed-note bodies are valid UTF-8 (`0x84` is a UTF-8
//! continuation byte, impossible at position 0), and X.509 TBS bytes
//! begin `0x30` (DER SEQUENCE). Among future lys COSE artifacts the
//! protected content type — itself signature-covered and pinned by the
//! verifier — separates the v2 attestation. The raw
//! [`Ed25519Identity::sign`] primitive itself stays unprefixed by
//! necessity: the CA path signs exact X.509 TBS bytes through it, and
//! those bytes must not be altered. Any raw-sign caller whose message
//! could start with a valid `Sig_structure` encoding would need its own
//! separation; within this crate no such caller exists.
//!
//! The signed claims embed the 32-byte hash, not the raw payload. This
//! keeps the signing input fixed-size and uniform regardless of payload
//! length. Consumers that need to attest to large payloads pass the bytes
//! once and let `sign_attestation` produce the canonical hash.
use Utc;
use ;
use crateAttestation;
use crateencoding;
use cratecbor;
use crate;
use cratebytes_eq_no_early_exit as bytes_eq;
use crateEd25519Identity;
/// Hash `payload` with SHA-256, capture the current timestamp, sign the
/// COSE `Sig_structure` with `signing_key`, and package the result as an
/// [`Attestation`].
///
/// The signature covers `["Signature1", protected, h'', claims]` — the
/// protected bucket carries the `EdDSA` algorithm, the v2 content type, and
/// the signer's public key; the claims carry the payload digest and the
/// unix-millisecond timestamp. The original payload bytes are not stored
/// on the attestation.
///
/// `sign_attestation` is infallible: `Utc::now().timestamp_millis()` is
/// total over the representable date range, the hand encoder is total over
/// the field types, and Ed25519 deterministic signing has no failure mode
/// in dalek 2.
/// Verify that `attestation` is a valid signature over `payload` and the
/// attestation's own timestamp by `attestation.signer_public_key`.
///
/// # ⚠️ This answers "is this self-consistent", NOT "is this from someone I trust"
///
/// **The key it verifies against is the key the artifact carries.** An attacker
/// signs an attestation over any payload with their own key, puts their own
/// public key in the artifact, and this function returns `Ok`. The signature is
/// cryptographically perfect and it vouches for nothing — the same trap as a
/// self-signed certificate, where the signature proves the holder signed it and
/// says nothing about who the holder is.
///
/// A caller that has not separately decided which key it expects has performed
/// no authentication. Use [`verify_attestation_by_signer`] to state the
/// expected key and have the comparison done here, where it is done without an
/// early exit and is not distinguishable from a signature failure.
///
/// The check is two-step: the SHA-256 digest of `payload` must equal
/// `attestation.payload_hash`, and the Ed25519 signature must strictly
/// verify against the embedded public key over the `Sig_structure` rebuilt
/// from the attestation's own fields. Because the signed bytes are rebuilt
/// from the struct, a tampered timestamp, hash, or signer key fails
/// signature verification. All failures collapse to
/// [`TrustError::InvalidSignature`] so callers cannot distinguish them by
/// error variant — a tampered payload, a tampered timestamp, and a forged
/// signature all look the same to the verifier, which is the desired
/// property.
///
/// Verification is v2-only: only signatures over the COSE `Sig_structure`
/// are accepted. A signature over the deleted v1 preimage or the bare
/// payload hash is rejected like any other invalid signature.
///
/// # Errors
///
/// Returns [`TrustError::InvalidSignature`] if the recomputed payload hash
/// does not match `attestation.payload_hash`, if the public key is not a
/// valid Ed25519 point, or if the signature does not strictly verify over
/// the rebuilt `Sig_structure` (covering tampered signature bytes and
/// tampered timestamps alike).
// The instrument behind `the_signature_check_still_runs_when_the_signer_is_wrong`.
//
// WHY ONE IS NEEDED: `verify_attestation_by_signer` claims that the signature
// work and the key comparison both always happen. An edit that returns early on
// a signer mismatch produces exactly the same error for exactly the same inputs,
// so every outcome-based test stays green while the claim becomes false. That is
// the defect shape `delegation::verify_delegation` was measured at 32.8× on, and
// the fix there needed a counter for the same reason: a wall-clock assertion is
// flaky under load, and a machine fast enough to blur the difference reports
// success.
//
// Thread-local because Rust's test harness gives each test its own thread, so
// the count is exactly "verifications this test caused" with no serialisation
// and no cross-test races. A global counter would race, and a race here fails
// OPEN.
//
// ⚠️ It is `#[cfg(test)]`, so the property is instrument-guaranteed in test
// builds only — the same limitation already recorded for the delegation counter,
// restated rather than inherited silently.
thread_local!
/// Rebuild the `Sig_structure` and check the signature, recording that it
/// happened.
///
/// Extracted so the count and the verification cannot be separated: an edit that
/// skips the work also skips the increment, which is what makes the counter a
/// measurement rather than a decoration sitting beside one.
/// Verify `attestation` against `payload` **and** that it was signed by
/// `expected_signer_public_key` — the authenticating verifier.
///
/// [`verify_attestation`] establishes only that an attestation is internally
/// consistent, because it verifies against the key the artifact itself carries.
/// This function adds the missing half: the caller says which key it is willing
/// to believe, and an artifact signed by any other key is refused however
/// well-formed it is.
///
/// # Why the expected key is compared here and not by the caller
///
/// A caller writing `if att.signer_public_key == expected` after a successful
/// verify gets the right answer and two wrong properties. It compares with
/// `==`, which may return on the first differing byte; and it runs the
/// comparison only when verification already succeeded, so *whether* the
/// comparison happens is itself observable. Doing it here means the signature
/// work and the key comparison both always happen, and the two failures are
/// indistinguishable in the returned value.
///
/// # Errors
///
/// Returns [`TrustError::InvalidSignature`] — the single value — for a payload
/// mismatch, an invalid signature, **and** a signer that is not
/// `expected_signer_public_key`. A caller must not be able to learn which of
/// the three it hit, since "your signature was fine but you are not who I
/// wanted" tells a prober exactly which key the verifier holds.
/// Parse a tagged `COSE_Sign1` artifact, verify it against `payload`, and
/// require that it was signed by `expected_signer_public_key` — the
/// authenticating counterpart to [`verify_attestation_bytes`].
///
/// # Errors
///
/// Returns [`TrustError::InvalidSignature`] for every failure: malformed or
/// non-canonical artifact, payload mismatch, invalid signature, and unexpected
/// signer are all deliberately indistinguishable.
/// Parse a tagged `COSE_Sign1` artifact and verify it against `payload` in
/// one step, returning the parsed [`Attestation`] on success — the
/// bytes-in convenience mirroring `verify_note`.
///
/// ⚠️ **Self-consistency only** — it verifies against the key the artifact
/// carries. See [`verify_attestation`]'s warning, and
/// [`verify_attestation_bytes_by_signer`] for the authenticating form.
///
/// Equivalent to [`Attestation::from_cose_bytes`] followed by
/// [`verify_attestation`]; used by consumers (such as the CLI) that hold
/// the raw `.cose` file bytes.
///
/// # Errors
///
/// Returns [`TrustError::InvalidSignature`] for every failure — malformed
/// or non-canonical artifact, payload mismatch, and invalid signature are
/// deliberately indistinguishable (non-oracle).
/// SHA-256 digest of `bytes` as a fixed-size 32-byte array.