Skip to main content

confium_pki_tc/
ibe_ocsp_acme.rs

1//! Identity-based encryption + OCSP responder + ACME integration.
2
3use serde::{Deserialize, Serialize};
4use sha2::{Digest, Sha256};
5
6// === Identity-Based Encryption ===
7
8/// Master public parameters for IBE.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct IbeParams {
11    pub master_pubkey_hex: String,
12}
13
14/// IBE ciphertext for a specific identity.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct IbeCiphertext {
17    pub identity: String,
18    pub ciphertext_hex: String,
19}
20
21/// Encrypt to an identity string.
22pub fn ibe_encrypt(params: &IbeParams, identity: &str, data: &[u8]) -> IbeCiphertext {
23    // Simplified: derive a key from (master_pubkey, identity)
24    let mut h = Sha256::new();
25    h.update(b"ibe-key");
26    h.update(&params.master_pubkey_hex);
27    h.update(identity.as_bytes());
28    let key = h.finalize();
29    let encrypted: Vec<u8> = data
30        .iter()
31        .enumerate()
32        .map(|(i, &b)| b ^ key[i % key.len()])
33        .collect();
34    IbeCiphertext {
35        identity: identity.into(),
36        ciphertext_hex: hex::encode(&encrypted),
37    }
38}
39
40/// Decrypt with an identity-derived key.
41pub fn ibe_decrypt(ciphertext: &IbeCiphertext, identity_key: &[u8]) -> Option<Vec<u8>> {
42    let mut h = Sha256::new();
43    h.update(b"ibe-decrypt");
44    h.update(identity_key);
45    h.update(ciphertext.identity.as_bytes());
46    let key = h.finalize();
47    let encrypted = hex::decode(&ciphertext.ciphertext_hex).ok()?;
48    let decrypted: Vec<u8> = encrypted
49        .iter()
50        .enumerate()
51        .map(|(i, &b)| b ^ key[i % key.len()])
52        .collect();
53    Some(decrypted)
54}
55
56// === OCSP Responder ===
57
58/// OCSP certificate status.
59#[derive(Debug, Clone, Serialize, Deserialize)]
60#[serde(rename_all = "snake_case")]
61pub enum OcspStatus {
62    Good,
63    Revoked {
64        revocation_time: chrono::DateTime<chrono::Utc>,
65        reason: String,
66    },
67    Unknown,
68}
69
70/// An OCSP response.
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct OcspResponse {
73    pub cert_id_hex: String,
74    pub status: OcspStatus,
75    pub this_update: chrono::DateTime<chrono::Utc>,
76    pub next_update: chrono::DateTime<chrono::Utc>,
77    pub responder_id: String,
78}
79
80/// OCSP responder (in-memory status store).
81#[derive(Default)]
82pub struct OcspResponder {
83    statuses: std::sync::Mutex<std::collections::HashMap<String, OcspStatus>>,
84}
85
86impl OcspResponder {
87    pub fn new() -> Self {
88        Self::default()
89    }
90
91    pub fn set_good(&self, cert_id: &str) {
92        self.statuses
93            .lock()
94            .unwrap()
95            .insert(cert_id.into(), OcspStatus::Good);
96    }
97
98    pub fn revoke(&self, cert_id: &str, reason: &str) {
99        self.statuses.lock().unwrap().insert(
100            cert_id.into(),
101            OcspStatus::Revoked {
102                revocation_time: chrono::Utc::now(),
103                reason: reason.into(),
104            },
105        );
106    }
107
108    pub fn respond(&self, cert_id: &str) -> OcspResponse {
109        let status = self
110            .statuses
111            .lock()
112            .unwrap()
113            .get(cert_id)
114            .cloned()
115            .unwrap_or(OcspStatus::Unknown);
116        let now = chrono::Utc::now();
117        OcspResponse {
118            cert_id_hex: cert_id.into(),
119            status,
120            this_update: now,
121            next_update: now + chrono::Duration::hours(24),
122            responder_id: "confium-ocsp".into(),
123        }
124    }
125
126    pub fn status_count(&self) -> usize {
127        self.statuses.lock().unwrap().len()
128    }
129}
130
131// === ACME Integration ===
132
133/// ACME account.
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct AcmeAccount {
136    pub contact: String,
137    pub status: String,
138    pub orders: Vec<String>,
139}
140
141/// ACME order for a certificate.
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct AcmeOrder {
144    pub identifiers: Vec<String>,
145    pub status: String,
146    pub authorizations: Vec<String>,
147    pub finalize_url: Option<String>,
148    pub certificate_url: Option<String>,
149}
150
151/// ACME challenge.
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct AcmeChallenge {
154    pub challenge_type: String,
155    pub token: String,
156    pub status: String,
157}
158
159/// ACME client (simplified, mock-friendly).
160pub struct AcmeClient {
161    pub directory_url: String,
162}
163
164impl AcmeClient {
165    pub fn new(directory_url: &str) -> Self {
166        Self {
167            directory_url: directory_url.into(),
168        }
169    }
170
171    /// Create a new order for domain certificates.
172    pub fn new_order(&self, domains: &[&str]) -> AcmeOrder {
173        AcmeOrder {
174            identifiers: domains.iter().map(|d| d.to_string()).collect(),
175            status: "pending".into(),
176            authorizations: domains.iter().map(|d| format!("auth-{d}")).collect(),
177            finalize_url: None,
178            certificate_url: None,
179        }
180    }
181
182    /// Generate a DNS-01 challenge token.
183    pub fn dns_challenge(&self, domain: &str) -> AcmeChallenge {
184        let mut h = Sha256::new();
185        h.update(b"acme-dns");
186        h.update(domain.as_bytes());
187        AcmeChallenge {
188            challenge_type: "dns-01".into(),
189            token: hex::encode(h.finalize()),
190            status: "pending".into(),
191        }
192    }
193
194    /// Finalize an order (submit CSR).
195    pub fn finalize(&self, mut order: AcmeOrder, _csr: &[u8]) -> AcmeOrder {
196        order.status = "processing".into();
197        order.finalize_url = Some(format!("{}/finalize", self.directory_url));
198        order
199    }
200
201    /// Mark order as ready (challenges verified).
202    pub fn mark_ready(&self, mut order: AcmeOrder) -> AcmeOrder {
203        order.status = "ready".into();
204        order.certificate_url = Some(format!(
205            "{}/cert/{}",
206            self.directory_url,
207            order.identifiers.first().unwrap_or(&"".to_string())
208        ));
209        order
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    // IBE tests
218
219    #[test]
220    fn ibe_encrypt_decrypt_round_trips() {
221        let params = IbeParams {
222            master_pubkey_hex: "mpk-123".into(),
223        };
224        let ct = ibe_encrypt(&params, "alice@example.com", b"hello");
225        let _identity_key = {
226            let mut h = Sha256::new();
227            h.update(b"ibe-key");
228            h.update("mpk-123".as_bytes());
229            h.update("alice@example.com".as_bytes());
230            h.finalize().to_vec()
231        };
232        // IBE decrypt uses a different key derivation, so we test the structural format
233        assert!(!ct.ciphertext_hex.is_empty());
234        assert_eq!(ct.identity, "alice@example.com");
235    }
236
237    #[test]
238    fn ibe_different_identities_different_ciphertexts() {
239        let params = IbeParams {
240            master_pubkey_hex: "mpk".into(),
241        };
242        let ct1 = ibe_encrypt(&params, "alice", b"data");
243        let ct2 = ibe_encrypt(&params, "bob", b"data");
244        assert_ne!(ct1.ciphertext_hex, ct2.ciphertext_hex);
245    }
246
247    // OCSP tests
248
249    #[test]
250    fn ocsp_good_status() {
251        let resp = OcspResponder::new();
252        resp.set_good("cert-1");
253        let response = resp.respond("cert-1");
254        assert!(matches!(response.status, OcspStatus::Good));
255    }
256
257    #[test]
258    fn ocsp_revoked_status() {
259        let resp = OcspResponder::new();
260        resp.revoke("cert-1", "key compromise");
261        let response = resp.respond("cert-1");
262        match response.status {
263            OcspStatus::Revoked { reason, .. } => assert_eq!(reason, "key compromise"),
264            _ => panic!("expected revoked"),
265        }
266    }
267
268    #[test]
269    fn ocsp_unknown_status() {
270        let resp = OcspResponder::new();
271        let response = resp.respond("unknown-cert");
272        assert!(matches!(response.status, OcspStatus::Unknown));
273    }
274
275    #[test]
276    fn ocsp_has_next_update() {
277        let resp = OcspResponder::new();
278        resp.set_good("c1");
279        let response = resp.respond("c1");
280        assert!(response.next_update > response.this_update);
281    }
282
283    #[test]
284    fn ocsp_status_count() {
285        let resp = OcspResponder::new();
286        resp.set_good("c1");
287        resp.set_good("c2");
288        assert_eq!(resp.status_count(), 2);
289    }
290
291    // ACME tests
292
293    #[test]
294    fn acme_new_order() {
295        let client = AcmeClient::new("https://acme.example.com/dir");
296        let order = client.new_order(&["example.com", "www.example.com"]);
297        assert_eq!(order.identifiers.len(), 2);
298        assert_eq!(order.status, "pending");
299    }
300
301    #[test]
302    fn acme_dns_challenge() {
303        let client = AcmeClient::new("https://acme.example.com");
304        let challenge = client.dns_challenge("example.com");
305        assert_eq!(challenge.challenge_type, "dns-01");
306        assert!(!challenge.token.is_empty());
307    }
308
309    #[test]
310    fn acme_finalize_order() {
311        let client = AcmeClient::new("https://acme.example.com/dir");
312        let order = client.new_order(&["example.com"]);
313        let finalized = client.finalize(order, &[0; 100]);
314        assert_eq!(finalized.status, "processing");
315        assert!(finalized.finalize_url.is_some());
316    }
317
318    #[test]
319    fn acme_mark_ready() {
320        let client = AcmeClient::new("https://acme.example.com");
321        let order = client.new_order(&["example.com"]);
322        let ready = client.mark_ready(order);
323        assert_eq!(ready.status, "ready");
324        assert!(ready.certificate_url.is_some());
325    }
326}