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}