1use crate::{
5 ServerError,
6 store::{Key, keys},
7};
8use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
9use ed25519_dalek::{Signature, VerifyingKey};
10use mkit_core::{hash::hash, repo_identity::Namespace, write_auth::validate_audience};
11use std::collections::BTreeSet;
12
13pub const DOMAIN: &str = "mkit-authority-generation:v1";
15pub const MAX_STATEMENT_BYTES: usize = 2048;
17pub const MAX_STEP: u64 = 1024;
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct AuthorityKey {
23 pub key_id: String,
25 pub public_key: [u8; 32],
27 pub namespaces: BTreeSet<Namespace>,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct AuthorityFence {
34 keys: Vec<AuthorityKey>,
35}
36
37fn rejected() -> ServerError {
38 ServerError::permission_denied("authority generation statement rejected")
39}
40fn valid_id(id: &str) -> bool {
41 (1..=64).contains(&id.len())
42 && id
43 .bytes()
44 .all(|b| b.is_ascii_alphanumeric() || b"._-".contains(&b))
45}
46fn decimal(text: &str) -> Result<u64, ServerError> {
47 text.parse::<u64>()
48 .ok()
49 .filter(|n| n.to_string() == text)
50 .ok_or_else(rejected)
51}
52
53impl AuthorityFence {
54 pub fn new(keys: Vec<AuthorityKey>) -> Result<Self, ServerError> {
59 if keys.is_empty() || keys.len() > 16 {
60 return Err(rejected());
61 }
62 let mut ids = BTreeSet::new();
63 let mut publics = BTreeSet::new();
64 for key in &keys {
65 let public = VerifyingKey::from_bytes(&key.public_key).map_err(|_| rejected())?;
66 if !valid_id(&key.key_id)
67 || !ids.insert(&key.key_id)
68 || !publics.insert(key.public_key)
69 || public.is_weak()
70 || key.namespaces.is_empty()
71 || key.namespaces.len() > 1024
72 || key
73 .namespaces
74 .iter()
75 .any(|ns| matches!(ns,Namespace::Ed25519(owner) if owner == &key.public_key))
76 {
77 return Err(rejected());
78 }
79 }
80 Ok(Self { keys })
81 }
82
83 pub fn parse(text: &str) -> Result<Self, ServerError> {
88 if text.len() > 256_000 {
89 return Err(rejected());
90 }
91 let mut keys = Vec::new();
92 for line in text.lines() {
93 if keys.len() >= 16 {
94 return Err(rejected());
95 }
96 let fields = line.split_whitespace().collect::<Vec<_>>();
97 let [id, public, namespaces] = fields.as_slice() else {
98 return Err(rejected());
99 };
100 if public.len() != 64 || !public.bytes().all(|b| matches!(b,b'0'..=b'9'|b'a'..=b'f')) {
101 return Err(rejected());
102 }
103 let public_key = mkit_core::hash::from_hex(public).map_err(|_| rejected())?;
104 let namespaces = namespaces
105 .split(',')
106 .map(|ns| Namespace::parse(ns).map_err(|_| rejected()))
107 .collect::<Result<BTreeSet<_>, _>>()?;
108 keys.push(AuthorityKey {
109 key_id: (*id).to_owned(),
110 public_key,
111 namespaces,
112 });
113 }
114 Self::new(keys)
115 }
116
117 pub fn public_keys(&self) -> impl Iterator<Item = [u8; 32]> + '_ {
119 self.keys.iter().map(|k| k.public_key)
120 }
121
122 pub fn verify(
129 &self,
130 wire: &str,
131 audience: &str,
132 now_ms: i64,
133 ) -> Result<AuthorityStatement, ServerError> {
134 if wire.len() > MAX_STATEMENT_BYTES {
135 return Err(rejected());
136 }
137 let (text, sig) = wire.split_once('.').ok_or_else(rejected)?;
138 let decode = |s: &str| {
139 let bytes = URL_SAFE_NO_PAD.decode(s).map_err(|_| rejected())?;
140 if URL_SAFE_NO_PAD.encode(&bytes) != s {
141 return Err(rejected());
142 }
143 Ok(bytes)
144 };
145 let bytes = decode(text)?;
146 let sig = decode(sig)?;
147 let text = std::str::from_utf8(&bytes).map_err(|_| rejected())?;
148 let fields = text.split('\n').collect::<Vec<_>>();
149 let [
150 domain,
151 id,
152 namespace,
153 generation,
154 origin,
155 created,
156 expiry,
157 nonce,
158 ] = fields.as_slice()
159 else {
160 return Err(rejected());
161 };
162 let raw_namespace = *namespace;
163 let namespace = Namespace::parse(raw_namespace).map_err(|_| rejected())?;
164 if namespace.to_string() != raw_namespace {
165 return Err(rejected());
166 }
167 let key = self
168 .keys
169 .iter()
170 .find(|key| key.key_id == *id && key.namespaces.contains(&namespace))
171 .ok_or_else(rejected)?;
172 let created = decimal(created)?;
173 let expiry = decimal(expiry)?;
174 let now = u64::try_from(now_ms).map_err(|_| rejected())?;
175 if *domain != DOMAIN
176 || *origin != audience
177 || validate_audience(origin).is_err()
178 || expiry <= created
179 || expiry - created > 300_000
180 || now >= expiry
181 || created > now.saturating_add(30_000)
182 || nonce.len() != 64
183 || !nonce.bytes().all(|b| matches!(b,b'0'..=b'9'|b'a'..=b'f'))
184 {
185 return Err(rejected());
186 }
187 let signature = Signature::from_slice(&sig).map_err(|_| rejected())?;
188 VerifyingKey::from_bytes(&key.public_key)
189 .map_err(|_| rejected())?
190 .verify_strict(&hash(&bytes), &signature)
191 .map_err(|_| rejected())?;
192 Ok(AuthorityStatement {
193 namespace,
194 generation: decimal(generation)?,
195 })
196 }
197}
198
199#[derive(Debug, Clone, PartialEq, Eq)]
201pub struct AuthorityStatement {
202 pub namespace: Namespace,
204 pub generation: u64,
206}
207
208#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
210pub(crate) enum FenceKind {
211 Grant,
212 Authority,
213}
214impl FenceKind {
215 pub(crate) fn key(self) -> Key {
216 match self {
217 Self::Grant => keys::grant_epoch(),
218 Self::Authority => keys::authority_generation(),
219 }
220 }
221}
222
223pub(crate) fn moved() -> ServerError {
224 ServerError::permission_denied("namespace authority generation changed")
225 .with_abort_cause(crate::error::AbortCause::EpochMismatch)
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use ed25519_dalek::{Signer, SigningKey};
232 fn ns() -> String {
233 format!(
234 "ed25519-{}",
235 mkit_core::hash::to_hex(SigningKey::from_bytes(&[1; 32]).verifying_key().as_bytes())
236 )
237 }
238 fn fence() -> AuthorityFence {
239 AuthorityFence::parse(&format!(
240 "deployment {} {}",
241 mkit_core::hash::to_hex(SigningKey::from_bytes(&[7; 32]).verifying_key().as_bytes()),
242 ns()
243 ))
244 .unwrap()
245 }
246 fn wire(fields: &[String], seed: [u8; 32]) -> String {
247 let bytes = fields.join("\n");
248 let signature = SigningKey::from_bytes(&seed).sign(&hash(bytes.as_bytes()));
249 format!(
250 "{}.{}",
251 URL_SAFE_NO_PAD.encode(bytes),
252 URL_SAFE_NO_PAD.encode(signature.to_bytes())
253 )
254 }
255 fn fields() -> Vec<String> {
256 vec![
257 DOMAIN.into(),
258 "deployment".into(),
259 ns(),
260 "0".into(),
261 "https://vcs.example".into(),
262 "0".into(),
263 "60000".into(),
264 "a".repeat(64),
265 ]
266 }
267 #[test]
268 fn statement_binds_every_field_and_strict_signature() {
269 let f = fence();
270 let fields = fields();
271 assert_eq!(
272 f.verify(&wire(&fields, [7; 32]), "https://vcs.example", 1)
273 .unwrap()
274 .generation,
275 0
276 );
277 for (index, value) in [
278 (0, "mkit-hook:v1"),
279 (1, "owner"),
280 (2, "bad"),
281 (3, "01"),
282 (4, "https://other.example"),
283 (5, "40000"),
284 (6, "300001"),
285 (7, "aa"),
286 ] {
287 let mut changed = fields.clone();
288 changed[index] = value.into();
289 assert!(
290 f.verify(&wire(&changed, [7; 32]), "https://vcs.example", 1)
291 .is_err(),
292 "field {index}"
293 );
294 }
295 assert!(
296 f.verify(&wire(&fields, [1; 32]), "https://vcs.example", 1)
297 .is_err()
298 );
299 assert!(
300 f.verify(&wire(&fields, [7; 32]), "https://vcs.example", 60000)
301 .is_err()
302 );
303 assert!(
304 f.verify(
305 &"a".repeat(MAX_STATEMENT_BYTES + 1),
306 "https://vcs.example",
307 1
308 )
309 .is_err()
310 );
311 let mut extended = fields.clone();
312 extended.push(String::new());
313 assert!(
314 f.verify(&wire(&extended, [7; 32]), "https://vcs.example", 1)
315 .is_err()
316 );
317 }
318 #[test]
319 fn deployment_keys_are_bounded_dedicated_and_permissioned() {
320 let public =
321 mkit_core::hash::to_hex(SigningKey::from_bytes(&[1; 32]).verifying_key().as_bytes());
322 assert!(AuthorityFence::parse(&format!("owner {public} {}", ns())).is_err());
323 assert!(AuthorityFence::parse("").is_err());
324 let key = format!(
325 "deployment {} {}",
326 mkit_core::hash::to_hex(SigningKey::from_bytes(&[7; 32]).verifying_key().as_bytes()),
327 ns()
328 );
329 assert!(AuthorityFence::parse(&format!("{key}\n{key}")).is_err());
330 let mut fields = fields();
331 fields[2] = format!(
332 "ed25519-{}",
333 mkit_core::hash::to_hex(SigningKey::from_bytes(&[2; 32]).verifying_key().as_bytes())
334 );
335 assert!(
336 fence()
337 .verify(&wire(&fields, [7; 32]), "https://vcs.example", 1)
338 .is_err()
339 );
340 }
341}