1use aws_lc_rs::constant_time;
4use aws_lc_rs::digest::{SHA256, digest};
5use base64::Engine;
6use base64::engine::general_purpose::URL_SAFE_NO_PAD;
7
8use crate::ids::{KeyId, TenantId};
9use crate::tenants::Tenants;
10
11const PREFIX: &str = "vcs_";
14
15const SECRET_LEN: usize = 32;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum Scope {
21 Write,
22 Read,
23 Delete,
24 Legal,
25 Audit,
27 Admin,
28}
29
30impl Scope {
31 const ALL: [Scope; 6] = [
33 Scope::Write,
34 Scope::Read,
35 Scope::Delete,
36 Scope::Legal,
37 Scope::Audit,
38 Scope::Admin,
39 ];
40
41 pub fn named(name: &str) -> Option<Scope> {
43 Scope::ALL.into_iter().find(|s| s.name() == name)
44 }
45
46 fn bit(self) -> u8 {
47 1 << self as u8
48 }
49
50 fn name(self) -> &'static str {
51 match self {
52 Scope::Write => "write",
53 Scope::Read => "read",
54 Scope::Delete => "delete",
55 Scope::Legal => "legal",
56 Scope::Audit => "audit",
57 Scope::Admin => "admin",
58 }
59 }
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
64pub struct Scopes(u8);
65
66impl Scopes {
67 pub fn of(scopes: &[Scope]) -> Scopes {
68 Scopes(scopes.iter().fold(0, |bits, scope| bits | scope.bit()))
69 }
70
71 pub fn contains(self, scope: Scope) -> bool {
72 self.0 & scope.bit() != 0
73 }
74
75 pub fn is_empty(self) -> bool {
76 self.0 == 0
77 }
78
79 pub fn named(names: &[String]) -> Option<Scopes> {
83 let mut held = Vec::new();
84 for name in names {
85 let scope = Scope::named(name)?;
86 if held.contains(&scope) {
87 return None;
88 }
89 held.push(scope);
90 }
91 Some(Scopes::of(&held))
92 }
93
94 pub fn parse(text: &str) -> Option<Scopes> {
97 let scopes: Vec<Scope> = text.split(',').map(Scope::named).collect::<Option<_>>()?;
98 let read = Scopes::of(&scopes);
99 (read.to_string() == text).then_some(read)
100 }
101}
102
103impl Scopes {
104 pub fn names(self) -> Vec<&'static str> {
107 Scope::ALL
108 .into_iter()
109 .filter(|s| self.contains(*s))
110 .map(Scope::name)
111 .collect()
112 }
113}
114
115impl std::fmt::Display for Scopes {
116 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117 f.write_str(&self.names().join(","))
118 }
119}
120
121pub(crate) fn new_token() -> (KeyId, String) {
124 let id = KeyId::generate();
125 let mut secret = [0; SECRET_LEN];
126 aws_lc_rs::rand::fill(&mut secret).expect("AWS-LC aborts instead of failing");
129 let token = format!("{PREFIX}{id}_{}", URL_SAFE_NO_PAD.encode(secret));
130 (id, token)
131}
132
133pub(crate) fn token_hash(token: &str) -> [u8; 32] {
135 digest(&SHA256, token.as_bytes())
136 .as_ref()
137 .try_into()
138 .expect("SHA-256 is 32 bytes")
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144pub struct Caller {
145 pub key: KeyId,
146 pub tenant: Option<TenantId>,
147 pub scopes: Scopes,
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub enum AuthError {
153 Missing,
155 Forbidden(Scope),
157 Malformed,
159 Unknown,
161 Revoked,
163 Io,
165}
166
167pub fn authenticate(keys: &Tenants, authorization: Option<&str>) -> Result<Caller, AuthError> {
174 match check(keys, authorization) {
175 Ok(caller) => Ok(caller),
176 Err((error, known)) => {
177 if error != AuthError::Io {
178 keys.count_auth_failure(known);
179 }
180 Err(error)
181 }
182 }
183}
184
185type Failure = (AuthError, Option<(KeyId, Option<TenantId>)>);
187
188fn check(keys: &Tenants, authorization: Option<&str>) -> Result<Caller, Failure> {
189 let unknown = |error| (error, None);
190 let header = authorization.ok_or(unknown(AuthError::Missing))?;
191 let token = bearer(header).ok_or(unknown(AuthError::Malformed))?;
192 let id = parse(token).map_err(unknown)?;
193 let key = keys
194 .stored_key(id)
195 .map_err(|_| unknown(AuthError::Io))?
196 .ok_or(unknown(AuthError::Unknown))?;
197 let known = |error| (error, Some((id, key.tenant)));
198 constant_time::verify_slices_are_equal(&token_hash(token), &key.hash)
201 .map_err(|_| known(AuthError::Unknown))?;
202 if key.revoked {
203 return Err(known(AuthError::Revoked));
204 }
205 Ok(Caller {
206 key: id,
207 tenant: key.tenant,
208 scopes: key.scopes,
209 })
210}
211
212pub fn require(caller: &Caller, scope: Scope) -> Result<(), AuthError> {
216 let of_its_kind = (scope == Scope::Admin) == caller.tenant.is_none();
217 if of_its_kind && caller.scopes.contains(scope) {
218 Ok(())
219 } else {
220 Err(AuthError::Forbidden(scope))
221 }
222}
223
224pub fn tenant_for(caller: &Caller, scope: Scope) -> Result<TenantId, AuthError> {
227 require(caller, scope)?;
228 caller.tenant.ok_or(AuthError::Forbidden(scope))
229}
230
231fn bearer(header: &str) -> Option<&str> {
234 let (scheme, rest) = header.split_once(' ')?;
235 scheme
236 .eq_ignore_ascii_case("Bearer")
237 .then(|| rest.trim_start_matches(' '))
238}
239
240pub fn parse(token: &str) -> Result<KeyId, AuthError> {
244 let malformed = AuthError::Malformed;
245 let rest = token.strip_prefix(PREFIX).ok_or(malformed)?;
246 let (id, rest) = rest.split_at_checked(16).ok_or(malformed)?;
247 let secret = rest.strip_prefix('_').ok_or(malformed)?;
248 let id = KeyId::parse(id).ok_or(malformed)?;
249 let secret = URL_SAFE_NO_PAD.decode(secret).map_err(|_| malformed)?;
252 if secret.len() != SECRET_LEN {
253 return Err(malformed);
254 }
255 Ok(id)
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
263 fn scopes_are_named_in_their_fixed_order() {
264 let scopes = Scopes::of(&[Scope::Legal, Scope::Write, Scope::Read]);
265 assert_eq!(scopes.names(), ["write", "read", "legal"]);
266 assert_eq!(Scopes::of(&[Scope::Admin]).names(), ["admin"]);
267 }
268
269 const ID: &str = "0123456789abcdef";
270
271 fn secret(byte: u8) -> String {
272 URL_SAFE_NO_PAD.encode([byte; 32])
273 }
274
275 #[test]
276 fn a_well_formed_token_gives_its_key_id() {
277 let id = KeyId([0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]);
278 let secrets = [
279 secret(0x00),
280 secret(0xff),
281 URL_SAFE_NO_PAD.encode([0xfb; 32]),
283 URL_SAFE_NO_PAD.encode([[0xff; 16], [0x00; 16]].concat()),
284 ];
285 for secret in secrets {
286 assert_eq!(secret.len(), 43);
287 let token = format!("vcs_{ID}_{secret}");
288 assert_eq!(parse(&token), Ok(id), "{token}");
289 }
290 assert!(
291 secret(0xff).contains('_'),
292 "a secret may hold the separator"
293 );
294 }
295
296 #[test]
297 fn a_malformed_token_is_refused() {
298 let good = secret(0x42);
299 let malformed = [
300 String::new(),
301 "vcs_".to_owned(),
302 format!("{ID}_{good}"),
303 format!("vck_{ID}_{good}"),
304 format!("VCS_{ID}_{good}"),
305 format!("vcs_{}_{good}", &ID[..15]),
306 format!("vcs_{ID}0_{good}"),
307 format!("vcs_{}_{good}", ID.to_uppercase()),
308 format!("vcs_0123456789abcdeg_{good}"),
309 format!("vcs_{ID}{good}"),
310 format!("vcs_{ID}-{good}"),
311 format!("vcs_{ID}_{}", &good[..42]),
312 format!("vcs_{ID}_{good}A"),
313 format!("vcs_{ID}_{good}="),
314 format!("vcs_{ID}_{}", good.replace('Q', "+")),
315 format!("vcs_{ID}_{}+", &good[..42]),
316 format!("vcs_{ID}_{}B", &good[..42]),
317 format!(" vcs_{ID}_{good}"),
318 format!("vcs_{ID}_{good} "),
319 format!("vcs_{ID}__{good}"),
320 format!("vcs_\u{0661}123456789abcdef_{good}"),
321 ];
322 for token in malformed {
323 assert_eq!(parse(&token), Err(AuthError::Malformed), "{token:?}");
324 }
325 }
326
327 #[test]
328 fn scopes_are_a_set_written_in_a_fixed_order() {
329 let none = Scopes::of(&[]);
330 assert!(none.is_empty());
331 let scopes = Scopes::of(&[Scope::Delete, Scope::Write, Scope::Delete]);
332 assert!(!scopes.is_empty());
333 for scope in Scope::ALL {
334 assert_eq!(
335 scopes.contains(scope),
336 matches!(scope, Scope::Write | Scope::Delete),
337 "{scope:?}"
338 );
339 }
340 assert_eq!(scopes.to_string(), "write,delete");
341 assert_eq!(
342 Scopes::of(&Scope::ALL).to_string(),
343 "write,read,delete,legal,audit,admin"
344 );
345 for scope in Scope::ALL {
346 let one = Scopes::of(&[scope]);
347 assert_eq!(Scopes::parse(&one.to_string()), Some(one), "{scope:?}");
348 }
349 let all = Scopes::of(&Scope::ALL);
350 assert_eq!(Scopes::parse(&all.to_string()), Some(all));
351 }
352
353 #[test]
354 fn only_the_written_spelling_of_scopes_is_read() {
355 for text in [
356 "",
357 "delete,write",
358 "write,write",
359 "write,",
360 ",write",
361 "write, read",
362 "Write",
363 "owner",
364 "write,read,",
365 ] {
366 assert_eq!(Scopes::parse(text), None, "{text:?}");
367 }
368 }
369
370 #[test]
372 fn audit_is_required_of_a_tenant_key_only() {
373 let tenant = Caller {
374 key: KeyId::generate(),
375 tenant: Some(TenantId::generate()),
376 scopes: Scopes::of(&[Scope::Audit]),
377 };
378 assert_eq!(Scope::named("audit"), Some(Scope::Audit));
379 assert_eq!(require(&tenant, Scope::Audit), Ok(()));
380 assert_eq!(
381 tenant_for(&tenant, Scope::Audit),
382 Ok(tenant.tenant.expect("one"))
383 );
384 assert_eq!(
385 require(&tenant, Scope::Read),
386 Err(AuthError::Forbidden(Scope::Read))
387 );
388 let operator = Caller {
389 tenant: None,
390 scopes: Scopes::of(&[Scope::Admin, Scope::Audit]),
391 ..tenant
392 };
393 assert_eq!(
394 require(&operator, Scope::Audit),
395 Err(AuthError::Forbidden(Scope::Audit))
396 );
397 }
398
399 #[test]
400 fn a_scope_is_found_by_its_exact_name() {
401 for scope in Scope::ALL {
402 assert_eq!(Scope::named(scope.name()), Some(scope), "{scope:?}");
403 }
404 for name in ["", "Read", "READ", " read", "read,write", "superuser"] {
405 assert_eq!(Scope::named(name), None, "{name:?}");
406 }
407 }
408
409 #[test]
410 fn a_new_token_is_well_formed_and_new_each_time() {
411 let (id, token) = new_token();
412 assert_eq!(parse(&token), Ok(id));
413 let (other_id, other) = new_token();
414 assert_ne!(other_id, id);
415 assert_ne!(other[21..], token[21..], "a new secret");
416 }
417
418 #[test]
420 fn a_token_is_stored_as_its_sha_256() {
421 assert_eq!(
422 token_hash("abc")
423 .iter()
424 .map(|b| format!("{b:02x}"))
425 .collect::<String>(),
426 "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
427 );
428 }
429
430 mod authenticate {
431 use super::*;
432 use crate::audit::Actor;
433 use crate::tenants::{Fixture, NewKey, fixture};
434
435 fn vcon_scopes() -> Scopes {
436 Scopes::of(&[Scope::Write, Scope::Read])
437 }
438
439 fn key(f: &Fixture) -> (TenantId, NewKey) {
440 let t = f.tenants.create(Actor::Cli, "acme", None).expect("made");
441 let key = f
442 .tenants
443 .create_key(Actor::Cli, Some(t), vcon_scopes(), "x")
444 .expect("made");
445 (t, key)
446 }
447
448 fn auth(f: &Fixture, header: &str) -> Result<Caller, AuthError> {
449 authenticate(&f.tenants, Some(header))
450 }
451
452 #[test]
453 fn a_valid_token_names_its_caller() {
454 let f = fixture();
455 let (t, key) = key(&f);
456 let caller = Caller {
457 key: key.id,
458 tenant: Some(t),
459 scopes: vcon_scopes(),
460 };
461 for header in [
462 format!("Bearer {}", key.token),
463 format!("bearer {}", key.token),
464 format!("BEARER {}", key.token),
465 ] {
466 assert_eq!(auth(&f, &header), Ok(caller), "{header}");
467 }
468 let operator = f
469 .tenants
470 .create_key(Actor::Cli, None, Scopes::of(&[Scope::Admin]), "op")
471 .expect("made");
472 assert_eq!(
473 auth(&f, &format!("Bearer {}", operator.token)),
474 Ok(Caller {
475 key: operator.id,
476 tenant: None,
477 scopes: Scopes::of(&[Scope::Admin]),
478 })
479 );
480 }
481
482 #[test]
483 fn a_missing_or_malformed_header_is_refused() {
484 let f = fixture();
485 let (_, key) = key(&f);
486 assert_eq!(authenticate(&f.tenants, None), Err(AuthError::Missing));
487 for header in [
488 String::new(),
489 "Bearer".to_owned(),
490 "Bearer ".to_owned(),
491 format!("Bearer{}", key.token),
492 format!("Basic {}", key.token),
493 format!("Bearers {}", key.token),
494 format!("Bearer {} ", key.token),
495 format!("Bearer\t{}", key.token),
496 "Bearer vcs_0123456789abcdef_short".to_owned(),
497 ] {
498 assert_eq!(auth(&f, &header), Err(AuthError::Malformed), "{header:?}");
499 }
500 }
501
502 #[test]
503 fn an_unknown_key_or_a_wrong_secret_is_unknown() {
504 let f = fixture();
505 let (_, key) = key(&f);
506 let (_, stranger) = new_token();
507 assert_eq!(
508 auth(&f, &format!("Bearer {stranger}")),
509 Err(AuthError::Unknown)
510 );
511 let (_, other) = new_token();
512 let forged = format!("{}{}", &key.token[..21], &other[21..]);
513 assert_eq!(
514 auth(&f, &format!("Bearer {forged}")),
515 Err(AuthError::Unknown)
516 );
517 f.tenants.revoke_key(Actor::Cli, key.id).expect("revoked");
518 assert_eq!(
519 auth(&f, &format!("Bearer {forged}")),
520 Err(AuthError::Unknown),
521 "a wrong secret learns nothing about revocation"
522 );
523 }
524
525 #[test]
526 fn a_revoked_key_is_refused_on_its_next_call() {
527 let f = fixture();
528 let (_, key) = key(&f);
529 let header = format!("Bearer {}", key.token);
530 assert!(auth(&f, &header).is_ok());
531 f.tenants.revoke_key(Actor::Cli, key.id).expect("revoked");
532 assert_eq!(auth(&f, &header), Err(AuthError::Revoked));
533 }
534
535 fn admin_failures(f: &Fixture) -> Vec<serde_json::Value> {
536 f.admin_records("auth_failed")
537 }
538
539 fn tenant_failures(f: &Fixture, t: TenantId) -> Vec<serde_json::Value> {
540 crate::audit::testing::exported(&f.tenant_catalog(t), None)
541 .into_iter()
542 .filter(|r| r["action"] == "auth_failed")
543 .collect()
544 }
545
546 fn forged(key: &NewKey) -> String {
547 let (_, other) = new_token();
548 format!("Bearer {}{}", &key.token[..21], &other[21..])
549 }
550
551 #[test]
552 fn a_burst_of_bad_tokens_leaves_one_counted_record() {
553 let f = fixture();
554 let (t, _) = key(&f);
555 for _ in 0..20 {
556 assert!(auth(&f, "Bearer not-a-token").is_err());
557 }
558 for _ in 0..5 {
559 assert!(authenticate(&f.tenants, None).is_err());
560 let (_, made_up) = new_token();
561 assert!(auth(&f, &format!("Bearer {made_up}")).is_err());
562 }
563 let records = admin_failures(&f);
564 assert_eq!(records.len(), 1, "{records:?}");
565 assert_eq!(records[0]["actor"], "");
566 assert_eq!(records[0]["status"], 401);
567 assert_eq!(records[0]["count"], 30);
568 assert_eq!(records[0]["at"], "2026-10-23T12:00:00Z");
569 assert_eq!(tenant_failures(&f, t), Vec::<serde_json::Value>::new());
570
571 f.clock
572 .set(time::macros::datetime!(2026-10-23 12:01:30 UTC));
573 assert!(auth(&f, "Bearer not-a-token").is_err());
574 let records = admin_failures(&f);
575 assert_eq!(records.len(), 2, "a new minute, a new record");
576 assert_eq!(records[1]["count"], 1);
577 }
578
579 #[test]
580 fn a_known_key_that_fails_is_counted_in_its_tenants_log() {
581 let f = fixture();
582 let (t, key) = key(&f);
583 for _ in 0..3 {
584 assert_eq!(auth(&f, &forged(&key)), Err(AuthError::Unknown));
585 }
586 f.tenants.revoke_key(Actor::Cli, key.id).expect("revoked");
587 for _ in 0..2 {
588 assert_eq!(
589 auth(&f, &format!("Bearer {}", key.token)),
590 Err(AuthError::Revoked)
591 );
592 }
593 let records = tenant_failures(&f, t);
594 assert_eq!(records.len(), 1, "{records:?}");
595 assert_eq!(records[0]["actor"], key.id.to_string());
596 assert_eq!(records[0]["status"], 401);
597 assert_eq!(records[0]["count"], 5);
598 assert_eq!(admin_failures(&f), Vec::<serde_json::Value>::new());
599 }
600
601 #[test]
602 fn an_operator_key_that_fails_is_counted_in_the_admin_log() {
603 let f = fixture();
604 let operator = f
605 .tenants
606 .create_key(Actor::Cli, None, Scopes::of(&[Scope::Admin]), "op")
607 .expect("made");
608 assert!(auth(&f, &forged(&operator)).is_err());
609 let records = admin_failures(&f);
610 assert_eq!(records.len(), 1);
611 assert_eq!(records[0]["actor"], operator.id.to_string());
612 }
613
614 #[test]
615 fn a_known_key_whose_tenant_log_is_gone_is_counted_in_the_admin_log() {
616 let f = fixture();
617 let (t, key) = key(&f);
618 f.tenants
619 .remove(Actor::Cli, t, crate::tenants::Removal::Force)
620 .expect("removed");
621 assert_eq!(
622 auth(&f, &format!("Bearer {}", key.token)),
623 Err(AuthError::Revoked)
624 );
625 let records = admin_failures(&f);
626 assert_eq!(records.len(), 1);
627 assert_eq!(records[0]["actor"], key.id.to_string());
628 }
629
630 #[test]
631 fn a_failure_that_cannot_be_counted_is_refused_all_the_same() {
632 let f = fixture();
633 f.conn()
634 .execute_batch(
635 "CREATE TEMP TRIGGER no_audit BEFORE INSERT ON audit
636 BEGIN SELECT RAISE(ABORT, 'no audit'); END;",
637 )
638 .expect("trigger");
639 assert_eq!(auth(&f, "Bearer not-a-token"), Err(AuthError::Malformed));
640 }
641
642 #[test]
643 fn a_successful_check_counts_nothing() {
644 let f = fixture();
645 let (t, key) = key(&f);
646 assert!(auth(&f, &format!("Bearer {}", key.token)).is_ok());
647 assert_eq!(admin_failures(&f), Vec::<serde_json::Value>::new());
648 assert_eq!(tenant_failures(&f, t), Vec::<serde_json::Value>::new());
649 }
650
651 #[test]
652 fn a_key_table_that_cannot_be_read_lets_nothing_through() {
653 let f = fixture();
654 let (_, key) = key(&f);
655 let header = format!("Bearer {}", key.token);
656 f.conn()
657 .execute(
658 "UPDATE api_key SET scopes = 'owner' WHERE id = ?1",
659 [key.id.bytes()],
660 )
661 .expect("damage");
662 assert_eq!(auth(&f, &header), Err(AuthError::Io), "damaged scopes");
663 f.conn()
664 .execute_batch("ALTER TABLE api_key RENAME TO gone")
665 .expect("lose the table");
666 assert_eq!(auth(&f, &header), Err(AuthError::Io), "no table");
667 assert_eq!(
668 admin_failures(&f),
669 Vec::<serde_json::Value>::new(),
670 "an unreadable table is not a failed check"
671 );
672 }
673 }
674
675 mod scopes {
676 use super::*;
677
678 const TENANT: TenantId = TenantId([1; 16]);
679
680 fn caller(tenant: Option<TenantId>, scopes: &[Scope]) -> Caller {
681 Caller {
682 key: KeyId([2; 8]),
683 tenant,
684 scopes: Scopes::of(scopes),
685 }
686 }
687
688 #[test]
690 fn each_key_may_use_only_the_scopes_it_holds_of_its_kind() {
691 let vcon = [Scope::Write, Scope::Read, Scope::Delete, Scope::Legal];
692 let matrix = [
693 (caller(Some(TENANT), &[Scope::Write]), vec![Scope::Write]),
694 (
695 caller(Some(TENANT), &[Scope::Read, Scope::Delete]),
696 vec![Scope::Read, Scope::Delete],
697 ),
698 (caller(Some(TENANT), &[Scope::Legal]), vec![Scope::Legal]),
699 (caller(Some(TENANT), &vcon), vcon.to_vec()),
700 (caller(None, &[Scope::Admin]), vec![Scope::Admin]),
701 (
704 caller(Some(TENANT), &[Scope::Admin, Scope::Read]),
705 vec![Scope::Read],
706 ),
707 (
708 caller(None, &[Scope::Admin, Scope::Write]),
709 vec![Scope::Admin],
710 ),
711 ];
712 for (who, allowed) in matrix {
713 for scope in Scope::ALL {
714 let expected = if allowed.contains(&scope) {
715 Ok(())
716 } else {
717 Err(AuthError::Forbidden(scope))
718 };
719 assert_eq!(require(&who, scope), expected, "{who:?} {scope:?}");
720 }
721 }
722 }
723
724 #[test]
725 fn a_request_acts_on_the_tenant_of_its_key() {
726 let tenant_key = caller(Some(TENANT), &[Scope::Write]);
727 assert_eq!(tenant_for(&tenant_key, Scope::Write), Ok(TENANT));
728 assert_eq!(
729 tenant_for(&tenant_key, Scope::Read),
730 Err(AuthError::Forbidden(Scope::Read))
731 );
732 }
733
734 #[test]
735 fn an_operator_key_acts_on_no_tenant() {
736 let operator = caller(None, &[Scope::Admin, Scope::Write]);
737 for scope in Scope::ALL {
738 assert_eq!(
739 tenant_for(&operator, scope),
740 Err(AuthError::Forbidden(scope)),
741 "{scope:?}"
742 );
743 }
744 }
745 }
746}