Skip to main content

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}