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