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(RevocationError::Malformed)?;
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.submissions.get_mut(submission_id).ok_or_else(|| {
83            RevocationError::Malformed(format!("unknown submission {submission_id}"))
84        })?;
85        sub.confirm_first()
86            .map_err(RevocationError::EmailVerificationFailed)
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.submissions.get_mut(submission_id).ok_or_else(|| {
92            RevocationError::Malformed(format!("unknown submission {submission_id}"))
93        })?;
94        sub.confirm_second()
95            .map_err(RevocationError::EmailVerificationFailed)
96    }
97
98    /// Service-side: process pending submissions that have reached second confirmation.
99    /// Returns the number of submissions processed.
100    pub fn process_pending(&mut self) -> Result<usize, RevocationError> {
101        let mut count = 0;
102        for sub in self.submissions.values_mut() {
103            if sub.state == SubmissionState::SecondConfirmed {
104                sub.mark_decrypted()
105                    .map_err(RevocationError::ThresholdDecryption)?;
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.submissions.get_mut(submission_id).ok_or_else(|| {
115            RevocationError::Malformed(format!("unknown submission {submission_id}"))
116        })?;
117        sub.mark_published().map_err(RevocationError::Publish)
118    }
119
120    /// Number of pending submissions.
121    pub fn pending_count(&self) -> usize {
122        self.submissions
123            .values()
124            .filter(|s| {
125                !matches!(
126                    s.state,
127                    SubmissionState::Published | SubmissionState::Cancelled
128                )
129            })
130            .count()
131    }
132
133    /// Lookup a submission by ID.
134    pub fn submission(&self, id: &str) -> Option<&Submission> {
135        self.submissions.get(id)
136    }
137}
138
139/// Encapsulator hook — caller provides concrete threshold KEM impl.
140pub trait Encapsulator {
141    /// Encapsulate to a quorum by ID. Returns (encapsulated_key, shared_secret).
142    fn encapsulate(&self, quorum_id: &str) -> Result<(Vec<u8>, Vec<u8>), String>;
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    struct MockEncapsulator;
150    impl Encapsulator for MockEncapsulator {
151        fn encapsulate(&self, _quorum_id: &str) -> Result<(Vec<u8>, Vec<u8>), String> {
152            Ok((vec![0u8; 32], vec![0u8; 32]))
153        }
154    }
155
156    #[test]
157    fn service_lifecycle_mock() {
158        let mut service = RevocationService::new("tb-revocation-quorum");
159
160        let blob = service
161            .prepare_revocation_blob(
162                "alice@example.com",
163                "ABCD1234",
164                &[1u8, 2, 3, 4],
165                &[5u8, 6, 7, 8],
166                &MockEncapsulator,
167            )
168            .unwrap();
169
170        let id = service.submit(blob, "valid-token").unwrap();
171        service.confirm_first(&id).unwrap();
172
173        // Mock delay elapsed by directly setting state
174        {
175            let sub_mut = service.submissions.get_mut(&id).unwrap();
176            sub_mut.delay_until = Some(chrono::Utc::now() - chrono::Duration::hours(1));
177        }
178
179        service.confirm_second(&id).unwrap();
180        let processed = service.process_pending().unwrap();
181        assert_eq!(processed, 1);
182        service.publish(&id).unwrap();
183
184        assert_eq!(
185            service.submission(&id).unwrap().state,
186            SubmissionState::Published
187        );
188    }
189
190    #[test]
191    fn submit_with_empty_token_fails() {
192        let mut service = RevocationService::new("q");
193        let blob = service
194            .prepare_revocation_blob("a@b", "X", &[], &[], &MockEncapsulator)
195            .unwrap();
196        let result = service.submit(blob, "");
197        assert!(matches!(result, Err(RevocationError::InvalidToken(_))));
198    }
199}