auths_core/trust/continuity.rs
1//! KEL continuity checking trait for key rotation verification.
2//!
3//! This module defines the trait that `auths-id` implements to verify
4//! rotation continuity without `auths-core` depending on `auths-id`.
5
6/// Proof that a key rotation is valid from a known state to a new state.
7///
8/// Returned by implementors of [`KelContinuityChecker`]. The trust module
9/// consumes this without knowing anything about KEL internals.
10#[derive(Debug, Clone)]
11pub struct RotationProof {
12 /// The new public key bytes (raw Ed25519, 32 bytes).
13 pub new_public_key: Vec<u8>,
14
15 /// The new KEL tip SAID after the rotation chain.
16 pub new_kel_tip: String,
17
18 /// The new sequence number.
19 pub new_sequence: u64,
20}
21
22/// Trait for verifying rotation continuity from a pinned state to a presented key.
23///
24/// Implemented by `auths-id` (which owns KEL types). The trust module in
25/// `auths-core` calls this trait without importing `auths-id`.
26///
27/// # Implementation Requirements
28///
29/// The implementation must:
30/// 1. Locate the event with SAID == `pinned_tip_said` in the KEL.
31/// 2. Replay **forward from that event** (not from inception), verifying:
32/// - Hash chain linkage (each event's `p` matches predecessor's `d`).
33/// - Sequence ordering (strict monotonic increment).
34/// - Pre-rotation commitment satisfaction for rotation events.
35/// - Event signatures.
36/// 3. Confirm the resulting key state's current key matches `presented_pk`.
37///
38/// # Return Values
39///
40/// - `Ok(Some(proof))` if continuity is verified.
41/// - `Ok(None)` if the pinned tip is not found or the chain doesn't lead to the presented key.
42/// - `Err` on internal errors (corrupt KEL, deserialization failure).
43pub trait KelContinuityChecker {
44 /// Verify that there is a valid, unbroken event chain from `pinned_tip_said`
45 /// to a state whose current key matches `presented_pk`.
46 ///
47 /// # Arguments
48 ///
49 /// * `did` - The DID being verified (e.g., "did:keri:EXq5...")
50 /// * `pinned_tip_said` - The SAID of the event at which we last pinned this identity
51 /// * `presented_pk` - The raw public key bytes presented for verification
52 ///
53 /// # Returns
54 ///
55 /// * `Ok(Some(proof))` - Rotation verified, contains new state to update pin
56 /// * `Ok(None)` - Cannot verify continuity (tip not found, chain broken, key mismatch)
57 /// * `Err(...)` - Internal error (corrupt data, I/O failure)
58 fn verify_rotation_continuity(
59 &self,
60 did: &str,
61 pinned_tip_said: &str,
62 presented_pk: &[u8],
63 ) -> Result<Option<RotationProof>, crate::error::TrustError>;
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 // Mock implementation for testing
71 struct MockChecker {
72 should_verify: bool,
73 proof: Option<RotationProof>,
74 }
75
76 impl KelContinuityChecker for MockChecker {
77 fn verify_rotation_continuity(
78 &self,
79 _did: &str,
80 _pinned_tip_said: &str,
81 _presented_pk: &[u8],
82 ) -> Result<Option<RotationProof>, crate::error::TrustError> {
83 if self.should_verify {
84 Ok(self.proof.clone())
85 } else {
86 Ok(None)
87 }
88 }
89 }
90
91 #[test]
92 fn test_rotation_proof_fields() {
93 let proof = RotationProof {
94 new_public_key: vec![1, 2, 3, 4],
95 new_kel_tip: "ENewTipSaid".to_string(),
96 new_sequence: 5,
97 };
98
99 assert_eq!(proof.new_public_key, vec![1, 2, 3, 4]);
100 assert_eq!(proof.new_kel_tip, "ENewTipSaid");
101 assert_eq!(proof.new_sequence, 5);
102 }
103
104 #[test]
105 fn test_mock_checker_verifies() {
106 let proof = RotationProof {
107 new_public_key: vec![5, 6, 7, 8],
108 new_kel_tip: "ENewTip".to_string(),
109 new_sequence: 2,
110 };
111
112 let checker = MockChecker {
113 should_verify: true,
114 proof: Some(proof.clone()),
115 };
116
117 let result = checker
118 .verify_rotation_continuity("did:keri:ETest", "EOldTip", &[1, 2, 3])
119 .unwrap();
120
121 assert!(result.is_some());
122 let returned_proof = result.unwrap();
123 assert_eq!(returned_proof.new_sequence, 2);
124 }
125
126 #[test]
127 fn test_mock_checker_fails() {
128 let checker = MockChecker {
129 should_verify: false,
130 proof: None,
131 };
132
133 let result = checker
134 .verify_rotation_continuity("did:keri:ETest", "EOldTip", &[1, 2, 3])
135 .unwrap();
136
137 assert!(result.is_none());
138 }
139}