1use std::time::{Duration, Instant};
2
3use biscuit_auth::error::{Logic, RunLimit, Token as TokenError};
4use biscuit_auth::{AuthorizerBuilder, AuthorizerLimits, Biscuit, PublicKey};
5
6use crate::revocation::hex;
7use crate::vocabulary::{request_facts, time_fact, unix_seconds};
8use crate::{AccessRequest, Error, RevocationList};
9
10pub const DEFAULT_POLICY: &str = "allow if server($s), user($u), right($s, $u);";
14
15pub const MAX_TOKEN_LEN: usize = 16 * 1024;
18
19pub const MAX_LIFETIME: Duration = Duration::from_secs(600);
22
23const LIMITS: AuthorizerLimits = AuthorizerLimits {
27 max_facts: 1_000,
28 max_iterations: 100,
29 max_time: Duration::from_millis(20),
30};
31
32const BUDGET: Duration = Duration::from_millis(500);
34
35#[derive(Debug, Clone)]
37pub struct Verifier {
38 root_key: PublicKey,
39 policies: AuthorizerBuilder,
40}
41
42#[derive(Debug)]
48pub struct Authorization {
49 request: AccessRequest,
50 revocation_ids: Vec<String>,
51 valid_from: u64,
52 valid_until: u64,
53}
54
55impl Authorization {
56 pub fn request(&self) -> &AccessRequest {
58 &self.request
59 }
60
61 pub fn revocation_ids(&self) -> &[String] {
64 &self.revocation_ids
65 }
66
67 pub fn token_id(&self) -> &str {
71 self.revocation_ids.last().map_or("", String::as_str)
73 }
74
75 pub fn valid_from(&self) -> u64 {
78 self.valid_from
79 }
80
81 pub fn valid_until(&self) -> u64 {
85 self.valid_until
86 }
87}
88
89impl Verifier {
90 pub fn new(root_key: PublicKey, policies: &str) -> Result<Self, Error> {
93 let policies = AuthorizerBuilder::new()
94 .code(policies)
95 .map_err(Error::Datalog)?
96 .set_limits(LIMITS);
97 Ok(Self { root_key, policies })
98 }
99
100 pub fn root_key(&self) -> PublicKey {
102 self.root_key
103 }
104
105 pub fn authorize(
117 &self,
118 token: &str,
119 request: &AccessRequest,
120 revoked: &RevocationList,
121 lifetime: Duration,
122 ) -> Result<Authorization, Error> {
123 let deadline = Instant::now() + BUDGET;
124 if lifetime < Duration::from_secs(1) || lifetime > MAX_LIFETIME {
125 return Err(Error::invalid("lifetime", "must be between 1 second and 10 minutes"));
126 }
127 let start = unix_seconds(request.time)?;
128 let token = token.trim();
129 if token.len() > MAX_TOKEN_LEN {
130 return Err(Error::invalid("token", "too long"));
131 }
132 let token = Biscuit::from_base64(token, self.root_key).map_err(Error::InvalidToken)?;
133
134 let revocation_ids: Vec<String> = token.revocation_identifiers().iter().map(|id| hex(id)).collect();
135 if let Some(id) = revoked.find(&revocation_ids) {
136 return Err(Error::Revoked(id.clone()));
137 }
138
139 let mut context = self.policies.clone();
140 for fact in request_facts(request) {
141 context = context.fact(fact).map_err(Error::Datalog)?;
142 }
143 let evaluate = |second| context.clone().fact(time_fact(second))?.build(&token)?.authorize();
144
145 evaluate(start).map_err(refusal)?;
146 let end = start
147 .checked_add(lifetime.as_secs())
148 .ok_or(Error::invalid("time", "too far in the future"))?;
149 let mut valid_until = end;
150 for second in start + 1..end {
151 if Instant::now() > deadline {
152 return Err(Error::Overloaded);
153 }
154 match evaluate(second) {
155 Ok(_) => {}
156 Err(TokenError::FailedLogic(Logic::Unauthorized { .. } | Logic::NoMatchingPolicy { .. })) => {
157 valid_until = second;
158 break;
159 }
160 Err(e) => return Err(refusal(e)),
161 }
162 }
163 Ok(Authorization {
164 request: request.clone(),
165 revocation_ids,
166 valid_from: start,
167 valid_until,
168 })
169 }
170}
171
172fn refusal(error: TokenError) -> Error {
179 match error {
180 TokenError::RunLimit(RunLimit::Timeout) => Error::Overloaded,
181 error => Error::Denied(error),
182 }
183}
184
185pub fn failed_checks(root_key: PublicKey, token: &str, request: &AccessRequest) -> Result<Vec<String>, Error> {
194 let verifier = Verifier::new(root_key, "allow if true;")?;
195 let result = verifier.authorize(token, request, &RevocationList::default(), Duration::from_secs(1));
196 match result {
197 Ok(_) => Ok(Vec::new()),
198 Err(Error::Denied(TokenError::FailedLogic(
199 Logic::Unauthorized { checks, .. } | Logic::NoMatchingPolicy { checks },
200 ))) => Ok(checks.iter().map(ToString::to_string).collect()),
201 Err(e) => Err(e),
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use biscuit_auth::{BlockBuilder, KeyPair, UnverifiedBiscuit};
208
209 use std::time::{SystemTime, UNIX_EPOCH};
210
211 use super::*;
212 use crate::{Mode, Network, Restriction, Right, attenuate, mint};
213
214 const KEY: &str = "SHA256:test";
215 const LIFETIME: Duration = Duration::from_secs(120);
216 const T0: u64 = 2_000_000_000;
219
220 fn at(seconds: u64) -> SystemTime {
221 UNIX_EPOCH + Duration::from_secs(seconds)
222 }
223
224 fn request(server: &str, user: &str, ip: &str) -> AccessRequest {
225 AccessRequest {
226 time: at(T0),
227 source_ip: ip.parse().unwrap(),
228 server: server.parse().unwrap(),
229 user: user.parse().unwrap(),
230 command: None,
231 public_key: KEY.to_owned(),
232 mode: Mode::Certificate,
233 }
234 }
235
236 fn setup(restrictions: &[Restriction]) -> (Verifier, KeyPair, String) {
237 let root = KeyPair::new();
238 let rights: Vec<Right> = vec!["web-1:deploy".parse().unwrap(), "web-2:admin".parse().unwrap()];
239 let token = mint(&root, &rights, restrictions).unwrap();
240 (Verifier::new(root.public(), DEFAULT_POLICY).unwrap(), root, token)
241 }
242
243 fn authorize(verifier: &Verifier, token: &str, request: &AccessRequest) -> Result<Authorization, Error> {
244 verifier.authorize(token, request, &RevocationList::default(), LIFETIME)
245 }
246
247 fn allowed(verifier: &Verifier, token: &str, request: &AccessRequest) -> bool {
248 authorize(verifier, token, request).is_ok()
249 }
250
251 fn append_code(token: &str, code: &str) -> String {
253 let block = BlockBuilder::new().code(code).unwrap();
254 let token = UnverifiedBiscuit::from_base64(token).unwrap();
255 token.append(block).unwrap().to_base64().unwrap()
256 }
257
258 #[test]
259 fn tells_overload_from_refusal() {
260 assert!(matches!(
261 refusal(TokenError::RunLimit(RunLimit::Timeout)),
262 Error::Overloaded
263 ));
264 assert_eq!(Error::Overloaded.kind(), "overloaded");
265 for limit in [RunLimit::TooManyFacts, RunLimit::TooManyIterations] {
266 assert!(matches!(refusal(TokenError::RunLimit(limit)), Error::Denied(_)));
267 }
268 }
269
270 #[test]
271 fn explains_refusals_with_the_token_checks_only() {
272 let (v, _, token) = setup(&[Restriction::Servers(vec!["web-2".parse().unwrap()])]);
273 let failed = failed_checks(v.root_key(), &token, &request("web-1", "deploy", "192.0.2.7")).unwrap();
274 assert_eq!(failed.len(), 1);
275 assert!(failed[0].contains("web-2"), "{failed:?}");
276
277 let failed = failed_checks(v.root_key(), &token, &request("web-2", "deploy", "192.0.2.7")).unwrap();
279 assert!(failed.is_empty());
280 assert!(!allowed(&v, &token, &request("web-2", "deploy", "192.0.2.7")));
281 }
282
283 #[test]
284 fn lists_only_authority_rights() {
285 let (v, _, token) = setup(&[]);
286 let forged = append_code(&token, r#"right("web-9", "root");"#);
287 let rights: Vec<String> = crate::granted_rights(v.root_key(), &forged)
288 .unwrap()
289 .iter()
290 .map(ToString::to_string)
291 .collect();
292 assert_eq!(rights, ["web-1:deploy", "web-2:admin"]);
293 let (_, other_root, _) = setup(&[]);
294 assert!(crate::granted_rights(other_root.public(), &token).is_err());
295 }
296
297 #[test]
298 fn grants_only_listed_rights() {
299 let (v, _, token) = setup(&[]);
300 assert!(allowed(&v, &token, &request("web-1", "deploy", "192.0.2.7")));
301 assert!(allowed(&v, &token, &request("web-2", "admin", "192.0.2.7")));
302 assert!(!allowed(&v, &token, &request("web-1", "admin", "192.0.2.7")));
303 assert!(!allowed(&v, &token, &request("web-3", "deploy", "192.0.2.7")));
304 }
305
306 #[test]
307 fn rejects_foreign_corrupt_or_oversized_tokens() {
308 let (v, _, _) = setup(&[]);
309 let (_, _, foreign) = setup(&[]);
310 let req = request("web-1", "deploy", "192.0.2.7");
311 assert!(matches!(authorize(&v, &foreign, &req), Err(Error::InvalidToken(_))));
312 assert!(matches!(authorize(&v, "garbage", &req), Err(Error::InvalidToken(_))));
313 let huge = "A".repeat(MAX_TOKEN_LEN + 1);
314 assert!(matches!(authorize(&v, &huge, &req), Err(Error::InvalidInput { .. })));
315 }
316
317 #[test]
318 fn rejects_invalid_times_and_lifetimes() {
319 let (v, _, token) = setup(&[]);
320 let mut req = request("web-1", "deploy", "192.0.2.7");
321 let none = RevocationList::default();
322 for lifetime in [
323 Duration::ZERO,
324 Duration::from_millis(999),
325 MAX_LIFETIME + Duration::from_secs(1),
326 ] {
327 assert!(matches!(
328 v.authorize(&token, &req, &none, lifetime),
329 Err(Error::InvalidInput { .. })
330 ));
331 }
332 req.time = UNIX_EPOCH - Duration::from_secs(1);
333 assert!(matches!(authorize(&v, &token, &req), Err(Error::InvalidInput { .. })));
334 assert!(Restriction::ExpiresAt(req.time).to_check().is_err());
335 }
336
337 #[test]
338 fn window_is_capped_by_expiration() {
339 let req = request("web-1", "deploy", "192.0.2.7");
340
341 let (v, _, token) = setup(&[]);
342 assert_eq!(authorize(&v, &token, &req).unwrap().valid_until(), T0 + 120);
343
344 let (v, _, token) = setup(&[Restriction::ExpiresAt(at(T0 + 30))]);
345 assert_eq!(authorize(&v, &token, &req).unwrap().valid_until(), T0 + 31);
346
347 let (v, _, token) = setup(&[Restriction::ExpiresAt(at(T0 - 1))]);
348 assert!(matches!(authorize(&v, &token, &req), Err(Error::Denied(_))));
349 }
350
351 #[test]
352 fn window_stops_at_first_refusal() {
353 let (v, _, token) = setup(&[]);
354 let gap = format!(
355 "check if time($t), $t < {} or time($t), $t > {};",
356 rfc3339(T0 + 10),
357 rfc3339(T0 + 20)
358 );
359 let token = append_code(&token, &gap);
360 let req = request("web-1", "deploy", "192.0.2.7");
361 assert_eq!(authorize(&v, &token, &req).unwrap().valid_until(), T0 + 10);
362 }
363
364 #[test]
365 fn forged_facts_do_not_bypass_earlier_checks() {
366 let (v, _, token) = setup(&[
367 Restriction::ExpiresAt(at(T0 - 1)),
368 Restriction::Servers(vec!["web-2".parse().unwrap()]),
369 ]);
370 let forged = append_code(&token, &format!(r#"time({}); server("web-2");"#, rfc3339(T0 - 10)));
371 assert!(!allowed(&v, &forged, &request("web-2", "admin", "192.0.2.7")));
372 }
373
374 #[test]
375 fn enforces_source_networks() {
376 let nets = vec![
377 "192.0.2.0/24".parse().unwrap(),
378 "2001:db8::/32".parse::<Network>().unwrap(),
379 ];
380 let (v, _, token) = setup(&[Restriction::SourceNetworks(nets)]);
381 assert!(allowed(&v, &token, &request("web-1", "deploy", "192.0.2.7")));
382 assert!(allowed(&v, &token, &request("web-1", "deploy", "::ffff:192.0.2.7")));
383 assert!(allowed(&v, &token, &request("web-1", "deploy", "2001:db8::1")));
384 assert!(!allowed(&v, &token, &request("web-1", "deploy", "198.51.100.1")));
385 }
386
387 #[test]
388 fn host_networks_match_whatever_the_spelling() {
389 for (restriction, client) in [
390 ("::ffff:192.0.2.7", "192.0.2.7"),
391 ("192.0.2.7", "::ffff:192.0.2.7"),
392 ("::ffff:192.0.2.7/128", "192.0.2.7"),
393 ("2001:DB8:0::1", "2001:db8::1"),
394 ("2001:db8::1/128", "2001:db8::1"),
395 ] {
396 let (v, _, token) = setup(&[Restriction::SourceNetworks(vec![restriction.parse().unwrap()])]);
397 assert!(
398 allowed(&v, &token, &request("web-1", "deploy", client)),
399 "{restriction} vs {client}"
400 );
401 }
402 }
403
404 #[test]
405 fn enforces_commands_and_keys() {
406 let (v, _, token) = setup(&[
407 Restriction::Commands(vec!["uptime".parse().unwrap()]),
408 Restriction::PublicKeys(vec![KEY.to_owned()]),
409 ]);
410 let mut req = request("web-1", "deploy", "192.0.2.7");
411 assert!(!allowed(&v, &token, &req), "an interactive session must be refused");
412 req.command = Some("uptime".parse().unwrap());
413 assert!(allowed(&v, &token, &req));
414 req.command = Some("reboot".parse().unwrap());
415 assert!(!allowed(&v, &token, &req));
416 req.command = Some("uptime".parse().unwrap());
417 req.public_key = "SHA256:other".to_owned();
418 assert!(!allowed(&v, &token, &req));
419 }
420
421 #[test]
422 fn attenuation_only_narrows() {
423 let (v, _, token) = setup(&[]);
424 let narrowed = attenuate(&token, &[Restriction::Servers(vec!["web-2".parse().unwrap()])]).unwrap();
425 assert!(allowed(&v, &narrowed, &request("web-2", "admin", "192.0.2.7")));
426 assert!(!allowed(&v, &narrowed, &request("web-1", "deploy", "192.0.2.7")));
427
428 let forged = append_code(&token, r#"right("web-9", "root");"#);
430 assert!(!allowed(&v, &forged, &request("web-9", "root", "192.0.2.7")));
431 }
432
433 #[test]
434 fn honors_revocation() {
435 let (v, _, token) = setup(&[]);
436 let narrowed = attenuate(&token, &[Restriction::Mode(Mode::Certificate)]).unwrap();
437 let req = request("web-1", "deploy", "192.0.2.7");
438 let auth = authorize(&v, &narrowed, &req).unwrap();
439 assert_eq!(auth.valid_from(), T0);
440 assert_eq!(auth.token_id(), auth.revocation_ids()[1]);
441 let ids = auth.revocation_ids();
442 assert_eq!(ids.len(), 2);
443
444 let revoked = RevocationList::parse(&ids[0]).unwrap();
446 for token in [&narrowed, &token] {
447 assert!(matches!(
448 v.authorize(token, &req, &revoked, LIFETIME),
449 Err(Error::Revoked(_))
450 ));
451 }
452 }
453
454 fn rfc3339(seconds: u64) -> String {
456 let check = Restriction::ExpiresAt(at(seconds)).to_check().unwrap().to_string();
457 check.rsplit(' ').next().unwrap().to_owned()
459 }
460}