1use std::collections::BTreeSet;
2
3use ed25519_dalek::{Signature, VerifyingKey};
4use mkit_core::hash::{hash, to_hex};
5use serde::Deserialize;
6use subtle::ConstantTimeEq;
7
8use crate::{ServerError, auth_v2};
9
10use super::{BodyCapture, Headers};
11
12pub const HEADER_NAMES: [&str; 8] = [
14 "x-mkit-admin-version",
15 "x-mkit-admin-key-id",
16 "x-mkit-admin-audience",
17 "x-mkit-admin-created-at",
18 "x-mkit-admin-expires-at",
19 "x-mkit-admin-nonce",
20 "x-mkit-admin-digest",
21 "x-mkit-admin-signature",
22];
23
24#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct Config {
27 pub(crate) audience: String,
28 keys: Vec<Key>,
29}
30
31#[derive(Clone, Debug, PartialEq, Eq)]
32struct Key {
33 id: String,
34 public: [u8; 32],
35 before: Option<i64>,
36 after: Option<i64>,
37 roles: BTreeSet<String>,
38}
39
40impl Config {
41 pub fn parse(audience: &str, json: &str) -> Result<Self, ServerError> {
46 #[derive(Deserialize)]
47 #[serde(rename_all = "camelCase", deny_unknown_fields)]
48 struct Entry {
49 key_id: String,
50 alg: String,
51 public_key: String,
52 not_before_ms: Option<String>,
53 not_after_ms: Option<String>,
54 roles: Vec<String>,
55 }
56 #[derive(Deserialize)]
57 #[serde(deny_unknown_fields)]
58 struct List {
59 version: u32,
60 keys: Vec<Entry>,
61 }
62 if json.len() > 64 * 1024 {
63 return Err(invalid("admin key list too large"));
64 }
65 mkit_core::write_auth::validate_audience(audience)
66 .map_err(|_| invalid("invalid admin audience"))?;
67 let list: List =
68 serde_json::from_str(json).map_err(|_| invalid("invalid admin key list"))?;
69 if list.keys.len() > 128 {
70 return Err(invalid("too many admin keys"));
71 }
72 if list.version != 1 {
73 return Err(invalid("invalid admin key-list version"));
74 }
75 let mut keys: Vec<Key> = Vec::new();
76 for e in list.keys {
77 if !identifier(&e.key_id, 64, false)
78 || e.alg != "ed25519"
79 || keys.iter().any(|k| k.id == e.key_id)
80 {
81 return Err(invalid("invalid or duplicate admin key id"));
82 }
83 let public =
84 hex::<32>(&e.public_key).ok_or_else(|| invalid("invalid admin public key"))?;
85 VerifyingKey::from_bytes(&public).map_err(|_| invalid("invalid Ed25519 admin key"))?;
86 let bound = |s: Option<String>| {
87 s.map(|s| {
88 decimal_i64(&s).ok_or_else(|| invalid("invalid admin key validity bound"))
89 })
90 .transpose()
91 };
92 let before = bound(e.not_before_ms)?;
93 let after = bound(e.not_after_ms)?;
94 if before.zip(after).is_some_and(|(b, a)| b > a) {
95 return Err(invalid("invalid admin key validity interval"));
96 }
97 let mut roles = BTreeSet::new();
98 for role in e.roles {
99 if !matches!(
100 role.as_str(),
101 "lease" | "moderation" | "grants" | "audit" | "all"
102 ) || !roles.insert(role)
103 {
104 return Err(invalid("invalid or duplicate admin role"));
105 }
106 }
107 if roles.is_empty() {
108 return Err(invalid("admin key requires roles"));
109 }
110 keys.push(Key {
111 id: e.key_id,
112 public,
113 before,
114 after,
115 roles,
116 });
117 }
118 Ok(Self {
119 audience: audience.to_owned(),
120 keys,
121 })
122 }
123
124 #[must_use]
126 pub fn audience(&self) -> &str {
127 &self.audience
128 }
129
130 #[must_use]
132 pub fn enabled(&self) -> bool {
133 !self.keys.is_empty()
134 }
135
136 #[must_use]
138 pub fn public_keys(&self) -> Vec<[u8; 32]> {
139 self.keys.iter().map(|k| k.public).collect()
140 }
141
142 pub fn check_separation(&self, other: &[[u8; 32]]) -> Result<(), ServerError> {
147 if self.keys.iter().any(|k| other.contains(&k.public)) {
148 return Err(invalid("admin credentials must use distinct keys"));
149 }
150 Ok(())
151 }
152
153 pub(crate) fn verify(
154 &self,
155 path: &str,
156 headers: &Headers,
157 body: &BodyCapture,
158 now: i64,
159 ) -> Result<Verified, ServerError> {
160 let verified = self.verify_envelope(path, headers, now)?;
161 let body_digest = body.digest();
162 if !bool::from(verified.digest.as_bytes().ct_eq(body_digest.as_bytes())) {
163 return Err(unauth("invalid admin envelope"));
164 }
165 Ok(verified)
166 }
167
168 pub(crate) fn verify_envelope(
169 &self,
170 path: &str,
171 headers: &Headers,
172 now: i64,
173 ) -> Result<Verified, ServerError> {
174 check_headers(headers)?;
175 if !self.enabled() {
176 return Err(ServerError::unauthenticated("admin service disabled"));
177 }
178 let h = HEADER_NAMES
179 .map(|name| one(headers, name).ok_or_else(|| unauth("missing admin header")));
180 let [
181 version,
182 id,
183 audience,
184 created,
185 expiry,
186 nonce,
187 digest,
188 signature,
189 ] = h;
190 if version? != "1" {
191 return Err(unauth("unsupported admin version"));
192 }
193 let id = id?;
194 let audience = audience?;
195 let created = created?;
196 let expiry = expiry?;
197 let nonce = nonce?;
198 let digest = digest?;
199 let signature = signature?;
200 if digest.len() != 69
201 || !digest.starts_with("body:")
202 || !digest[5..]
203 .bytes()
204 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
205 {
206 return Err(unauth("invalid admin digest"));
207 }
208 let key = self
209 .keys
210 .iter()
211 .find(|k| k.id == id)
212 .ok_or_else(|| unauth("unknown admin key"))?;
213 let created_ms =
214 decimal_u64(created).ok_or_else(|| unauth("invalid admin creation time"))?;
215 let expiry_ms = decimal_u64(expiry).ok_or_else(|| unauth("invalid admin expiry time"))?;
216 let now_ms = u64::try_from(now).map_err(|_| unauth("invalid backend clock"))?;
217 let nonce_bytes = hex::<32>(nonce).ok_or_else(|| unauth("invalid admin nonce"))?;
218 let signature = hex::<64>(signature).ok_or_else(|| unauth("invalid admin signature"))?;
219 if audience != self.audience
220 || !path.starts_with(super::PREFIX)
221 || expiry_ms <= created_ms
222 || expiry_ms - created_ms > 300_000
223 || created_ms > now_ms.saturating_add(30_000)
224 || expiry_ms <= now_ms
225 || key
226 .before
227 .is_some_and(|b| i128::from(created_ms) < i128::from(b))
228 || key
229 .after
230 .is_some_and(|a| i128::from(created_ms) > i128::from(a))
231 || path == super::READ_PRESERVED_PATH
232 && (key.before.is_some_and(|b| now < b) || key.after.is_some_and(|a| now > a))
233 {
234 return Err(unauth("invalid admin envelope"));
235 }
236 let canonical = format!(
237 "mkit-admin:v1\n{id}\n{audience}\n{path}\n{digest}\n{created}\n{expiry}\n{nonce}"
238 );
239 let public =
240 VerifyingKey::from_bytes(&key.public).map_err(|_| unauth("invalid admin key"))?;
241 public
242 .verify_strict(
243 &hash(canonical.as_bytes()),
244 &Signature::from_bytes(&signature),
245 )
246 .map_err(|_| unauth("invalid admin signature"))?;
247 let replay_key = to_hex(&hash(
248 format!("{audience}\n{id}\n{}", to_hex(&nonce_bytes)).as_bytes(),
249 ));
250 Ok(Verified {
251 actor: id.to_owned(),
252 nonce: nonce.to_owned(),
253 digest: digest.to_owned(),
254 path: path.to_owned(),
255 replay_key,
256 expiry_ms,
257 roles: key.roles.clone(),
258 })
259 }
260}
261
262pub(crate) struct Verified {
263 pub actor: String,
264 pub nonce: String,
265 pub digest: String,
266 pub path: String,
267 pub replay_key: String,
268 pub expiry_ms: u64,
269 pub roles: BTreeSet<String>,
270}
271
272pub(crate) fn check_headers(headers: &Headers) -> Result<(), ServerError> {
273 let admin = headers
274 .iter()
275 .any(|(n, _)| n.to_ascii_lowercase().starts_with("x-mkit-admin-"));
276 if admin
277 && headers.iter().any(|(n, _)| {
278 auth_v2::HEADER_NAMES
279 .iter()
280 .any(|a| n.eq_ignore_ascii_case(a))
281 || n.eq_ignore_ascii_case("x-write-grant")
282 })
283 {
284 return Err(invalid("mixed admin and write credentials"));
285 }
286 if one(headers, HEADER_NAMES[0]) != Some("1") {
287 return Err(unauth("missing or unsupported admin version"));
288 }
289 for name in HEADER_NAMES {
290 if one(headers, name).is_none_or(|s| s.contains(',')) {
291 return Err(unauth("missing, repeated or joined admin header"));
292 }
293 }
294 Ok(())
295}
296
297fn one<'a>(headers: &'a Headers, name: &str) -> Option<&'a str> {
298 let mut values = headers.iter().filter(|(n, _)| n.eq_ignore_ascii_case(name));
299 let value = values.next()?.1.as_str();
300 values.next().is_none().then_some(value)
301}
302
303pub(crate) fn identifier(s: &str, max: usize, operation: bool) -> bool {
304 !s.is_empty()
305 && s.len() <= max
306 && s.bytes().all(|b| {
307 b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-') || operation && b == b':'
308 })
309}
310pub(crate) fn decimal_u64(s: &str) -> Option<u64> {
311 s.parse::<u64>().ok().filter(|n| n.to_string() == s)
312}
313fn decimal_i64(s: &str) -> Option<i64> {
314 s.parse::<i64>().ok().filter(|n| n.to_string() == s)
315}
316pub(crate) fn hex<const N: usize>(s: &str) -> Option<[u8; N]> {
317 if s.len() != 2 * N
318 || !s
319 .bytes()
320 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
321 {
322 return None;
323 }
324 let mut out = [0; N];
325 for (i, b) in out.iter_mut().enumerate() {
326 *b = u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).ok()?;
327 }
328 Some(out)
329}
330pub(crate) fn invalid(message: &str) -> ServerError {
331 ServerError::invalid_argument(message.to_owned())
332}
333fn unauth(message: &str) -> ServerError {
334 ServerError::unauthenticated(message.to_owned())
335}