1use crate::{DidError, DidResult};
14use serde::{Deserialize, Serialize};
15use sha2::{Digest, Sha256};
16use std::collections::HashMap;
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub enum RevocationStatus {
23 Valid,
25 Revoked { reason: String },
27 Unknown,
29}
30
31impl RevocationStatus {
32 pub fn is_revoked(&self) -> bool {
34 matches!(self, RevocationStatus::Revoked { .. })
35 }
36
37 pub fn is_valid(&self) -> bool {
39 matches!(self, RevocationStatus::Valid)
40 }
41
42 pub fn is_unknown(&self) -> bool {
44 matches!(self, RevocationStatus::Unknown)
45 }
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
52pub struct RevocationEntry {
53 pub index: usize,
55 pub credential_id: String,
57 pub reason: String,
59 pub revoked_at: String,
61}
62
63impl RevocationEntry {
64 pub fn new(index: usize, credential_id: &str, reason: &str, revoked_at: &str) -> Self {
66 Self {
67 index,
68 credential_id: credential_id.to_string(),
69 reason: reason.to_string(),
70 revoked_at: revoked_at.to_string(),
71 }
72 }
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct BloomFilter {
83 bits: Vec<u8>,
85 k: usize,
87 m: usize,
89 count: usize,
91}
92
93impl BloomFilter {
94 pub fn new(m: usize, k: usize) -> Self {
97 let byte_count = (m + 7) / 8;
98 Self {
99 bits: vec![0u8; byte_count],
100 k,
101 m,
102 count: 0,
103 }
104 }
105
106 pub fn with_defaults() -> Self {
108 Self::new(131_072, 3)
109 }
110
111 pub fn insert(&mut self, item: &str) {
113 for i in 0..self.k {
114 let bit = self.hash(item, i);
115 let byte_idx = bit / 8;
116 let bit_idx = bit % 8;
117 self.bits[byte_idx] |= 1 << bit_idx;
118 }
119 self.count += 1;
120 }
121
122 pub fn might_contain(&self, item: &str) -> bool {
125 for i in 0..self.k {
126 let bit = self.hash(item, i);
127 let byte_idx = bit / 8;
128 let bit_idx = bit % 8;
129 if self.bits[byte_idx] & (1 << bit_idx) == 0 {
130 return false;
131 }
132 }
133 true
134 }
135
136 pub fn count(&self) -> usize {
138 self.count
139 }
140
141 pub fn false_positive_rate(&self) -> f64 {
143 let exponent = -(self.k as f64) * (self.count as f64) / (self.m as f64);
145 (1.0 - exponent.exp()).powi(self.k as i32)
146 }
147
148 fn hash(&self, item: &str, seed: usize) -> usize {
150 let mut hasher = Sha256::new();
151 hasher.update(seed.to_be_bytes());
152 hasher.update(item.as_bytes());
153 let digest = hasher.finalize();
154 let value = u64::from_be_bytes(digest[..8].try_into().unwrap_or([0u8; 8]));
156 (value as usize) % self.m
157 }
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct RevocationList2020 {
169 pub id: String,
171 pub issuer: String,
173 bits: Vec<u8>,
175 capacity: usize,
177}
178
179impl RevocationList2020 {
180 pub fn new(id: &str, issuer: &str, capacity: usize) -> DidResult<Self> {
184 const MIN_CAPACITY: usize = 16_384;
185 if capacity < MIN_CAPACITY {
186 return Err(DidError::InvalidKey(format!(
187 "RevocationList2020 capacity must be at least {MIN_CAPACITY}, got {capacity}"
188 )));
189 }
190 let byte_count = (capacity + 7) / 8;
191 Ok(Self {
192 id: id.to_string(),
193 issuer: issuer.to_string(),
194 bits: vec![0u8; byte_count],
195 capacity,
196 })
197 }
198
199 pub fn is_revoked(&self, index: usize) -> DidResult<bool> {
201 self.check_bounds(index)?;
202 let byte = self.bits[index / 8];
203 Ok(byte & (1 << (index % 8)) != 0)
204 }
205
206 pub fn set_status(&mut self, index: usize, revoked: bool) -> DidResult<()> {
208 self.check_bounds(index)?;
209 if revoked {
210 self.bits[index / 8] |= 1 << (index % 8);
211 } else {
212 self.bits[index / 8] &= !(1 << (index % 8));
213 }
214 Ok(())
215 }
216
217 pub fn capacity(&self) -> usize {
219 self.capacity
220 }
221
222 pub fn revoked_count(&self) -> usize {
224 self.bits.iter().map(|b| b.count_ones() as usize).sum()
225 }
226
227 pub fn revoked_indices(&self) -> Vec<usize> {
229 let mut result = Vec::new();
230 for (byte_idx, byte) in self.bits.iter().enumerate() {
231 for bit_idx in 0..8 {
232 let global = byte_idx * 8 + bit_idx;
233 if global >= self.capacity {
234 break;
235 }
236 if byte & (1 << bit_idx) != 0 {
237 result.push(global);
238 }
239 }
240 }
241 result
242 }
243
244 pub fn to_credential(&self) -> DidResult<serde_json::Value> {
246 use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
247 let encoded = URL_SAFE_NO_PAD.encode(&self.bits);
248 Ok(serde_json::json!({
249 "@context": [
250 "https://www.w3.org/2018/credentials/v1",
251 "https://w3id.org/vc-revocation-list-2020/v1"
252 ],
253 "id": self.id,
254 "type": ["VerifiableCredential", "RevocationList2020Credential"],
255 "issuer": self.issuer,
256 "credentialSubject": {
257 "id": format!("{}#list", self.id),
258 "type": "RevocationList2020",
259 "encodedList": encoded
260 }
261 }))
262 }
263
264 fn check_bounds(&self, index: usize) -> DidResult<()> {
265 if index >= self.capacity {
266 Err(DidError::InvalidKey(format!(
267 "index {index} out of range (capacity {})",
268 self.capacity
269 )))
270 } else {
271 Ok(())
272 }
273 }
274}
275
276#[derive(Debug, Clone)]
281pub struct RevocationRegistry2020 {
282 list: RevocationList2020,
283 bloom: BloomFilter,
284 credential_map: HashMap<String, usize>,
286 entries: HashMap<usize, RevocationEntry>,
288 next_index: usize,
290}
291
292impl RevocationRegistry2020 {
293 pub fn new(id: &str, issuer: &str, capacity: usize) -> DidResult<Self> {
295 Ok(Self {
296 list: RevocationList2020::new(id, issuer, capacity)?,
297 bloom: BloomFilter::with_defaults(),
298 credential_map: HashMap::new(),
299 entries: HashMap::new(),
300 next_index: 0,
301 })
302 }
303
304 pub fn register(&mut self, credential_id: &str) -> DidResult<usize> {
307 if self.credential_map.contains_key(credential_id) {
308 return Err(DidError::InvalidKey(format!(
309 "Credential already registered: {credential_id}"
310 )));
311 }
312 if self.next_index >= self.list.capacity() {
313 return Err(DidError::InvalidKey("Revocation list is full".to_string()));
314 }
315 let index = self.next_index;
316 self.next_index += 1;
317 self.credential_map.insert(credential_id.to_string(), index);
318 Ok(index)
319 }
320
321 pub fn check_status(&self, credential_id: &str) -> RevocationStatus {
323 let Some(&index) = self.credential_map.get(credential_id) else {
325 return RevocationStatus::Unknown;
326 };
327
328 if !self.bloom.might_contain(credential_id) {
330 return RevocationStatus::Valid;
331 }
332
333 match self.list.is_revoked(index) {
335 Ok(true) => {
336 let reason = self
337 .entries
338 .get(&index)
339 .map_or("unspecified", |e| e.reason.as_str())
340 .to_string();
341 RevocationStatus::Revoked { reason }
342 }
343 _ => RevocationStatus::Valid,
344 }
345 }
346
347 pub fn revoke(&mut self, credential_id: &str, reason: &str) -> DidResult<()> {
349 let index = self.resolve_index(credential_id)?;
350 self.list.set_status(index, true)?;
351 self.bloom.insert(credential_id);
352 let ts = chrono::Utc::now().to_rfc3339();
353 self.entries.insert(
354 index,
355 RevocationEntry::new(index, credential_id, reason, &ts),
356 );
357 Ok(())
358 }
359
360 pub fn reinstate(&mut self, credential_id: &str) -> DidResult<()> {
362 let index = self.resolve_index(credential_id)?;
363 self.list.set_status(index, false)?;
364 self.entries.remove(&index);
365 Ok(())
368 }
369
370 pub fn revoked_count(&self) -> usize {
372 self.list.revoked_count()
373 }
374
375 pub fn registered_count(&self) -> usize {
377 self.credential_map.len()
378 }
379
380 pub fn list(&self) -> &RevocationList2020 {
382 &self.list
383 }
384
385 pub fn bloom(&self) -> &BloomFilter {
387 &self.bloom
388 }
389
390 pub fn entries(&self) -> impl Iterator<Item = &RevocationEntry> {
392 self.entries.values()
393 }
394
395 fn resolve_index(&self, credential_id: &str) -> DidResult<usize> {
396 self.credential_map
397 .get(credential_id)
398 .copied()
399 .ok_or_else(|| {
400 DidError::InvalidKey(format!("Credential not registered: {credential_id}"))
401 })
402 }
403}
404
405#[cfg(test)]
408mod tests {
409 use super::*;
410
411 const ID: &str = "https://example.com/status/rl2020";
412 const ISSUER: &str = "did:key:z6Mk";
413
414 fn make_registry(cap: usize) -> RevocationRegistry2020 {
415 RevocationRegistry2020::new(ID, ISSUER, cap).unwrap()
416 }
417
418 #[test]
421 fn test_status_valid_is_not_revoked() {
422 assert!(!RevocationStatus::Valid.is_revoked());
423 assert!(RevocationStatus::Valid.is_valid());
424 assert!(!RevocationStatus::Valid.is_unknown());
425 }
426
427 #[test]
428 fn test_status_revoked_is_revoked() {
429 let s = RevocationStatus::Revoked {
430 reason: "test".to_string(),
431 };
432 assert!(s.is_revoked());
433 assert!(!s.is_valid());
434 }
435
436 #[test]
437 fn test_status_unknown() {
438 assert!(RevocationStatus::Unknown.is_unknown());
439 }
440
441 #[test]
444 fn test_bloom_insert_and_query() {
445 let mut bf = BloomFilter::with_defaults();
446 bf.insert("urn:uuid:cred-1");
447 assert!(bf.might_contain("urn:uuid:cred-1"));
448 }
449
450 #[test]
451 fn test_bloom_non_member_definite_negative() {
452 let bf = BloomFilter::with_defaults();
453 assert!(!bf.might_contain("urn:uuid:never-inserted"));
455 }
456
457 #[test]
458 fn test_bloom_count() {
459 let mut bf = BloomFilter::with_defaults();
460 bf.insert("a");
461 bf.insert("b");
462 bf.insert("c");
463 assert_eq!(bf.count(), 3);
464 }
465
466 #[test]
467 fn test_bloom_false_positive_rate_low_for_empty() {
468 let bf = BloomFilter::with_defaults();
469 assert!(bf.false_positive_rate() < 0.01);
470 }
471
472 #[test]
473 fn test_bloom_custom_params() {
474 let mut bf = BloomFilter::new(1024, 2);
475 bf.insert("item");
476 assert!(bf.might_contain("item"));
477 assert!(!bf.might_contain("other-item-definitely-not-here-xyz"));
478 }
479
480 #[test]
483 fn test_list_new_min_capacity_error() {
484 assert!(RevocationList2020::new(ID, ISSUER, 1024).is_err());
485 }
486
487 #[test]
488 fn test_list_set_and_check() {
489 let mut list = RevocationList2020::new(ID, ISSUER, 16_384).unwrap();
490 assert!(!list.is_revoked(42).unwrap());
491 list.set_status(42, true).unwrap();
492 assert!(list.is_revoked(42).unwrap());
493 }
494
495 #[test]
496 fn test_list_set_false_clears_bit() {
497 let mut list = RevocationList2020::new(ID, ISSUER, 16_384).unwrap();
498 list.set_status(10, true).unwrap();
499 list.set_status(10, false).unwrap();
500 assert!(!list.is_revoked(10).unwrap());
501 }
502
503 #[test]
504 fn test_list_out_of_bounds_error() {
505 let list = RevocationList2020::new(ID, ISSUER, 16_384).unwrap();
506 assert!(list.is_revoked(16_384).is_err());
507 assert!(list.is_revoked(99_999).is_err());
508 }
509
510 #[test]
511 fn test_list_revoked_count() {
512 let mut list = RevocationList2020::new(ID, ISSUER, 16_384).unwrap();
513 list.set_status(1, true).unwrap();
514 list.set_status(5, true).unwrap();
515 list.set_status(1000, true).unwrap();
516 assert_eq!(list.revoked_count(), 3);
517 }
518
519 #[test]
520 fn test_list_revoked_indices() {
521 let mut list = RevocationList2020::new(ID, ISSUER, 16_384).unwrap();
522 list.set_status(3, true).unwrap();
523 list.set_status(7, true).unwrap();
524 list.set_status(200, true).unwrap();
525 assert_eq!(list.revoked_indices(), vec![3, 7, 200]);
526 }
527
528 #[test]
529 fn test_list_to_credential_json() {
530 let mut list = RevocationList2020::new(ID, ISSUER, 16_384).unwrap();
531 list.set_status(0, true).unwrap();
532 let cred = list.to_credential().unwrap();
533 assert_eq!(cred["type"][1], "RevocationList2020Credential");
534 assert_eq!(cred["issuer"], ISSUER);
535 assert_eq!(cred["credentialSubject"]["type"], "RevocationList2020");
536 assert!(cred["credentialSubject"]["encodedList"].is_string());
537 }
538
539 #[test]
540 fn test_list_capacity() {
541 let list = RevocationList2020::new(ID, ISSUER, 32_768).unwrap();
542 assert_eq!(list.capacity(), 32_768);
543 }
544
545 #[test]
548 fn test_registry_register_and_valid() {
549 let mut reg = make_registry(16_384);
550 reg.register("urn:uuid:a").unwrap();
551 assert_eq!(reg.check_status("urn:uuid:a"), RevocationStatus::Valid);
552 }
553
554 #[test]
555 fn test_registry_unknown_credential() {
556 let reg = make_registry(16_384);
557 assert_eq!(
558 reg.check_status("urn:uuid:never-registered"),
559 RevocationStatus::Unknown
560 );
561 }
562
563 #[test]
564 fn test_registry_revoke_and_check() {
565 let mut reg = make_registry(16_384);
566 reg.register("urn:uuid:cred-1").unwrap();
567 reg.revoke("urn:uuid:cred-1", "keyCompromise").unwrap();
568 match reg.check_status("urn:uuid:cred-1") {
569 RevocationStatus::Revoked { reason } => assert_eq!(reason, "keyCompromise"),
570 other => panic!("Expected Revoked, got {other:?}"),
571 }
572 }
573
574 #[test]
575 fn test_registry_reinstate() {
576 let mut reg = make_registry(16_384);
577 reg.register("urn:uuid:cred-2").unwrap();
578 reg.revoke("urn:uuid:cred-2", "superseded").unwrap();
579 reg.reinstate("urn:uuid:cred-2").unwrap();
580 assert_eq!(reg.check_status("urn:uuid:cred-2"), RevocationStatus::Valid);
581 }
582
583 #[test]
584 fn test_registry_revoked_count() {
585 let mut reg = make_registry(16_384);
586 for i in 0..5 {
587 reg.register(&format!("urn:uuid:{i}")).unwrap();
588 }
589 reg.revoke("urn:uuid:0", "a").unwrap();
590 reg.revoke("urn:uuid:2", "b").unwrap();
591 assert_eq!(reg.revoked_count(), 2);
592 }
593
594 #[test]
595 fn test_registry_registered_count() {
596 let mut reg = make_registry(16_384);
597 reg.register("urn:uuid:x").unwrap();
598 reg.register("urn:uuid:y").unwrap();
599 assert_eq!(reg.registered_count(), 2);
600 }
601
602 #[test]
603 fn test_registry_double_register_error() {
604 let mut reg = make_registry(16_384);
605 reg.register("urn:uuid:dup").unwrap();
606 assert!(reg.register("urn:uuid:dup").is_err());
607 }
608
609 #[test]
610 fn test_registry_revoke_unregistered_error() {
611 let mut reg = make_registry(16_384);
612 assert!(reg.revoke("urn:uuid:ghost", "reason").is_err());
613 }
614
615 #[test]
616 fn test_registry_reinstate_unregistered_error() {
617 let mut reg = make_registry(16_384);
618 assert!(reg.reinstate("urn:uuid:ghost").is_err());
619 }
620
621 #[test]
622 fn test_registry_entries_after_revoke() {
623 let mut reg = make_registry(16_384);
624 reg.register("urn:uuid:e1").unwrap();
625 reg.revoke("urn:uuid:e1", "expired").unwrap();
626 let entries: Vec<_> = reg.entries().collect();
627 assert_eq!(entries.len(), 1);
628 assert_eq!(entries[0].credential_id, "urn:uuid:e1");
629 assert_eq!(entries[0].reason, "expired");
630 }
631
632 #[test]
633 fn test_registry_entries_cleared_after_reinstate() {
634 let mut reg = make_registry(16_384);
635 reg.register("urn:uuid:e2").unwrap();
636 reg.revoke("urn:uuid:e2", "admin").unwrap();
637 reg.reinstate("urn:uuid:e2").unwrap();
638 let entries: Vec<_> = reg.entries().collect();
639 assert_eq!(entries.len(), 0);
640 }
641
642 #[test]
643 fn test_registry_multiple_credentials() {
644 let mut reg = make_registry(16_384);
645 for i in 0..10 {
646 reg.register(&format!("urn:uuid:multi-{i}")).unwrap();
647 }
648 reg.revoke("urn:uuid:multi-3", "r3").unwrap();
649 reg.revoke("urn:uuid:multi-7", "r7").unwrap();
650
651 assert!(reg.check_status("urn:uuid:multi-3").is_revoked());
652 assert!(reg.check_status("urn:uuid:multi-7").is_revoked());
653 assert!(reg.check_status("urn:uuid:multi-5").is_valid());
654 assert_eq!(reg.revoked_count(), 2);
655 }
656
657 #[test]
660 fn test_revocation_entry_fields() {
661 let entry =
662 RevocationEntry::new(42, "urn:uuid:test", "keyCompromise", "2026-01-01T00:00:00Z");
663 assert_eq!(entry.index, 42);
664 assert_eq!(entry.credential_id, "urn:uuid:test");
665 assert_eq!(entry.reason, "keyCompromise");
666 assert_eq!(entry.revoked_at, "2026-01-01T00:00:00Z");
667 }
668}