1use serde::{Deserialize, Serialize};
4use sha2::{Digest, Sha256};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct IbeParams {
11 pub master_pubkey_hex: String,
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct IbeCiphertext {
17 pub identity: String,
18 pub ciphertext_hex: String,
19}
20
21pub fn ibe_encrypt(params: &IbeParams, identity: &str, data: &[u8]) -> IbeCiphertext {
23 let mut h = Sha256::new();
25 h.update(b"ibe-key");
26 h.update(¶ms.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
40pub 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#[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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct AcmeAccount {
136 pub contact: String,
137 pub status: String,
138 pub orders: Vec<String>,
139}
140
141#[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#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct AcmeChallenge {
154 pub challenge_type: String,
155 pub token: String,
156 pub status: String,
157}
158
159pub 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 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 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 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 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 #[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(¶ms, "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 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(¶ms, "alice", b"data");
243 let ct2 = ibe_encrypt(¶ms, "bob", b"data");
244 assert_ne!(ct1.ciphertext_hex, ct2.ciphertext_hex);
245 }
246
247 #[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 #[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}