#![forbid(unsafe_code)]
#![allow(missing_docs)]
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HashAlgorithm {
Sha256,
Sha384,
Sha512,
Sha3_256,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvidenceRecord {
pub version: u32,
pub digest_algorithms: Vec<HashAlgorithm>,
pub archive_time_stamp_sequences: Vec<ArchiveTimeStampSequence>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArchiveTimeStampSequence {
pub sequence_number: u32,
pub reduced_hash_tree: Vec<[u8; 32]>,
pub time_stamp: TimeStamp,
pub applied_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimeStamp {
pub tsa_id: String,
pub token: Vec<u8>,
pub hashed_message: [u8; 32],
}
#[derive(Debug, thiserror::Error)]
pub enum ErsError {
#[error("hash algorithm mismatch: expected {expected:?}, got {actual:?}")]
HashMismatch {
expected: HashAlgorithm,
actual: HashAlgorithm,
},
#[error("TSA not trusted: {0}")]
UntrustedTsa(String),
#[error("hash chain broken at sequence {0}")]
BrokenChain(u32),
#[error("algorithm {0:?} not supported by the verifier's 32-byte digest model")]
UnsupportedAlgorithm(HashAlgorithm),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tsa {
pub id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SequenceCheck {
pub sequence_number: u32,
pub verified: bool,
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErsVerificationResult {
pub valid: bool,
pub sequences: Vec<SequenceCheck>,
}
fn hash_with(algorithm: &HashAlgorithm, data: &[u8]) -> Result<[u8; 32], ErsError> {
match algorithm {
HashAlgorithm::Sha256 => {
let d: [u8; 32] = Sha256::digest(data).into();
Ok(d)
}
other => Err(ErsError::UnsupportedAlgorithm(other.clone())),
}
}
pub fn verify_evidence_record(
record: &EvidenceRecord,
artifact: &[u8],
trusted_tsas: &[Tsa],
) -> Result<ErsVerificationResult, ErsError> {
let mut sequences = Vec::with_capacity(record.archive_time_stamp_sequences.len());
let mut all_valid = record.digest_algorithms.len() == record.archive_time_stamp_sequences.len();
let mut prev_applied_at: Option<DateTime<Utc>> = None;
for (i, seq) in record.archive_time_stamp_sequences.iter().enumerate() {
let algorithm = record.digest_algorithms.get(i);
let mut err = match algorithm {
None => Some("no digest algorithm declared for this sequence".to_string()),
Some(alg) => match hash_with(alg, artifact) {
Ok(digest) if seq.time_stamp.hashed_message == digest => None,
Ok(_) => Some(format!("artifact digest mismatch under {alg:?}")),
Err(e) => Some(e.to_string()),
},
};
if err.is_none() && !trusted_tsas.iter().any(|t| t.id == seq.time_stamp.tsa_id) {
err = Some(format!("untrusted TSA: {}", seq.time_stamp.tsa_id));
}
if err.is_none() {
if let Some(prev) = prev_applied_at {
if seq.applied_at < prev {
err = Some("timestamp went backwards".to_string());
}
}
}
prev_applied_at = Some(seq.applied_at);
let verified = err.is_none();
if !verified {
all_valid = false;
}
sequences.push(SequenceCheck {
sequence_number: seq.sequence_number,
verified,
error: err,
});
}
Ok(ErsVerificationResult {
valid: all_valid,
sequences,
})
}
pub fn build_initial_evidence_record(
artifact_hash: [u8; 32],
algorithm: HashAlgorithm,
tsa_id: impl Into<String>,
timestamp_token: Vec<u8>,
) -> EvidenceRecord {
let ts = TimeStamp {
tsa_id: tsa_id.into(),
token: timestamp_token,
hashed_message: artifact_hash,
};
let seq = ArchiveTimeStampSequence {
sequence_number: 0,
reduced_hash_tree: vec![artifact_hash],
time_stamp: ts,
applied_at: Utc::now(),
};
EvidenceRecord {
version: 1,
digest_algorithms: vec![algorithm],
archive_time_stamp_sequences: vec![seq],
}
}
pub fn renew_evidence_record(
existing: &mut EvidenceRecord,
new_algorithm: HashAlgorithm,
new_artifact_hash: [u8; 32],
tsa_id: impl Into<String>,
timestamp_token: Vec<u8>,
) {
let next_seq = existing.archive_time_stamp_sequences.len() as u32;
let ts = TimeStamp {
tsa_id: tsa_id.into(),
token: timestamp_token,
hashed_message: new_artifact_hash,
};
let seq = ArchiveTimeStampSequence {
sequence_number: next_seq,
reduced_hash_tree: vec![new_artifact_hash],
time_stamp: ts,
applied_at: Utc::now(),
};
existing.digest_algorithms.push(new_algorithm);
existing.archive_time_stamp_sequences.push(seq);
}
pub fn renewal_count(record: &EvidenceRecord) -> u32 {
record.archive_time_stamp_sequences.len() as u32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_initial_record() {
let r = build_initial_evidence_record(
[1u8; 32],
HashAlgorithm::Sha256,
"tsa.example.com",
vec![0u8; 100],
);
assert_eq!(renewal_count(&r), 1);
assert_eq!(r.digest_algorithms, vec![HashAlgorithm::Sha256]);
}
#[test]
fn renew_adds_sequence() {
let mut r = build_initial_evidence_record([1u8; 32], HashAlgorithm::Sha256, "tsa", vec![]);
renew_evidence_record(&mut r, HashAlgorithm::Sha384, [2u8; 32], "tsa2", vec![]);
assert_eq!(renewal_count(&r), 2);
assert_eq!(r.digest_algorithms.len(), 2);
}
fn sha256(data: &[u8]) -> [u8; 32] {
use sha2::Digest;
Sha256::digest(data).into()
}
fn trusted(id: &str) -> Vec<Tsa> {
vec![Tsa { id: id.to_string() }]
}
#[test]
fn verify_accepts_valid_initial_record() {
let artifact = b"calibration report 2026";
let digest = sha256(artifact);
let r = build_initial_evidence_record(digest, HashAlgorithm::Sha256, "tsa", vec![0u8; 100]);
let res = verify_evidence_record(&r, artifact, &trusted("tsa")).unwrap();
assert!(res.valid);
assert_eq!(res.sequences.len(), 1);
assert!(res.sequences[0].verified);
}
#[test]
fn verify_rejects_wrong_artifact() {
let digest = sha256(b"real artifact");
let r = build_initial_evidence_record(digest, HashAlgorithm::Sha256, "tsa", vec![]);
let res = verify_evidence_record(&r, b"tampered artifact", &trusted("tsa")).unwrap();
assert!(!res.valid);
assert!(
res.sequences[0]
.error
.as_deref()
.unwrap()
.contains("digest mismatch")
);
}
#[test]
fn verify_rejects_untrusted_tsa() {
let artifact = b"artifact";
let r = build_initial_evidence_record(
sha256(artifact),
HashAlgorithm::Sha256,
"rogue-tsa",
vec![],
);
let res = verify_evidence_record(&r, artifact, &trusted("good-tsa")).unwrap();
assert!(!res.valid);
assert!(
res.sequences[0]
.error
.as_deref()
.unwrap()
.contains("untrusted TSA")
);
}
#[test]
fn verify_rejects_backwards_timestamps() {
let artifact = b"artifact";
let mut r =
build_initial_evidence_record(sha256(artifact), HashAlgorithm::Sha256, "tsa", vec![]);
let earlier = Utc::now() - chrono::Duration::days(365);
r.archive_time_stamp_sequences
.push(ArchiveTimeStampSequence {
sequence_number: 1,
reduced_hash_tree: vec![sha256(artifact)],
time_stamp: TimeStamp {
tsa_id: "tsa".into(),
token: vec![],
hashed_message: sha256(artifact),
},
applied_at: earlier,
});
r.digest_algorithms.push(HashAlgorithm::Sha256);
let res = verify_evidence_record(&r, artifact, &trusted("tsa")).unwrap();
assert!(!res.valid);
assert!(
res.sequences[1]
.error
.as_deref()
.unwrap()
.contains("backwards")
);
}
#[test]
fn verify_rejects_algorithm_count_mismatch() {
let artifact = b"artifact";
let mut r =
build_initial_evidence_record(sha256(artifact), HashAlgorithm::Sha256, "tsa", vec![]);
r.digest_algorithms.clear();
let res = verify_evidence_record(&r, artifact, &trusted("tsa")).unwrap();
assert!(!res.valid);
assert!(
res.sequences[0]
.error
.as_deref()
.unwrap()
.contains("no digest algorithm")
);
}
#[test]
fn verify_reports_unsupported_algorithm_honestly() {
let artifact = b"artifact";
let mut r =
build_initial_evidence_record(sha256(artifact), HashAlgorithm::Sha256, "tsa", vec![]);
renew_evidence_record(&mut r, HashAlgorithm::Sha384, [9u8; 32], "tsa", vec![]);
let res = verify_evidence_record(&r, artifact, &trusted("tsa")).unwrap();
assert!(!res.valid);
assert!(res.sequences[0].verified);
assert!(
res.sequences[1]
.error
.as_deref()
.unwrap()
.contains("not supported")
);
}
}