Skip to main content

confium_patterns/revocation/
revocation_service.rs

1//! The revocation service.
2
3use std::collections::HashMap;
4
5use crate::revocation::revocation_blob::{RevocationBlob, RevocationError};
6use crate::revocation::revocation_submission::{Submission, SubmissionState};
7
8/// The revocation service.
9pub struct RevocationService {
10    service_quorum_id: String,
11    submissions: HashMap<String, Submission>,
12}
13
14impl RevocationService {
15    /// Construct a new service backed by the named quorum.
16    pub fn new(quorum_id: impl Into<String>) -> Self {
17        Self {
18            service_quorum_id: quorum_id.into(),
19            submissions: HashMap::new(),
20        }
21    }
22
23    /// Quorum identifier backing this service.
24    pub fn quorum_id(&self) -> &str {
25        &self.service_quorum_id
26    }
27
28    /// User-side: prepare a revocation blob.
29    ///
30    /// In a real implementation this encrypts (revocation_signature + public_key)
31    /// to the service quorum's threshold public key.
32    pub fn prepare_revocation_blob(
33        &self,
34        user_email: &str,
35        key_fingerprint: &str,
36        revocation_signature: &[u8],
37        public_key: &[u8],
38        encapsulator: &dyn Encapsulator,
39    ) -> Result<RevocationBlob, RevocationError> {
40        let (encapsulated_key, shared_secret) = encapsulator
41            .encapsulate(&self.service_quorum_id)
42            .map_err(|e| RevocationError::Malformed(e))?;
43
44        // Encrypt payload with mock AEAD (XOR shared_secret).
45        let mut payload = Vec::new();
46        payload.extend_from_slice(revocation_signature);
47        payload.extend_from_slice(public_key);
48        let ciphertext: Vec<u8> = payload
49            .iter()
50            .enumerate()
51            .map(|(i, b)| b ^ shared_secret[i % shared_secret.len()])
52            .collect();
53
54        Ok(RevocationBlob {
55            user_email: user_email.into(),
56            key_fingerprint: key_fingerprint.into(),
57            encapsulated_key,
58            ciphertext,
59            nonce: vec![0u8; 12],
60        })
61    }
62
63    /// Service-side: submit a blob for processing.
64    pub fn submit(
65        &mut self,
66        blob: RevocationBlob,
67        verification_token: &str,
68    ) -> Result<String, RevocationError> {
69        if verification_token.is_empty() {
70            return Err(RevocationError::InvalidToken(
71                "token must be non-empty".into(),
72            ));
73        }
74        let id = format!("sub-{}", self.submissions.len() + 1);
75        let submission = Submission::new(id.clone(), blob);
76        self.submissions.insert(id.clone(), submission);
77        Ok(id)
78    }
79
80    /// Service-side: process first confirmation for a submission.
81    pub fn confirm_first(&mut self, submission_id: &str) -> Result<(), RevocationError> {
82        let sub = self
83            .submissions
84            .get_mut(submission_id)
85            .ok_or_else(|| RevocationError::Malformed(format!("unknown submission {submission_id}")))?;
86        sub.confirm_first().map_err(|e| RevocationError::EmailVerificationFailed(e))
87    }
88
89    /// Service-side: process second confirmation (after 24h delay).
90    pub fn confirm_second(&mut self, submission_id: &str) -> Result<(), RevocationError> {
91        let sub = self
92            .submissions
93            .get_mut(submission_id)
94            .ok_or_else(|| RevocationError::Malformed(format!("unknown submission {submission_id}")))?;
95        sub.confirm_second()
96            .map_err(|e| RevocationError::EmailVerificationFailed(e))
97    }
98
99    /// Service-side: process pending submissions that have reached second confirmation.
100    /// Returns the number of submissions processed.
101    pub fn process_pending(&mut self) -> Result<usize, RevocationError> {
102        let mut count = 0;
103        for sub in self.submissions.values_mut() {
104            if sub.state == SubmissionState::SecondConfirmed {
105                sub.mark_decrypted().map_err(|e| RevocationError::ThresholdDecryption(e))?;
106                count += 1;
107            }
108        }
109        Ok(count)
110    }
111
112    /// Service-side: publish a processed submission to keyservers.
113    pub fn publish(&mut self, submission_id: &str) -> Result<(), RevocationError> {
114        let sub = self
115            .submissions
116            .get_mut(submission_id)
117            .ok_or_else(|| RevocationError::Malformed(format!("unknown submission {submission_id}")))?;
118        sub.mark_published()
119            .map_err(|e| RevocationError::Publish(e))
120    }
121
122    /// Number of pending submissions.
123    pub fn pending_count(&self) -> usize {
124        self.submissions
125            .values()
126            .filter(|s| !matches!(s.state, SubmissionState::Published | SubmissionState::Cancelled))
127            .count()
128    }
129
130    /// Lookup a submission by ID.
131    pub fn submission(&self, id: &str) -> Option<&Submission> {
132        self.submissions.get(id)
133    }
134}
135
136/// Encapsulator hook — caller provides concrete threshold KEM impl.
137pub trait Encapsulator {
138    /// Encapsulate to a quorum by ID. Returns (encapsulated_key, shared_secret).
139    fn encapsulate(&self, quorum_id: &str) -> Result<(Vec<u8>, Vec<u8>), String>;
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    struct MockEncapsulator;
147    impl Encapsulator for MockEncapsulator {
148        fn encapsulate(&self, _quorum_id: &str) -> Result<(Vec<u8>, Vec<u8>), String> {
149            Ok((vec![0u8; 32], vec![0u8; 32]))
150        }
151    }
152
153    #[test]
154    fn service_lifecycle_mock() {
155        let mut service = RevocationService::new("tb-revocation-quorum");
156
157        let blob = service
158            .prepare_revocation_blob(
159                "alice@example.com",
160                "ABCD1234",
161                &[1u8, 2, 3, 4],
162                &[5u8, 6, 7, 8],
163                &MockEncapsulator,
164            )
165            .unwrap();
166
167        let id = service.submit(blob, "valid-token").unwrap();
168        service.confirm_first(&id).unwrap();
169
170        // Mock delay elapsed by directly setting state
171        {
172            let sub_mut = service.submissions.get_mut(&id).unwrap();
173            sub_mut.delay_until = Some(chrono::Utc::now() - chrono::Duration::hours(1));
174        }
175
176        service.confirm_second(&id).unwrap();
177        let processed = service.process_pending().unwrap();
178        assert_eq!(processed, 1);
179        service.publish(&id).unwrap();
180
181        assert_eq!(service.submission(&id).unwrap().state, SubmissionState::Published);
182    }
183
184    #[test]
185    fn submit_with_empty_token_fails() {
186        let mut service = RevocationService::new("q");
187        let blob = service
188            .prepare_revocation_blob("a@b", "X", &[], &[], &MockEncapsulator)
189            .unwrap();
190        let result = service.submit(blob, "");
191        assert!(matches!(result, Err(RevocationError::InvalidToken(_))));
192    }
193}