biscuit_ssh_core/
revocation.rs1use std::collections::HashSet;
2use std::path::Path;
3
4use crate::Error;
5
6const MIN_ID_LEN: usize = 64;
8
9#[derive(Debug, Clone, Default)]
18pub struct RevocationList(HashSet<String>);
19
20impl RevocationList {
21 pub fn parse(text: &str) -> Result<Self, Error> {
24 text.lines()
25 .map(str::trim)
26 .filter(|line| !line.is_empty() && !line.starts_with('#'))
27 .map(|line| {
28 if line.len() < MIN_ID_LEN || line.len() % 2 != 0 || !line.bytes().all(|b| b.is_ascii_hexdigit()) {
29 return Err(Error::invalid(
30 "revocation identifier",
31 "expected at least 32 hex-encoded bytes",
32 ));
33 }
34 Ok(line.to_ascii_lowercase())
35 })
36 .collect::<Result<_, _>>()
37 .map(Self)
38 }
39
40 pub fn load(path: &Path) -> std::io::Result<Self> {
42 let text = std::fs::read_to_string(path)?;
43 Self::parse(&text).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))
44 }
45
46 pub(crate) fn find<'a>(&self, ids: &'a [String]) -> Option<&'a String> {
48 ids.iter().find(|id| self.0.contains(*id))
49 }
50}
51
52pub(crate) fn hex(bytes: &[u8]) -> String {
54 bytes.iter().map(|b| format!("{b:02x}")).collect()
55}
56
57#[cfg(test)]
58mod tests {
59 use super::*;
60
61 #[test]
62 fn parses_identifiers() {
63 let (a, b) = ("ab".repeat(64), "cd".repeat(64));
64 let list = RevocationList::parse(&format!("# revoked\n\n{}\n {b} \n", a.to_uppercase())).unwrap();
65 let ids = ["ef".repeat(64), a];
66 assert_eq!(list.find(&ids), Some(&ids[1]));
67 assert_eq!(list.find(&ids[..1]), None);
68 }
69
70 #[test]
71 fn rejects_malformed_identifiers() {
72 for bad in ["abcd".to_owned(), "a".repeat(129), "z".repeat(128)] {
73 assert!(RevocationList::parse(&bad).is_err(), "{bad}");
74 }
75 }
76}