1#![allow(clippy::too_long_first_doc_paragraph)]
30
31use polyc_crypto::signing_role::JournalAttestationSigner;
32use polyc_mmr::{SignedRoot, VerifiableLog, verify_root_signature_with_trust};
33
34use crate::Event;
35
36pub const MMR_SIGNED_ROOT_KIND: &str = "__mmr_signed_root__";
41
42#[derive(Debug, thiserror::Error)]
44#[non_exhaustive]
45pub enum IntegrityError {
46 #[error("mmr: {0}")]
48 Mmr(#[from] polyc_mmr::MmrError),
49 #[error("malformed signed-root marker at leaf count {leaf_count_hint}: {source}")]
53 MalformedRoot {
54 leaf_count_hint: u64,
57 source: serde_json::Error,
59 },
60 #[error("signed root at leaf count {leaf_count} does not verify under the expected signer")]
64 SignatureInvalid {
65 leaf_count: u64,
67 },
68 #[error(
72 "integrity violation: at leaf count {leaf_count}, replay computed root {computed_root_hex} \
73 but the signed marker recorded {expected_root_hex}"
74 )]
75 RootMismatch {
76 leaf_count: u64,
78 expected_root_hex: String,
80 computed_root_hex: String,
82 },
83}
84
85pub fn extend_and_sign(
101 log: &VerifiableLog,
102 new_events: &[Event],
103 signer: &JournalAttestationSigner,
104) -> Result<Event, IntegrityError> {
105 for event in new_events {
106 log.append(&event.kind, &event.payload)?;
107 }
108 let root = log.sign_root(signer)?;
109 let payload = serde_json::to_vec(&root).expect("SignedRoot serializes");
110 Ok(Event::new(MMR_SIGNED_ROOT_KIND, payload))
111}
112
113pub fn rebuild_from_events(events: &[Event]) -> Result<VerifiableLog, IntegrityError> {
123 let log = VerifiableLog::rebuild(
124 events
125 .iter()
126 .filter(|e| e.kind != MMR_SIGNED_ROOT_KIND)
127 .map(|e| (e.kind.as_str(), e.payload.as_slice())),
128 )?;
129 Ok(log)
130}
131
132pub fn verify_replay(events: &[Event], expected_signer_pk_hex: &str) -> Result<(), IntegrityError> {
147 let public_key = hex::decode(expected_signer_pk_hex)
148 .map_err(|_| IntegrityError::SignatureInvalid { leaf_count: 0 })?;
149 let trust = polyc_crypto::signing_role::RoleTrustSet::<
150 polyc_crypto::signing_role::JournalAttestationRole,
151 >::from_public_keys(vec![public_key])
152 .map_err(|_| IntegrityError::SignatureInvalid { leaf_count: 0 })?;
153 verify_replay_with_trust(events, &trust)
154}
155
156pub fn verify_replay_with_trust(
162 events: &[Event],
163 trust: &polyc_crypto::signing_role::RoleTrustSet<
164 polyc_crypto::signing_role::JournalAttestationRole,
165 >,
166) -> Result<(), IntegrityError> {
167 verify_extension_with_trust(&VerifiableLog::new(), events, trust)
168}
169
170pub fn verify_extension_with_trust(
191 log: &VerifiableLog,
192 events: &[Event],
193 trust: &polyc_crypto::signing_role::RoleTrustSet<
194 polyc_crypto::signing_role::JournalAttestationRole,
195 >,
196) -> Result<(), IntegrityError> {
197 for event in events {
198 if event.kind == MMR_SIGNED_ROOT_KIND {
199 let leaf_count = log.leaf_count()?;
200 let root: SignedRoot = serde_json::from_slice(&event.payload).map_err(|source| {
201 IntegrityError::MalformedRoot {
202 leaf_count_hint: leaf_count,
203 source,
204 }
205 })?;
206 let sig_ok = verify_root_signature_with_trust(&root, trust).unwrap_or(false);
207 if !sig_ok {
208 return Err(IntegrityError::SignatureInvalid { leaf_count });
209 }
210 let computed_root = log.root()?;
211 let computed_root_hex = hex::encode(computed_root.as_ref());
212 if root.leaf_count != leaf_count || root.root_hex != computed_root_hex {
213 return Err(IntegrityError::RootMismatch {
214 leaf_count,
215 expected_root_hex: root.root_hex,
216 computed_root_hex,
217 });
218 }
219 } else {
220 log.append(&event.kind, &event.payload)?;
221 }
222 }
223 Ok(())
224}
225
226#[cfg(test)]
227mod tests {
228 #![allow(clippy::pedantic, clippy::nursery, missing_docs)]
229 use super::*;
230
231 fn signer() -> JournalAttestationSigner {
232 JournalAttestationSigner::from_seed(7)
233 }
234
235 fn pk_hex(signer: &JournalAttestationSigner) -> String {
236 hex::encode(signer.public_key_bytes())
237 }
238
239 #[test]
244 fn mmr_verify_replay_detects_tampered_event() {
245 let log = VerifiableLog::new();
246 let s = signer();
247 let turn_events = vec![
248 Event::new("user_msg", b"what is 2+2?".to_vec()),
249 Event::new("output_msg", b"4".to_vec()),
250 ];
251 let marker = extend_and_sign(&log, &turn_events, &s).expect("sign");
252
253 let mut persisted = turn_events.clone();
254 persisted.push(marker);
255
256 verify_replay(&persisted, &pk_hex(&s)).expect("untampered replay must verify");
258
259 persisted[1].payload[0] ^= 0xFF;
262 let err = verify_replay(&persisted, &pk_hex(&s))
263 .expect_err("tampered replay must report an integrity violation");
264 assert!(
265 matches!(err, IntegrityError::RootMismatch { .. }),
266 "expected a root mismatch, got {err:?}"
267 );
268 }
269
270 #[test]
283 fn the_signed_root_is_blind_to_the_trust_tag() {
284 let s = signer();
285 let tagged = vec![
286 Event::trusted("user_msg", b"a".to_vec()),
287 Event::quarantined("tool_result", b"b".to_vec()),
288 ];
289 let untagged = vec![
290 Event::new("user_msg", b"a".to_vec()),
291 Event::new("tool_result", b"b".to_vec()),
292 ];
293
294 let tagged_tree = rebuild_from_events(&tagged).expect("rebuild tagged");
295 let untagged_tree = rebuild_from_events(&untagged).expect("rebuild untagged");
296 assert_eq!(
297 tagged_tree.root().expect("tagged root"),
298 untagged_tree.root().expect("untagged root"),
299 "the trust tag is not a leaf input, so it cannot move the root"
300 );
301 assert_eq!(
302 tagged_tree.leaf_count().expect("tagged leaves"),
303 untagged_tree.leaf_count().expect("untagged leaves")
304 );
305
306 let marker = extend_and_sign(&VerifiableLog::new(), &tagged, &s).expect("sign tagged");
310 let mut swapped = untagged;
311 swapped.push(marker);
312 verify_replay(&swapped, &pk_hex(&s))
313 .expect("a tag-only difference does not disturb verification");
314 }
315
316 #[test]
317 fn mmr_verify_replay_accepts_multi_turn_untampered_log() {
318 let log = VerifiableLog::new();
319 let s = signer();
320 let mut persisted = Vec::new();
321
322 for turn in 0..3u8 {
323 let events = vec![
324 Event::new("user_msg", vec![turn]),
325 Event::new("output_msg", vec![turn, turn]),
326 ];
327 let marker = extend_and_sign(&log, &events, &s).expect("sign");
328 persisted.extend(events);
329 persisted.push(marker);
330 }
331
332 verify_replay(&persisted, &pk_hex(&s)).expect("three untampered turns must verify");
333 }
334
335 #[test]
336 fn replay_spans_attestation_rotation_only_with_explicit_history() {
337 use polyc_crypto::signing_role::{JournalAttestationRole, RoleTrustSet};
338
339 let first = JournalAttestationSigner::from_seed(71);
340 let second = JournalAttestationSigner::from_seed(72);
341 let log = VerifiableLog::new();
342 let first_events = vec![Event::new("user_msg", b"before".to_vec())];
343 let first_marker = extend_and_sign(&log, &first_events, &first).expect("first root");
344 let second_events = vec![Event::new("output_msg", b"after".to_vec())];
345 let second_marker = extend_and_sign(&log, &second_events, &second).expect("second root");
346 let persisted = [
347 first_events,
348 vec![first_marker],
349 second_events,
350 vec![second_marker],
351 ]
352 .concat();
353
354 let current_only = RoleTrustSet::<JournalAttestationRole>::current(&second);
355 assert!(verify_replay_with_trust(&persisted, ¤t_only).is_err());
356 let history = RoleTrustSet::<JournalAttestationRole>::checked(vec![
357 second.identity(),
358 first.identity(),
359 ])
360 .expect("valid history");
361 verify_replay_with_trust(&persisted, &history)
362 .expect("retired root remains verifiable during rotation overlap");
363 }
364
365 #[test]
366 fn mmr_verify_replay_rejects_root_signed_by_a_different_key() {
367 let log = VerifiableLog::new();
368 let s = signer();
369 let events = vec![Event::new("user_msg", b"hi".to_vec())];
370 let marker = extend_and_sign(&log, &events, &s).expect("sign");
371 let mut persisted = events;
372 persisted.push(marker);
373
374 let other = JournalAttestationSigner::from_seed(999);
375 let err = verify_replay(&persisted, &pk_hex(&other))
376 .expect_err("a root signed under a different key must not verify");
377 assert!(matches!(err, IntegrityError::SignatureInvalid { .. }));
378 }
379
380 #[test]
381 fn mmr_verify_replay_accepts_partition_with_no_signed_roots_yet() {
382 let events = vec![Event::new("user_msg", b"no marker yet".to_vec())];
383 verify_replay(&events, &pk_hex(&signer())).expect("no markers is trivially fine");
384 }
385
386 #[test]
391 fn verifying_a_tail_against_a_running_tree_matches_a_full_replay() {
392 use polyc_crypto::signing_role::{JournalAttestationRole, RoleTrustSet};
393
394 let s = signer();
395 let trust = RoleTrustSet::<JournalAttestationRole>::current(&s);
396 let writer = VerifiableLog::new();
397 let mut persisted = Vec::new();
398 let running = VerifiableLog::new();
399
400 for turn in 0..4u8 {
401 let events = vec![
402 Event::new("user_msg", vec![turn]),
403 Event::new("output_msg", vec![turn, turn]),
404 ];
405 let marker = extend_and_sign(&writer, &events, &s).expect("sign");
406 let mut tail = events;
407 tail.push(marker);
408
409 verify_extension_with_trust(&running, &tail, &trust)
410 .expect("each tail verifies against the tree its predecessors built");
411 persisted.extend(tail);
412 verify_replay_with_trust(&persisted, &trust).expect("and so does the whole partition");
413 assert_eq!(running.root().unwrap(), writer.root().unwrap());
414 assert_eq!(running.leaf_count().unwrap(), writer.leaf_count().unwrap());
415 }
416
417 let events = vec![Event::new("user_msg", b"honest".to_vec())];
421 let marker = extend_and_sign(&writer, &events, &s).expect("sign");
422 let mut tampered = events;
423 tampered[0].payload[0] ^= 0xFF;
424 tampered.push(marker);
425 assert!(matches!(
426 verify_extension_with_trust(&running, &tampered, &trust)
427 .expect_err("a tampered tail must not verify"),
428 IntegrityError::RootMismatch { .. }
429 ));
430 }
431
432 #[test]
433 fn mmr_rebuild_from_events_skips_marker_events() {
434 let log = VerifiableLog::new();
435 let s = signer();
436 let events = vec![
437 Event::new("user_msg", b"a".to_vec()),
438 Event::new("output_msg", b"b".to_vec()),
439 ];
440 let marker = extend_and_sign(&log, &events, &s).expect("sign");
441 let mut persisted = events;
442 persisted.push(marker);
443
444 let rebuilt = rebuild_from_events(&persisted).expect("rebuild");
445 assert_eq!(rebuilt.leaf_count().unwrap(), 2, "markers are not leaves");
446 assert_eq!(rebuilt.root().unwrap(), log.root().unwrap());
447 }
448}