mkit_server/scanner_retrieval/
config.rs1use core::fmt;
2use ed25519_dalek::{SigningKey, VerifyingKey};
3use mkit_core::hash::{Hash, from_hex};
4use std::collections::BTreeSet;
5use subtle::ConstantTimeEq;
6use zeroize::Zeroizing;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
10#[error("invalid or conflicting scanner retrieval configuration")]
11pub struct ConfigError;
12
13#[derive(Clone, PartialEq, Eq)]
14pub(super) struct Key {
15 pub id: String,
16 pub secret: Zeroizing<Hash>,
17 pub retired_at_ms: Option<u64>,
18}
19
20#[derive(Clone, PartialEq, Eq)]
22pub struct RetrievalConfig {
23 pub(super) keys: Vec<Key>,
24 scanners: Vec<Hash>,
25}
26
27impl fmt::Debug for RetrievalConfig {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 f.debug_struct("RetrievalConfig")
30 .field(
31 "key_ids",
32 &self.keys.iter().map(|k| &k.id).collect::<Vec<_>>(),
33 )
34 .field("scanner_count", &self.scanners.len())
35 .finish_non_exhaustive()
36 }
37}
38
39fn lines(text: &str) -> impl Iterator<Item = &str> {
40 text.lines()
41 .map(str::trim)
42 .filter(|s| !s.is_empty() && !s.starts_with('#'))
43}
44
45impl RetrievalConfig {
46 pub fn parse(keys: &str, scanners: &str) -> Result<Self, ConfigError> {
54 let mut parsed: Vec<Key> = Vec::new();
55 let mut ids = BTreeSet::new();
56 for line in lines(keys) {
57 let fields: Vec<_> = line.split_whitespace().collect();
58 let (id, secret, retired_at_ms) = match fields.as_slice() {
59 ["active", id, secret] if parsed.is_empty() => (*id, *secret, None),
60 ["retained", id, secret, at] if !parsed.is_empty() => {
61 let at_ms = at.parse::<u64>().map_err(|_| ConfigError)?;
62 if at_ms.to_string() != *at {
63 return Err(ConfigError);
64 }
65 (*id, *secret, Some(at_ms))
66 }
67 _ => return Err(ConfigError),
68 };
69 if id.is_empty()
70 || id.len() > 32
71 || !id
72 .bytes()
73 .all(|b| b.is_ascii_alphanumeric() || b"._-".contains(&b))
74 || !ids.insert(id.to_owned())
75 || parsed.len() >= 16
76 {
77 return Err(ConfigError);
78 }
79 let secret = Zeroizing::new(from_hex(secret).map_err(|_| ConfigError)?);
80 if parsed.iter().any(|k| bool::from(k.secret.ct_eq(&*secret))) {
81 return Err(ConfigError);
82 }
83 parsed.push(Key {
84 id: id.to_owned(),
85 secret,
86 retired_at_ms,
87 });
88 }
89 let mut public = Vec::new();
90 for line in lines(scanners) {
91 let key = from_hex(line).map_err(|_| ConfigError)?;
92 let verifying = VerifyingKey::from_bytes(&key).map_err(|_| ConfigError)?;
93 if public.len() >= 32 || verifying.is_weak() || public.contains(&key) {
94 return Err(ConfigError);
95 }
96 public.push(key);
97 }
98 if parsed.is_empty() || public.is_empty() {
99 return Err(ConfigError);
100 }
101 let config = Self {
102 keys: parsed,
103 scanners: public,
104 };
105 config.check_role_keys(&[], &[])?;
106 Ok(config)
107 }
108
109 pub fn scanner_keys(&self) -> impl Iterator<Item = Hash> + '_ {
111 self.scanners.iter().copied()
112 }
113
114 pub fn check_secret(&self, secret: &Hash) -> Result<(), ConfigError> {
119 self.check_role_keys(&[], &[*secret])
120 }
121
122 pub fn check_role_keys(&self, public: &[Hash], seeds: &[Hash]) -> Result<(), ConfigError> {
129 let derived: Vec<_> = self
130 .keys
131 .iter()
132 .map(|k| SigningKey::from_bytes(&k.secret).verifying_key().to_bytes())
133 .collect();
134 for key in &self.keys {
135 if seeds.iter().any(|s| bool::from(key.secret.ct_eq(s)))
136 || public.contains(&*key.secret)
137 || self.scanners.contains(&*key.secret)
138 {
139 return Err(ConfigError);
140 }
141 }
142 if derived
143 .iter()
144 .any(|k| public.contains(k) || seeds.contains(k) || self.scanners.contains(k))
145 || self.scanners.iter().any(|k| {
146 public.contains(k)
147 || seeds.contains(k)
148 || seeds
149 .iter()
150 .any(|s| SigningKey::from_bytes(s).verifying_key().as_bytes() == k)
151 })
152 {
153 return Err(ConfigError);
154 }
155 Ok(())
156 }
157
158 #[cfg(feature = "remote-hooks")]
159 pub(crate) fn accepts(&self, public: &Hash) -> bool {
160 self.scanners.contains(public)
161 }
162}