1use crate::codec::{decode_evidence, encode_evidence};
4use crate::crypto::{hash_sha256, sign_evidence_cose, verify_evidence_cose, EvidenceSigner};
5use crate::error::{Error, Result};
6use crate::rfc::{
7 AttestationTier, Checkpoint, DocumentRef, EvidencePacket, HashAlgorithm, HashValue,
8};
9use cpop_jitter::{EntropySource, PhysJitter};
10use ed25519_dalek::VerifyingKey;
11use rand::rngs::OsRng;
12use rand::RngCore;
13use std::time::{SystemTime, UNIX_EPOCH};
14
15fn hash_document_ref(doc: &DocumentRef) -> Result<HashValue> {
16 let mut buf = Vec::new();
17 ciborium::into_writer(doc, &mut buf)
18 .map_err(|e| Error::Protocol(format!("CBOR encode document-ref: {e}")))?;
19 Ok(hash_sha256(&buf))
20}
21
22fn now_millis() -> Result<u64> {
23 Ok(SystemTime::now()
24 .duration_since(UNIX_EPOCH)
25 .map_err(|e| Error::Protocol(format!("system clock error: {}", e)))?
26 .as_millis() as u64)
27}
28
29pub struct Builder {
31 version: u32,
32 profile_uri: String,
33 packet_id: [u8; 16],
34 created: u64,
35 document: DocumentRef,
36 checkpoints: Vec<Checkpoint>,
37 last_checkpoint_hash: HashValue,
38 signer: Box<dyn EvidenceSigner>,
39 jitter: PhysJitter,
40 attestation_tier: AttestationTier,
41 baseline_verification: Option<crate::baseline::BaselineVerification>,
42}
43
44impl Builder {
45 pub fn new(document: DocumentRef, signer: Box<dyn EvidenceSigner>) -> Result<Self> {
46 let mut packet_id = [0u8; 16];
47 OsRng.fill_bytes(&mut packet_id);
48
49 let now = now_millis()?;
50
51 let initial_hash = hash_document_ref(&document)?;
52
53 Ok(Self {
54 version: 1,
55 profile_uri: "urn:ietf:params:pop:profile:1.0".to_string(),
56 packet_id,
57 created: now,
58 document,
59 checkpoints: Vec::new(),
60 last_checkpoint_hash: initial_hash,
61 signer,
62 jitter: PhysJitter::new(1), attestation_tier: AttestationTier::SoftwareOnly,
64 baseline_verification: None,
65 })
66 }
67
68 pub fn with_attestation_tier(mut self, tier: AttestationTier) -> Self {
69 self.attestation_tier = tier;
70 self
71 }
72
73 pub fn with_baseline_verification(mut self, bv: crate::baseline::BaselineVerification) -> Self {
74 self.baseline_verification = Some(bv);
75 self
76 }
77
78 pub fn add_checkpoint(&mut self, content: &[u8], char_count: u64) -> Result<()> {
80 let now = now_millis()?;
81
82 let sequence = self.checkpoints.len() as u64;
83 let mut checkpoint_id = [0u8; 16];
84 OsRng.fill_bytes(&mut checkpoint_id);
85
86 let content_hash = hash_sha256(content);
87
88 let entropy = self
89 .jitter
90 .sample(content)
91 .map_err(|e| Error::Crypto(format!("PhysJitter sampling failed: {}", e)))?;
92
93 let checkpoint_hash = crate::crypto::compute_causality_lock_v2(
95 &self.packet_id,
96 &self.last_checkpoint_hash.digest,
97 &content_hash.digest,
98 &entropy.hash,
99 )?;
100
101 let checkpoint = Checkpoint {
102 sequence,
103 checkpoint_id: checkpoint_id.to_vec(),
104 timestamp: now,
105 content_hash,
106 char_count,
107 prev_hash: self.last_checkpoint_hash.clone(),
108 checkpoint_hash: checkpoint_hash.clone(),
109 jitter_hash: Some(HashValue {
110 algorithm: HashAlgorithm::Sha256,
111 digest: entropy.hash.to_vec(),
112 }),
113 };
114
115 self.last_checkpoint_hash = checkpoint_hash;
116 self.checkpoints.push(checkpoint);
117
118 Ok(())
119 }
120
121 pub fn finalize(self) -> Result<Vec<u8>> {
123 let packet = EvidencePacket {
124 version: self.version,
125 profile_uri: self.profile_uri,
126 packet_id: self.packet_id.to_vec(),
127 created: self.created,
128 document: self.document,
129 checkpoints: self.checkpoints,
130 attestation_tier: Some(self.attestation_tier),
131 baseline_verification: self.baseline_verification,
132 };
133
134 let encoded = encode_evidence(&packet)?;
135 sign_evidence_cose(&encoded, self.signer.as_ref())
136 }
137}
138
139pub struct Verifier {
141 verifying_key: VerifyingKey,
142}
143
144impl Verifier {
145 pub fn new(verifying_key: VerifyingKey) -> Self {
146 Self { verifying_key }
147 }
148
149 pub fn verify(&self, cose_data: &[u8]) -> Result<EvidencePacket> {
151 let payload = verify_evidence_cose(cose_data, &self.verifying_key)?;
152 let packet = decode_evidence(&payload)?;
153 self.validate_structure(&packet)?;
154
155 let mut last_hash = hash_document_ref(&packet.document)?;
156
157 for (i, checkpoint) in packet.checkpoints.iter().enumerate() {
158 if checkpoint.sequence != i as u64 {
159 return Err(Error::Validation(format!(
160 "Sequence mismatch at index {}: expected {}, got {}",
161 i, i, checkpoint.sequence
162 )));
163 }
164
165 if !checkpoint.prev_hash.ct_eq(&last_hash) {
166 return Err(Error::Validation(format!(
167 "Causality chain broken at sequence {}: prev_hash mismatch",
168 checkpoint.sequence
169 )));
170 }
171
172 let expected_hash = if let Some(ref jitter) = checkpoint.jitter_hash {
173 crate::crypto::compute_causality_lock_v2(
174 &packet.packet_id,
175 &last_hash.digest,
176 &checkpoint.content_hash.digest,
177 &jitter.digest,
178 )?
179 } else {
180 crate::crypto::compute_causality_lock(
181 &packet.packet_id,
182 &last_hash.digest,
183 &checkpoint.content_hash.digest,
184 )?
185 };
186
187 if !checkpoint.checkpoint_hash.ct_eq(&expected_hash) {
188 return Err(Error::Validation(format!(
189 "Causality chain broken at sequence {}: checkpoint_hash mismatch",
190 checkpoint.sequence
191 )));
192 }
193
194 last_hash = expected_hash;
195 }
196
197 self.validate_temporal_consistency(&packet)?;
198
199 if let Some(ref bv) = packet.baseline_verification {
200 self.validate_baseline_verification(bv)?;
201 }
202
203 Ok(packet)
204 }
205
206 fn validate_baseline_verification(
210 &self,
211 bv: &crate::baseline::BaselineVerification,
212 ) -> Result<()> {
213 if let Some(ref digest) = bv.digest {
214 let pubkey_hash = hash_sha256(self.verifying_key.as_bytes());
215 if digest.identity_fingerprint != pubkey_hash.digest {
216 return Err(Error::Validation(
217 "Baseline digest identity_fingerprint does not match signer public key"
218 .to_string(),
219 ));
220 }
221
222 if bv.digest_signature.is_none() {
223 return Err(Error::Validation(
224 "Baseline digest present but digest_signature is missing".to_string(),
225 ));
226 }
227 }
228 Ok(())
229 }
230
231 fn validate_structure(&self, packet: &EvidencePacket) -> Result<()> {
232 const EXPECTED_PROFILE_URI: &str = "urn:ietf:params:pop:profile:1.0";
233 if packet.profile_uri != EXPECTED_PROFILE_URI {
234 return Err(Error::Validation(format!(
235 "Invalid profile_uri: expected \"{}\", got \"{}\"",
236 EXPECTED_PROFILE_URI, packet.profile_uri
237 )));
238 }
239
240 if packet.packet_id.len() != 16 {
241 return Err(Error::Validation(format!(
242 "Invalid packet_id length: expected 16, got {}",
243 packet.packet_id.len()
244 )));
245 }
246
247 if !packet.document.content_hash.validate() {
248 return Err(Error::Validation(
249 "Document content_hash digest length does not match algorithm".to_string(),
250 ));
251 }
252
253 const MAX_FILENAME_LEN: usize = 256;
254 if let Some(ref filename) = packet.document.filename {
255 if filename.len() > MAX_FILENAME_LEN {
256 return Err(Error::Validation(format!(
257 "Document filename too long: {} bytes exceeds limit of {}",
258 filename.len(),
259 MAX_FILENAME_LEN
260 )));
261 }
262 }
263
264 const MAX_CHECKPOINTS: usize = 100_000;
265 if packet.checkpoints.len() > MAX_CHECKPOINTS {
266 return Err(Error::Validation(format!(
267 "Too many checkpoints: {} exceeds limit of {}",
268 packet.checkpoints.len(),
269 MAX_CHECKPOINTS
270 )));
271 }
272
273 for checkpoint in &packet.checkpoints {
274 if checkpoint.checkpoint_id.len() != 16 {
275 return Err(Error::Validation(format!(
276 "Invalid checkpoint_id length at sequence {}: expected 16, got {}",
277 checkpoint.sequence,
278 checkpoint.checkpoint_id.len()
279 )));
280 }
281 if !checkpoint.content_hash.validate() {
282 return Err(Error::Validation(format!(
283 "Invalid content_hash at sequence {}: digest length mismatch",
284 checkpoint.sequence
285 )));
286 }
287 if !checkpoint.prev_hash.validate() {
288 return Err(Error::Validation(format!(
289 "Invalid prev_hash at sequence {}: digest length mismatch",
290 checkpoint.sequence
291 )));
292 }
293 if !checkpoint.checkpoint_hash.validate() {
294 return Err(Error::Validation(format!(
295 "Invalid checkpoint_hash at sequence {}: digest length mismatch",
296 checkpoint.sequence
297 )));
298 }
299 if let Some(ref jitter) = checkpoint.jitter_hash {
300 if !jitter.validate() {
301 return Err(Error::Validation(format!(
302 "Invalid jitter_hash at sequence {}: digest length mismatch",
303 checkpoint.sequence
304 )));
305 }
306 }
307 }
308
309 Ok(())
310 }
311
312 fn validate_temporal_consistency(&self, packet: &EvidencePacket) -> Result<()> {
313 if packet.checkpoints.is_empty() {
314 return Ok(());
315 }
316
317 let mut last_ts = packet.created;
318 let mut intervals = Vec::with_capacity(packet.checkpoints.len().saturating_sub(1));
319
320 for checkpoint in &packet.checkpoints {
321 if checkpoint.timestamp < last_ts {
322 return Err(Error::Validation(format!(
323 "Temporal anomaly: checkpoint {} timestamp is before previous",
324 checkpoint.sequence
325 )));
326 }
327
328 if checkpoint.sequence > 0 {
329 intervals.push(checkpoint.timestamp - last_ts);
330 }
331 last_ts = checkpoint.timestamp;
332 }
333
334 if intervals.len() >= 3 {
336 let first = intervals[0];
337 if intervals.iter().all(|&x| x == first) {
338 return Err(Error::Validation(
339 "Adversarial collapse detected: non-human timing uniformity".to_string(),
340 ));
341 }
342 }
343
344 Ok(())
345 }
346}