1use std::fmt;
23
24use serde::{Deserialize, Serialize};
25
26use crate::digest::Digest;
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(rename_all = "snake_case")]
35pub enum Verdict {
36 Pass,
38 Fail(String),
40 Inconclusive(String),
43}
44
45impl Verdict {
46 pub fn is_pass(&self) -> bool {
50 matches!(self, Self::Pass)
51 }
52
53 pub fn reason(&self) -> Option<&str> {
55 match self {
56 Self::Pass => None,
57 Self::Fail(r) | Self::Inconclusive(r) => Some(r),
58 }
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
67pub struct CheckerId {
68 pub name: String,
70 pub version: u32,
72}
73
74impl CheckerId {
75 pub fn new(name: impl Into<String>, version: u32) -> Self {
77 Self {
78 name: name.into(),
79 version,
80 }
81 }
82}
83
84impl fmt::Display for CheckerId {
88 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89 write!(f, "{}@{}", self.name, self.version)
90 }
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
99#[serde(transparent)]
100pub struct SigningKeyId(pub String);
101
102impl fmt::Display for SigningKeyId {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 f.write_str(&self.0)
105 }
106}
107
108#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
113#[serde(transparent)]
114pub struct Signature(#[serde(with = "sig_bytes")] [u8; 64]);
115
116impl Signature {
117 pub const fn from_bytes(bytes: [u8; 64]) -> Self {
119 Self(bytes)
120 }
121
122 pub const fn as_bytes(&self) -> &[u8; 64] {
124 &self.0
125 }
126}
127
128impl fmt::Debug for Signature {
129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130 use fmt::Write as _;
131 let head = self.0[..6].iter().fold(String::new(), |mut s, b| {
132 let _ = write!(s, "{b:02x}");
133 s
134 });
135 write!(f, "Signature({head}…)")
136 }
137}
138
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
145#[serde(rename_all = "snake_case")]
146pub enum EvidenceKind {
147 Deterministic,
149 Quorum {
151 n: u8,
153 k: u8,
155 },
156 Human {
158 identity: String,
160 },
161}
162
163#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
187pub struct Evidence {
188 pub verdict: Verdict,
190 pub checker: CheckerId,
192 pub inputs: Vec<Digest>,
197 pub signature: Signature,
199 pub key: SigningKeyId,
201 pub kind: EvidenceKind,
203}
204
205impl Evidence {
206 pub fn new(
211 verdict: Verdict,
212 checker: CheckerId,
213 inputs: impl IntoIterator<Item = Digest>,
214 signature: Signature,
215 key: SigningKeyId,
216 kind: EvidenceKind,
217 ) -> Self {
218 let mut inputs: Vec<Digest> = inputs.into_iter().collect();
219 inputs.sort_unstable();
220 inputs.dedup();
221 Self {
222 verdict,
223 checker,
224 inputs,
225 signature,
226 key,
227 kind,
228 }
229 }
230
231 pub fn permits_commit(&self) -> bool {
235 self.verdict.is_pass()
236 }
237
238 pub fn covers(&self, digest: &Digest) -> bool {
243 self.inputs.binary_search(digest).is_ok()
244 }
245
246 pub fn signing_payload(&self) -> Result<Vec<u8>, crate::CanonicalError> {
251 crate::to_canonical_cbor(&(
252 &self.verdict,
253 &self.checker,
254 &self.inputs,
255 &self.key,
256 &self.kind,
257 ))
258 }
259}
260
261mod sig_bytes {
263 use serde::de::{Error as _, Unexpected};
264 use serde::{Deserializer, Serializer};
265
266 pub(super) fn serialize<S: Serializer>(bytes: &[u8; 64], s: S) -> Result<S::Ok, S::Error> {
267 s.serialize_bytes(bytes)
268 }
269
270 pub(super) fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<[u8; 64], D::Error> {
271 struct V;
272 impl<'de> serde::de::Visitor<'de> for V {
273 type Value = [u8; 64];
274 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
275 f.write_str("a 64-byte signature")
276 }
277 fn visit_bytes<E: serde::de::Error>(self, v: &[u8]) -> Result<[u8; 64], E> {
278 <[u8; 64]>::try_from(v)
279 .map_err(|_| E::invalid_value(Unexpected::Bytes(v), &"exactly 64 bytes"))
280 }
281 fn visit_seq<A: serde::de::SeqAccess<'de>>(
282 self,
283 mut seq: A,
284 ) -> Result<[u8; 64], A::Error> {
285 let mut out = Vec::with_capacity(64);
286 while let Some(b) = seq.next_element::<u8>()? {
287 out.push(b);
288 }
289 <[u8; 64]>::try_from(out.as_slice())
290 .map_err(|_| A::Error::invalid_length(out.len(), &"exactly 64 bytes"))
291 }
292 }
293 d.deserialize_bytes(V)
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300 use crate::canonical::Canonical;
301
302 fn evidence(verdict: Verdict) -> Evidence {
303 Evidence::new(
304 verdict,
305 CheckerId::new("cargo-test", 1),
306 [Digest::of(b"patch")],
307 Signature::from_bytes([7u8; 64]),
308 SigningKeyId("host-key-1".into()),
309 EvidenceKind::Deterministic,
310 )
311 }
312
313 #[test]
314 fn carries_verdict_checker_inputs_signature_and_kind() {
315 let e = evidence(Verdict::Pass);
316 assert_eq!(e.verdict, Verdict::Pass);
317 assert_eq!(e.checker, CheckerId::new("cargo-test", 1));
318 assert_eq!(e.inputs, vec![Digest::of(b"patch")]);
319 assert_eq!(e.signature, Signature::from_bytes([7u8; 64]));
320 assert_eq!(e.kind, EvidenceKind::Deterministic);
321 }
322
323 #[test]
324 fn only_pass_permits_a_commit() {
325 assert!(evidence(Verdict::Pass).permits_commit());
326 assert!(!evidence(Verdict::Fail("2 tests failed".into())).permits_commit());
327 assert!(!evidence(Verdict::Inconclusive("timed out".into())).permits_commit());
328 }
329
330 #[test]
331 fn inconclusive_is_not_an_implicit_pass() {
332 let e = evidence(Verdict::Inconclusive("checker crashed".into()));
333 assert!(!e.permits_commit());
334 assert_eq!(e.verdict.reason(), Some("checker crashed"));
335 }
336
337 #[test]
338 fn evidence_cannot_be_repointed_at_other_data() {
339 let e = evidence(Verdict::Pass);
340 assert!(e.covers(&Digest::of(b"patch")));
341 assert!(
342 !e.covers(&Digest::of(b"a different patch")),
343 "passing evidence must not vouch for something it never saw"
344 );
345 }
346
347 #[test]
348 fn input_order_does_not_change_identity() {
349 let a = Evidence::new(
350 Verdict::Pass,
351 CheckerId::new("c", 1),
352 [Digest::of(b"x"), Digest::of(b"y")],
353 Signature::from_bytes([0u8; 64]),
354 SigningKeyId("k".into()),
355 EvidenceKind::Deterministic,
356 );
357 let b = Evidence::new(
358 Verdict::Pass,
359 CheckerId::new("c", 1),
360 [Digest::of(b"y"), Digest::of(b"x")],
361 Signature::from_bytes([0u8; 64]),
362 SigningKeyId("k".into()),
363 EvidenceKind::Deterministic,
364 );
365 assert_eq!(a.digest().unwrap(), b.digest().unwrap());
366 assert!(a.covers(&Digest::of(b"x")));
368 assert!(a.covers(&Digest::of(b"y")));
369 }
370
371 #[test]
372 fn ids_render_for_a_diagnostic() {
373 assert_eq!(
374 CheckerId::new("json-schema", 2).to_string(),
375 "json-schema@2"
376 );
377 assert_eq!(SigningKeyId("host-key-1".into()).to_string(), "host-key-1");
378 }
379
380 #[test]
381 fn kinds_are_distinguishable() {
382 assert_ne!(
383 EvidenceKind::Deterministic,
384 EvidenceKind::Quorum { n: 3, k: 2 }
385 );
386 assert_ne!(
387 EvidenceKind::Quorum { n: 3, k: 2 },
388 EvidenceKind::Human {
389 identity: "alex".into()
390 }
391 );
392 }
393
394 #[test]
395 fn signing_payload_excludes_the_signature_but_covers_everything_else() {
396 let base = evidence(Verdict::Pass);
397
398 let mut resigned = base.clone();
400 resigned.signature = Signature::from_bytes([9u8; 64]);
401 assert_eq!(
402 base.signing_payload().unwrap(),
403 resigned.signing_payload().unwrap()
404 );
405
406 let flipped = evidence(Verdict::Fail("no".into()));
408 assert_ne!(
409 base.signing_payload().unwrap(),
410 flipped.signing_payload().unwrap()
411 );
412
413 let mut rekeyed = base.clone();
414 rekeyed.key = SigningKeyId("other-key".into());
415 assert_ne!(
416 base.signing_payload().unwrap(),
417 rekeyed.signing_payload().unwrap()
418 );
419
420 let mut recovered = base.clone();
421 recovered.inputs = vec![Digest::of(b"other")];
422 assert_ne!(
423 base.signing_payload().unwrap(),
424 recovered.signing_payload().unwrap()
425 );
426 }
427
428 #[test]
429 fn signature_debug_does_not_dump_all_64_bytes() {
430 let d = format!("{:?}", Signature::from_bytes([0xab; 64]));
431 assert!(d.len() < 40, "debug output should be abbreviated: {d}");
432 }
433
434 #[test]
435 fn round_trips_through_canonical_cbor() {
436 let e = evidence(Verdict::Fail("2 tests failed".into()));
437 let bytes = crate::to_canonical_cbor(&e).unwrap();
438 let back: Evidence = crate::from_canonical_cbor(&bytes).unwrap();
439 assert_eq!(e, back);
440 }
441}