1use std::num::NonZeroUsize;
2use std::sync::{Arc, Mutex};
3
4use hmac::{Hmac, Mac};
5use lru::LruCache;
6use orbit_core::Fleet;
7use serde::{Deserialize, Serialize};
8use sha2::Sha256;
9use zeroize::Zeroizing;
10
11use crate::keyring::{context, derive};
12use crate::table::{
13 CONSUMED, REVOKED, Record, RecordProtection, SESSION, Snapshot, StoreKey,
14 Table, find, insert, subject_sessions,
15};
16use crate::{
17 Authority, CacheKey, CachedPrincipal, Claims, Error, MemoryCache,
18 Principal, PrincipalCache, Purpose, Result, SecretKey, StateProtection,
19 ValidationHook, Validator,
20};
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
24pub struct SessionId(pub [u8; 16]);
25
26#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(deny_unknown_fields)]
28pub(crate) struct SessionBinding {
29 pub(crate) id: SessionId,
30 generation: u64,
31}
32
33pub struct Session {
36 binding: SessionBinding,
37 subject: String,
38 created_at: u64,
39 expires_at: u64,
40 realm: String,
41 issuer: String,
42}
43impl Session {
44 pub fn id(&self) -> SessionId {
45 self.binding.id
46 }
47 pub fn subject(&self) -> &str {
48 &self.subject
49 }
50 pub fn created_at(&self) -> u64 {
51 self.created_at
52 }
53 pub fn expires_at(&self) -> u64 {
54 self.expires_at
55 }
56 pub(crate) fn binding(&self) -> SessionBinding {
57 self.binding.clone()
58 }
59 pub(crate) fn check_issuance(
60 &self,
61 authority: &Authority,
62 claims: &Claims,
63 ) -> Result<()> {
64 if self.realm != authority.realm
65 || self.issuer != authority.issuer
66 || claims.subject != self.subject
67 || claims.issued_at < self.created_at
68 || claims.expires_at > self.expires_at
69 {
70 return Err(Error::InvalidInput);
71 }
72 Ok(())
73 }
74}
75
76struct SessionView {
77 snapshot: Snapshot,
78 subject: String,
80}
81
82pub struct FleetAuth<P: StateProtection> {
92 _fleet: Arc<Fleet>,
93 table: Table,
94 local: MemoryCache,
95 sessions: Mutex<LruCache<SessionId, SessionView>>,
96 state_key: Zeroizing<[u8; 32]>,
97 protection: RecordProtection<P>,
98 realm: String,
99 issuer: String,
100}
101impl<P: StateProtection> FleetAuth<P> {
102 pub fn new(
111 fleet: Arc<Fleet>,
112 authority: &Authority,
113 state_key: SecretKey,
114 local_capacity: NonZeroUsize,
115 _policy: P,
116 ) -> Result<Self> {
117 let info = context(&[
118 b"orbit-auth/v2/fleet-state",
119 fleet.name().as_bytes(),
120 authority.realm.as_bytes(),
121 authority.issuer.as_bytes(),
122 ]);
123 Ok(Self {
124 table: Table::new(fleet.clone(), P::ID)?,
125 protection: RecordProtection::new(derive(
129 &state_key,
130 &context(&[
131 b"orbit-auth/v4/fleet-state-protection",
132 fleet.name().as_bytes(),
133 &P::ID.to_le_bytes(),
134 ]),
135 )),
136 _fleet: fleet,
137 local: MemoryCache::new(local_capacity),
138 sessions: Mutex::new(LruCache::new(local_capacity)),
139 state_key: derive(&state_key, &info),
140 realm: authority.realm.clone(),
141 issuer: authority.issuer.clone(),
142 })
143 }
144
145 pub fn clear_local(&self) {
147 self.local.clear();
148 self.sessions.lock().unwrap_or_else(|e| e.into_inner()).clear();
149 }
150
151 pub fn create_session(
152 &self,
153 subject: impl Into<String>,
154 now: u64,
155 expires_at: u64,
156 ) -> Result<Session> {
157 self.create(subject.into(), now, expires_at, None)
158 }
159
160 pub fn create_session_with_limit(
164 &self,
165 subject: impl Into<String>,
166 now: u64,
167 expires_at: u64,
168 limit: NonZeroUsize,
169 ) -> Result<Session> {
170 self.create(subject.into(), now, expires_at, Some(limit))
171 }
172
173 fn create(
174 &self,
175 subject: String,
176 now: u64,
177 expires_at: u64,
178 limit: Option<NonZeroUsize>,
179 ) -> Result<Session> {
180 if !crate::principal::valid_label(&subject) || now >= expires_at {
181 return Err(Error::InvalidInput);
182 }
183 let mut id = [0; 16];
184 getrandom::fill(&mut id).map_err(|_| Error::Randomness)?;
185 let id = SessionId(id);
186 let record = Record {
187 key: self.session_key(id),
188 subject: self.subject_tag(&subject),
189 created_at: now,
190 expires_at,
191 generation: 0,
192 status: SESSION,
193 };
194 self.table.write(|slots| {
195 if find(slots, record.key, &self.protection)?.is_some() {
196 return Err(Error::InvalidInput);
197 }
198 let mut live = match limit {
199 Some(_) => subject_sessions(
200 slots,
201 record.subject,
202 now,
203 &self.protection,
204 )?,
205 None => Vec::new(),
206 };
207 insert(slots, record, now, &self.protection)?;
209 if let Some(limit) = limit {
210 live.sort_unstable_by_key(|session| {
211 (session.record.created_at, session.index)
212 });
213 let excess = live.len().saturating_sub(limit.get() - 1);
214 for session in live.into_iter().take(excess) {
215 let mut old = session.record;
216 old.status = REVOKED;
217 slots[session.index].store(
218 old,
219 session.index,
220 &self.protection,
221 )?;
222 }
223 }
224 Ok(())
225 })?;
226 Ok(self.handle(id, subject, record))
227 }
228
229 pub fn revoke_subject_sessions(
232 &self,
233 subject: &str,
234 now: u64,
235 ) -> Result<usize> {
236 if !crate::principal::valid_label(subject) {
237 return Err(Error::InvalidInput);
238 }
239 self.table.write(|slots| {
240 let live = subject_sessions(
241 slots,
242 self.subject_tag(subject),
243 now,
244 &self.protection,
245 )?;
246 for session in &live {
247 let mut record = session.record;
248 record.status = REVOKED;
249 slots[session.index].store(
250 record,
251 session.index,
252 &self.protection,
253 )?;
254 }
255 Ok(live.len())
256 })
257 }
258
259 pub fn extend_subject_sessions(
263 &self,
264 subject: &str,
265 now: u64,
266 horizon: impl Fn(u64, u64) -> u64,
267 ) -> Result<usize> {
268 if !crate::principal::valid_label(subject) {
269 return Err(Error::InvalidInput);
270 }
271 self.table.write(|slots| {
272 let live = subject_sessions(
273 slots,
274 self.subject_tag(subject),
275 now,
276 &self.protection,
277 )?;
278 let targets: Vec<_> = live
279 .into_iter()
280 .map(|session| {
281 let target = horizon(
282 session.record.created_at,
283 session.record.expires_at,
284 );
285 (session, target)
286 })
287 .collect();
288 if targets.iter().any(|(session, target)| {
289 *target < session.record.expires_at || *target <= now
290 }) {
291 return Err(Error::InvalidInput);
292 }
293 let mut changed = 0;
294 for (session, target) in targets {
295 if target > session.record.expires_at {
296 let mut record = session.record;
297 record.expires_at = target;
298 slots[session.index].store(
299 record,
300 session.index,
301 &self.protection,
302 )?;
303 changed += 1;
304 }
305 }
306 Ok(changed)
307 })
308 }
309
310 pub fn revoke_session(
314 &self,
315 id: SessionId,
316 now: u64,
317 ) -> Result<()> {
318 let key = self.session_key(id);
319 self.table.write(|slots| {
320 let snapshot =
321 find(slots, key, &self.protection)?.ok_or(Error::Revoked)?;
322 let mut record = snapshot.record;
323 if !matches!(record.status, SESSION | REVOKED) {
324 return Err(Error::PolicyUnavailable);
325 }
326 if now < record.created_at {
327 return Err(Error::NotYetValid);
328 }
329 record.status = REVOKED;
330 slots[snapshot.index].store(
331 record,
332 snapshot.index,
333 &self.protection,
334 )?;
335 Ok(())
336 })
337 }
338
339 pub fn session(
343 &self,
344 id: SessionId,
345 subject: &str,
346 now: u64,
347 ) -> Result<Session> {
348 if !crate::principal::valid_label(subject) {
349 return Err(Error::InvalidInput);
350 }
351 let snapshot = self
352 .table
353 .lookup(self.session_key(id), &self.protection)?
354 .ok_or(Error::Revoked)?;
355 if snapshot.record.subject != self.subject_tag(subject) {
356 return Err(Error::InvalidInput);
357 }
358 self.check_live(snapshot.record, now)?;
359 Ok(self.handle(id, subject.to_owned(), snapshot.record))
360 }
361
362 pub fn extend_session(
368 &self,
369 id: SessionId,
370 subject: &str,
371 now: u64,
372 expires_at: u64,
373 ) -> Result<Session> {
374 if !crate::principal::valid_label(subject) || now >= expires_at {
375 return Err(Error::InvalidInput);
376 }
377 let key = self.session_key(id);
378 let tag = self.subject_tag(subject);
379 self.table.write(|slots| {
380 let snapshot =
381 find(slots, key, &self.protection)?.ok_or(Error::Revoked)?;
382 let mut record = snapshot.record;
383 if record.subject != tag {
384 return Err(Error::InvalidInput);
385 }
386 self.check_live(record, now)?;
387 if expires_at < record.expires_at {
388 return Err(Error::InvalidInput);
389 }
390 if expires_at > record.expires_at {
391 record.expires_at = expires_at;
392 slots[snapshot.index].store(
393 record,
394 snapshot.index,
395 &self.protection,
396 )?;
397 }
398 Ok(self.handle(id, subject.to_owned(), record))
399 })
400 }
401
402 pub fn refresh(
408 &self,
409 validator: &Validator<'_>,
410 token: &str,
411 now: u64,
412 expires_at: u64,
413 ) -> Result<Session> {
414 if validator.validation.purpose != Purpose::Refresh {
415 return Err(Error::InvalidInput);
416 }
417 let principal = validator.validate(token, now, self)?;
418 let binding = principal.session.as_ref().ok_or(Error::InvalidToken)?;
419 let key = self.session_key(binding.id);
420 let subject = self.subject_tag(principal.subject());
421 self.table.write(|slots| {
422 let snapshot =
423 find(slots, key, &self.protection)?.ok_or(Error::Revoked)?;
424 let mut record = snapshot.record;
425 self.check_record(record, subject, &principal, now)?;
426 if expires_at < record.expires_at {
427 return Err(Error::InvalidInput);
428 }
429 record.generation = record
430 .generation
431 .checked_add(1)
432 .ok_or(Error::PolicyUnavailable)?;
433 record.expires_at = expires_at;
434 slots[snapshot.index].store(
435 record,
436 snapshot.index,
437 &self.protection,
438 )?;
439 Ok(self.handle(binding.id, principal.subject().to_owned(), record))
440 })
441 }
442
443 pub fn replay_guard(&self) -> ReplayGuard<'_, P> {
445 ReplayGuard(self)
446 }
447
448 fn handle(
449 &self,
450 id: SessionId,
451 subject: String,
452 record: Record,
453 ) -> Session {
454 Session {
455 binding: SessionBinding { id, generation: record.generation },
456 subject,
457 created_at: record.created_at,
458 expires_at: record.expires_at,
459 realm: self.realm.clone(),
460 issuer: self.issuer.clone(),
461 }
462 }
463 fn session_key(
464 &self,
465 id: SessionId,
466 ) -> StoreKey {
467 StoreKey(mac(self.state_key.as_ref(), &context(&[b"session", &id.0])))
468 }
469 fn subject_tag(
470 &self,
471 subject: &str,
472 ) -> [u64; 4] {
473 mac(
474 self.state_key.as_ref(),
475 &context(&[b"subject", subject.as_bytes()]),
476 )
477 }
478 fn check_scope(
479 &self,
480 principal: &Principal,
481 now: u64,
482 ) -> Result<()> {
483 self.check_scope_lapsed(principal, now)?;
484 if now >= principal.claims.expires_at {
485 return Err(Error::Expired);
486 }
487 Ok(())
488 }
489 fn check_scope_lapsed(
490 &self,
491 principal: &Principal,
492 now: u64,
493 ) -> Result<()> {
494 if principal.realm != self.realm || principal.issuer != self.issuer {
495 return Err(Error::InvalidToken);
496 }
497 if now < principal.claims.not_before || now < principal.claims.issued_at
498 {
499 return Err(Error::NotYetValid);
500 }
501 Ok(())
502 }
503 fn check_session(
505 &self,
506 principal: &Principal,
507 now: u64,
508 ) -> Result<()> {
509 let binding = principal.session.as_ref().ok_or(Error::InvalidToken)?;
510 {
511 let mut views =
512 self.sessions.lock().map_err(|_| Error::PolicyUnavailable)?;
513 if let Some(view) = views.get(&binding.id) {
514 if self.table.unchanged::<P>(&view.snapshot)? {
515 if view.subject != principal.subject() {
516 return Err(Error::InvalidToken);
517 }
518 return self.check_metadata(
519 view.snapshot.record,
520 principal,
521 now,
522 );
523 }
524 views.pop(&binding.id);
525 }
526 }
527 let key = self.session_key(binding.id);
528 let snapshot =
529 self.table.lookup(key, &self.protection)?.ok_or(Error::Revoked)?;
530 self.check_record(
531 snapshot.record,
532 self.subject_tag(principal.subject()),
533 principal,
534 now,
535 )?;
536 self.sessions.lock().map_err(|_| Error::PolicyUnavailable)?.put(
537 binding.id,
538 SessionView { snapshot, subject: principal.subject().to_owned() },
539 );
540 Ok(())
541 }
542 fn check_record(
543 &self,
544 record: Record,
545 subject: [u64; 4],
546 principal: &Principal,
547 now: u64,
548 ) -> Result<()> {
549 if record.subject != subject {
550 return Err(Error::InvalidToken);
551 }
552 self.check_metadata(record, principal, now)
553 }
554 fn check_live(
555 &self,
556 record: Record,
557 now: u64,
558 ) -> Result<()> {
559 match record.status {
560 REVOKED => return Err(Error::Revoked),
561 SESSION => (),
562 _ => return Err(Error::PolicyUnavailable),
563 }
564 if now >= record.expires_at {
565 return Err(Error::Expired);
566 }
567 if now < record.created_at {
568 return Err(Error::NotYetValid);
569 }
570 Ok(())
571 }
572 fn check_metadata(
573 &self,
574 record: Record,
575 principal: &Principal,
576 now: u64,
577 ) -> Result<()> {
578 self.check_live(record, now)?;
579 if principal.claims.issued_at < record.created_at
580 || principal.claims.expires_at > record.expires_at
581 {
582 return Err(Error::InvalidToken);
583 }
584 let binding = principal.session.as_ref().ok_or(Error::InvalidToken)?;
585 if principal.claims.purpose == Purpose::Refresh
586 && binding.generation != record.generation
587 {
588 return Err(Error::Replayed);
589 }
590 Ok(())
591 }
592}
593
594impl<P: StateProtection> PrincipalCache for FleetAuth<P> {
595 fn get(
596 &self,
597 key: &CacheKey,
598 ) -> Option<CachedPrincipal> {
599 self.local.get(key)
600 }
601 fn insert(
602 &self,
603 key: CacheKey,
604 entry: CachedPrincipal,
605 ) {
606 if key == entry.key {
607 self.local.insert(key, entry);
608 }
609 }
610}
611
612impl<P: StateProtection> ValidationHook for FleetAuth<P> {
613 fn check(
614 &self,
615 principal: &Principal,
616 now: u64,
617 ) -> Result<()> {
618 self.check_scope(principal, now)?;
619 self.check_session(principal, now)
620 }
621
622 fn check_lapsed(
625 &self,
626 principal: &Principal,
627 now: u64,
628 ) -> Result<()> {
629 self.check_scope_lapsed(principal, now)?;
630 self.check_session(principal, now)
631 }
632}
633
634pub struct ReplayGuard<'a, P: StateProtection>(&'a FleetAuth<P>);
635impl<P: StateProtection> ValidationHook for ReplayGuard<'_, P> {
636 fn check(
637 &self,
638 principal: &Principal,
639 now: u64,
640 ) -> Result<()> {
641 let auth = self.0;
642 auth.check_scope(principal, now)?;
643 let key = StoreKey(mac(
644 auth.state_key.as_ref(),
645 &context(&[b"replay", &principal.jti.0]),
646 ));
647 auth.table.write(|slots| {
649 if let Some(binding) = &principal.session {
650 let snapshot = find(
651 slots,
652 auth.session_key(binding.id),
653 &auth.protection,
654 )?
655 .ok_or(Error::Revoked)?;
656 auth.check_record(
657 snapshot.record,
658 auth.subject_tag(principal.subject()),
659 principal,
660 now,
661 )?;
662 }
663 if find(slots, key, &auth.protection)?.is_some() {
664 return Err(Error::Replayed);
665 }
666 insert(
667 slots,
668 Record {
669 key,
670 subject: [0; 4],
671 created_at: now,
672 expires_at: principal.claims.expires_at,
673 generation: 0,
674 status: CONSUMED,
675 },
676 now,
677 &auth.protection,
678 )
679 })
680 }
681}
682
683fn mac(
684 key: &[u8],
685 value: &[u8],
686) -> [u64; 4] {
687 let mut mac =
688 <Hmac<Sha256> as hmac::KeyInit>::new_from_slice(key).expect("HMAC key");
689 mac.update(value);
690 let bytes = mac.finalize().into_bytes();
691 std::array::from_fn(|i| {
692 u64::from_le_bytes(
693 bytes[i * 8..i * 8 + 8].try_into().expect("eight bytes"),
694 )
695 })
696}
697
698#[cfg(test)]
699#[path = "../tests/unit/fleet.rs"]
700mod tests;