confium_transparency/ers/
mod.rs1#![forbid(unsafe_code)]
14#![allow(missing_docs)] use chrono::{DateTime, Utc};
17use serde::{Deserialize, Serialize};
18use sha2::{Digest, Sha256};
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "snake_case")]
23pub enum HashAlgorithm {
24 Sha256,
26 Sha384,
28 Sha512,
30 Sha3_256,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct EvidenceRecord {
37 pub version: u32,
39 pub digest_algorithms: Vec<HashAlgorithm>,
41 pub archive_time_stamp_sequences: Vec<ArchiveTimeStampSequence>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct ArchiveTimeStampSequence {
48 pub sequence_number: u32,
50 pub reduced_hash_tree: Vec<[u8; 32]>,
52 pub time_stamp: TimeStamp,
54 pub applied_at: DateTime<Utc>,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct TimeStamp {
61 pub tsa_id: String,
63 pub token: Vec<u8>,
65 pub hashed_message: [u8; 32],
67}
68
69#[derive(Debug, thiserror::Error)]
71pub enum ErsError {
72 #[error("hash algorithm mismatch: expected {expected:?}, got {actual:?}")]
74 HashMismatch {
75 expected: HashAlgorithm,
77 actual: HashAlgorithm,
79 },
80 #[error("TSA not trusted: {0}")]
82 UntrustedTsa(String),
83 #[error("hash chain broken at sequence {0}")]
85 BrokenChain(u32),
86 #[error("algorithm {0:?} not supported by the verifier's 32-byte digest model")]
95 UnsupportedAlgorithm(HashAlgorithm),
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct Tsa {
101 pub id: String,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct SequenceCheck {
108 pub sequence_number: u32,
110 pub verified: bool,
112 pub error: Option<String>,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct ErsVerificationResult {
119 pub valid: bool,
121 pub sequences: Vec<SequenceCheck>,
123}
124
125fn hash_with(algorithm: &HashAlgorithm, data: &[u8]) -> Result<[u8; 32], ErsError> {
127 match algorithm {
128 HashAlgorithm::Sha256 => {
129 let d: [u8; 32] = Sha256::digest(data).into();
130 Ok(d)
131 }
132 other => Err(ErsError::UnsupportedAlgorithm(other.clone())),
133 }
134}
135
136pub fn verify_evidence_record(
153 record: &EvidenceRecord,
154 artifact: &[u8],
155 trusted_tsas: &[Tsa],
156) -> Result<ErsVerificationResult, ErsError> {
157 let mut sequences = Vec::with_capacity(record.archive_time_stamp_sequences.len());
158 let mut all_valid = record.digest_algorithms.len() == record.archive_time_stamp_sequences.len();
159 let mut prev_applied_at: Option<DateTime<Utc>> = None;
160
161 for (i, seq) in record.archive_time_stamp_sequences.iter().enumerate() {
162 let algorithm = record.digest_algorithms.get(i);
163 let mut err = match algorithm {
164 None => Some("no digest algorithm declared for this sequence".to_string()),
165 Some(alg) => match hash_with(alg, artifact) {
166 Ok(digest) if seq.time_stamp.hashed_message == digest => None,
167 Ok(_) => Some(format!("artifact digest mismatch under {alg:?}")),
168 Err(e) => Some(e.to_string()),
169 },
170 };
171
172 if err.is_none() && !trusted_tsas.iter().any(|t| t.id == seq.time_stamp.tsa_id) {
173 err = Some(format!("untrusted TSA: {}", seq.time_stamp.tsa_id));
174 }
175
176 if err.is_none() {
177 if let Some(prev) = prev_applied_at {
178 if seq.applied_at < prev {
179 err = Some("timestamp went backwards".to_string());
180 }
181 }
182 }
183
184 prev_applied_at = Some(seq.applied_at);
185 let verified = err.is_none();
186 if !verified {
187 all_valid = false;
188 }
189 sequences.push(SequenceCheck {
190 sequence_number: seq.sequence_number,
191 verified,
192 error: err,
193 });
194 }
195
196 Ok(ErsVerificationResult {
197 valid: all_valid,
198 sequences,
199 })
200}
201
202pub fn build_initial_evidence_record(
204 artifact_hash: [u8; 32],
205 algorithm: HashAlgorithm,
206 tsa_id: impl Into<String>,
207 timestamp_token: Vec<u8>,
208) -> EvidenceRecord {
209 let ts = TimeStamp {
210 tsa_id: tsa_id.into(),
211 token: timestamp_token,
212 hashed_message: artifact_hash,
213 };
214 let seq = ArchiveTimeStampSequence {
215 sequence_number: 0,
216 reduced_hash_tree: vec![artifact_hash],
217 time_stamp: ts,
218 applied_at: Utc::now(),
219 };
220 EvidenceRecord {
221 version: 1,
222 digest_algorithms: vec![algorithm],
223 archive_time_stamp_sequences: vec![seq],
224 }
225}
226
227pub fn renew_evidence_record(
230 existing: &mut EvidenceRecord,
231 new_algorithm: HashAlgorithm,
232 new_artifact_hash: [u8; 32],
233 tsa_id: impl Into<String>,
234 timestamp_token: Vec<u8>,
235) {
236 let next_seq = existing.archive_time_stamp_sequences.len() as u32;
237 let ts = TimeStamp {
238 tsa_id: tsa_id.into(),
239 token: timestamp_token,
240 hashed_message: new_artifact_hash,
241 };
242 let seq = ArchiveTimeStampSequence {
243 sequence_number: next_seq,
244 reduced_hash_tree: vec![new_artifact_hash],
245 time_stamp: ts,
246 applied_at: Utc::now(),
247 };
248 existing.digest_algorithms.push(new_algorithm);
249 existing.archive_time_stamp_sequences.push(seq);
250}
251
252pub fn renewal_count(record: &EvidenceRecord) -> u32 {
254 record.archive_time_stamp_sequences.len() as u32
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
262 fn build_initial_record() {
263 let r = build_initial_evidence_record(
264 [1u8; 32],
265 HashAlgorithm::Sha256,
266 "tsa.example.com",
267 vec![0u8; 100],
268 );
269 assert_eq!(renewal_count(&r), 1);
270 assert_eq!(r.digest_algorithms, vec![HashAlgorithm::Sha256]);
271 }
272
273 #[test]
274 fn renew_adds_sequence() {
275 let mut r = build_initial_evidence_record([1u8; 32], HashAlgorithm::Sha256, "tsa", vec![]);
276 renew_evidence_record(&mut r, HashAlgorithm::Sha384, [2u8; 32], "tsa2", vec![]);
277 assert_eq!(renewal_count(&r), 2);
278 assert_eq!(r.digest_algorithms.len(), 2);
279 }
280
281 fn sha256(data: &[u8]) -> [u8; 32] {
282 use sha2::Digest;
283 Sha256::digest(data).into()
284 }
285
286 fn trusted(id: &str) -> Vec<Tsa> {
287 vec![Tsa { id: id.to_string() }]
288 }
289
290 #[test]
291 fn verify_accepts_valid_initial_record() {
292 let artifact = b"calibration report 2026";
293 let digest = sha256(artifact);
294 let r = build_initial_evidence_record(digest, HashAlgorithm::Sha256, "tsa", vec![0u8; 100]);
295 let res = verify_evidence_record(&r, artifact, &trusted("tsa")).unwrap();
296 assert!(res.valid);
297 assert_eq!(res.sequences.len(), 1);
298 assert!(res.sequences[0].verified);
299 }
300
301 #[test]
302 fn verify_rejects_wrong_artifact() {
303 let digest = sha256(b"real artifact");
304 let r = build_initial_evidence_record(digest, HashAlgorithm::Sha256, "tsa", vec![]);
305 let res = verify_evidence_record(&r, b"tampered artifact", &trusted("tsa")).unwrap();
306 assert!(!res.valid);
307 assert!(
308 res.sequences[0]
309 .error
310 .as_deref()
311 .unwrap()
312 .contains("digest mismatch")
313 );
314 }
315
316 #[test]
317 fn verify_rejects_untrusted_tsa() {
318 let artifact = b"artifact";
319 let r = build_initial_evidence_record(
320 sha256(artifact),
321 HashAlgorithm::Sha256,
322 "rogue-tsa",
323 vec![],
324 );
325 let res = verify_evidence_record(&r, artifact, &trusted("good-tsa")).unwrap();
326 assert!(!res.valid);
327 assert!(
328 res.sequences[0]
329 .error
330 .as_deref()
331 .unwrap()
332 .contains("untrusted TSA")
333 );
334 }
335
336 #[test]
337 fn verify_rejects_backwards_timestamps() {
338 let artifact = b"artifact";
339 let mut r =
340 build_initial_evidence_record(sha256(artifact), HashAlgorithm::Sha256, "tsa", vec![]);
341 let earlier = Utc::now() - chrono::Duration::days(365);
343 r.archive_time_stamp_sequences
344 .push(ArchiveTimeStampSequence {
345 sequence_number: 1,
346 reduced_hash_tree: vec![sha256(artifact)],
347 time_stamp: TimeStamp {
348 tsa_id: "tsa".into(),
349 token: vec![],
350 hashed_message: sha256(artifact),
351 },
352 applied_at: earlier,
353 });
354 r.digest_algorithms.push(HashAlgorithm::Sha256);
355 let res = verify_evidence_record(&r, artifact, &trusted("tsa")).unwrap();
356 assert!(!res.valid);
357 assert!(
358 res.sequences[1]
359 .error
360 .as_deref()
361 .unwrap()
362 .contains("backwards")
363 );
364 }
365
366 #[test]
367 fn verify_rejects_algorithm_count_mismatch() {
368 let artifact = b"artifact";
369 let mut r =
370 build_initial_evidence_record(sha256(artifact), HashAlgorithm::Sha256, "tsa", vec![]);
371 r.digest_algorithms.clear();
372 let res = verify_evidence_record(&r, artifact, &trusted("tsa")).unwrap();
373 assert!(!res.valid);
374 assert!(
375 res.sequences[0]
376 .error
377 .as_deref()
378 .unwrap()
379 .contains("no digest algorithm")
380 );
381 }
382
383 #[test]
384 fn verify_reports_unsupported_algorithm_honestly() {
385 let artifact = b"artifact";
386 let mut r =
387 build_initial_evidence_record(sha256(artifact), HashAlgorithm::Sha256, "tsa", vec![]);
388 renew_evidence_record(&mut r, HashAlgorithm::Sha384, [9u8; 32], "tsa", vec![]);
389 let res = verify_evidence_record(&r, artifact, &trusted("tsa")).unwrap();
390 assert!(!res.valid);
391 assert!(res.sequences[0].verified);
393 assert!(
394 res.sequences[1]
395 .error
396 .as_deref()
397 .unwrap()
398 .contains("not supported")
399 );
400 }
401}