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, Table, find, insert
14};
15use crate::{
16 Authority, CacheKey, CachedPrincipal, Claims, Error, MemoryCache, Principal, PrincipalCache,
17 Purpose, Result, SecretKey, StateProtection, ValidationHook, Validator
18};
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
22pub struct SessionId(pub [u8; 16]);
23
24#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(deny_unknown_fields)]
26pub(crate) struct SessionBinding {
27 pub(crate) id: SessionId,
28 generation: u64
29}
30
31pub struct Session {
34 binding: SessionBinding,
35 subject: String,
36 created_at: u64,
37 expires_at: u64,
38 realm: String,
39 issuer: String
40}
41impl Session {
42 pub fn id(&self) -> SessionId {
43 self.binding.id
44 }
45 pub fn subject(&self) -> &str {
46 &self.subject
47 }
48 pub fn created_at(&self) -> u64 {
49 self.created_at
50 }
51 pub fn expires_at(&self) -> u64 {
52 self.expires_at
53 }
54 pub(crate) fn binding(&self) -> SessionBinding {
55 self.binding.clone()
56 }
57 pub(crate) fn check_issuance(
58 &self,
59 authority: &Authority,
60 claims: &Claims
61 ) -> Result<()> {
62 if self.realm != authority.realm
63 || self.issuer != authority.issuer
64 || claims.subject != self.subject
65 || claims.issued_at < self.created_at
66 || claims.expires_at > self.expires_at
67 {
68 return Err(Error::InvalidInput);
69 }
70 Ok(())
71 }
72}
73
74struct SessionView {
75 snapshot: Snapshot,
76 subject: String
78}
79
80pub struct FleetAuth<P: StateProtection> {
90 _fleet: Arc<Fleet>,
91 table: Table,
92 local: MemoryCache,
93 sessions: Mutex<LruCache<SessionId, SessionView>>,
94 state_key: Zeroizing<[u8; 32]>,
95 protection: RecordProtection<P>,
96 realm: String,
97 issuer: String
98}
99impl<P: StateProtection> FleetAuth<P> {
100 pub fn new(
109 fleet: Arc<Fleet>,
110 authority: &Authority,
111 state_key: SecretKey,
112 local_capacity: NonZeroUsize,
113 _policy: P
114 ) -> Result<Self> {
115 let info = context(&[
116 b"orbit-auth/v2/fleet-state",
117 fleet.name().as_bytes(),
118 authority.realm.as_bytes(),
119 authority.issuer.as_bytes()
120 ]);
121 Ok(Self {
122 table: Table::new(fleet.clone(), P::ID)?,
123 protection: RecordProtection::new(derive(
127 &state_key,
128 &context(&[
129 b"orbit-auth/v4/fleet-state-protection",
130 fleet.name().as_bytes(),
131 &P::ID.to_le_bytes()
132 ])
133 )),
134 _fleet: fleet,
135 local: MemoryCache::new(local_capacity),
136 sessions: Mutex::new(LruCache::new(local_capacity)),
137 state_key: derive(&state_key, &info),
138 realm: authority.realm.clone(),
139 issuer: authority.issuer.clone()
140 })
141 }
142
143 pub fn clear_local(&self) {
145 self.local.clear();
146 self.sessions.lock().unwrap_or_else(|e| e.into_inner()).clear();
147 }
148
149 pub fn create_session(
150 &self,
151 subject: impl Into<String>,
152 now: u64,
153 expires_at: u64
154 ) -> Result<Session> {
155 let subject = subject.into();
156 if !crate::principal::valid_label(&subject) || now >= expires_at {
157 return Err(Error::InvalidInput);
158 }
159 let mut id = [0; 16];
160 getrandom::fill(&mut id).map_err(|_| Error::Randomness)?;
161 let id = SessionId(id);
162 let record = Record {
163 key: self.session_key(id),
164 subject: self.subject_tag(&subject),
165 created_at: now,
166 expires_at,
167 generation: 0,
168 status: SESSION
169 };
170 self.table.write(|slots| {
171 if find(slots, record.key, &self.protection)?.is_some() {
172 return Err(Error::InvalidInput);
173 }
174 insert(slots, record, now, &self.protection)
175 })?;
176 Ok(self.handle(id, subject, record))
177 }
178
179 pub fn revoke_session(
183 &self,
184 id: SessionId,
185 now: u64
186 ) -> Result<()> {
187 let key = self.session_key(id);
188 self.table.write(|slots| {
189 let snapshot = find(slots, key, &self.protection)?.ok_or(Error::Revoked)?;
190 let mut record = snapshot.record;
191 if !matches!(record.status, SESSION | REVOKED) {
192 return Err(Error::PolicyUnavailable);
193 }
194 if now < record.created_at {
195 return Err(Error::NotYetValid);
196 }
197 record.status = REVOKED;
198 slots[snapshot.index].store(record, snapshot.index, &self.protection)?;
199 Ok(())
200 })
201 }
202
203 pub fn session(
207 &self,
208 id: SessionId,
209 subject: &str,
210 now: u64
211 ) -> Result<Session> {
212 if !crate::principal::valid_label(subject) {
213 return Err(Error::InvalidInput);
214 }
215 let snapshot =
216 self.table.lookup(self.session_key(id), &self.protection)?.ok_or(Error::Revoked)?;
217 if snapshot.record.subject != self.subject_tag(subject) {
218 return Err(Error::InvalidInput);
219 }
220 self.check_live(snapshot.record, now)?;
221 Ok(self.handle(id, subject.to_owned(), snapshot.record))
222 }
223
224 pub fn extend_session(
230 &self,
231 id: SessionId,
232 subject: &str,
233 now: u64,
234 expires_at: u64
235 ) -> Result<Session> {
236 if !crate::principal::valid_label(subject) || now >= expires_at {
237 return Err(Error::InvalidInput);
238 }
239 let key = self.session_key(id);
240 let tag = self.subject_tag(subject);
241 self.table.write(|slots| {
242 let snapshot = find(slots, key, &self.protection)?.ok_or(Error::Revoked)?;
243 let mut record = snapshot.record;
244 if record.subject != tag {
245 return Err(Error::InvalidInput);
246 }
247 self.check_live(record, now)?;
248 if expires_at < record.expires_at {
249 return Err(Error::InvalidInput);
250 }
251 if expires_at > record.expires_at {
252 record.expires_at = expires_at;
253 slots[snapshot.index].store(record, snapshot.index, &self.protection)?;
254 }
255 Ok(self.handle(id, subject.to_owned(), record))
256 })
257 }
258
259 pub fn refresh(
265 &self,
266 validator: &Validator<'_>,
267 token: &str,
268 now: u64,
269 expires_at: u64
270 ) -> Result<Session> {
271 if validator.validation.purpose != Purpose::Refresh {
272 return Err(Error::InvalidInput);
273 }
274 let principal = validator.validate(token, now, self)?;
275 let binding = principal.session.as_ref().ok_or(Error::InvalidToken)?;
276 let key = self.session_key(binding.id);
277 let subject = self.subject_tag(principal.subject());
278 self.table.write(|slots| {
279 let snapshot = find(slots, key, &self.protection)?.ok_or(Error::Revoked)?;
280 let mut record = snapshot.record;
281 self.check_record(record, subject, &principal, now)?;
282 if expires_at < record.expires_at {
283 return Err(Error::InvalidInput);
284 }
285 record.generation = record.generation.checked_add(1).ok_or(Error::PolicyUnavailable)?;
286 record.expires_at = expires_at;
287 slots[snapshot.index].store(record, snapshot.index, &self.protection)?;
288 Ok(self.handle(binding.id, principal.subject().to_owned(), record))
289 })
290 }
291
292 pub fn replay_guard(&self) -> ReplayGuard<'_, P> {
294 ReplayGuard(self)
295 }
296
297 fn handle(
298 &self,
299 id: SessionId,
300 subject: String,
301 record: Record
302 ) -> Session {
303 Session {
304 binding: SessionBinding { id, generation: record.generation },
305 subject,
306 created_at: record.created_at,
307 expires_at: record.expires_at,
308 realm: self.realm.clone(),
309 issuer: self.issuer.clone()
310 }
311 }
312 fn session_key(
313 &self,
314 id: SessionId
315 ) -> StoreKey {
316 StoreKey(mac(self.state_key.as_ref(), &context(&[b"session", &id.0])))
317 }
318 fn subject_tag(
319 &self,
320 subject: &str
321 ) -> [u64; 4] {
322 mac(self.state_key.as_ref(), &context(&[b"subject", subject.as_bytes()]))
323 }
324 fn check_scope(
325 &self,
326 principal: &Principal,
327 now: u64
328 ) -> Result<()> {
329 self.check_scope_lapsed(principal, now)?;
330 if now >= principal.claims.expires_at {
331 return Err(Error::Expired);
332 }
333 Ok(())
334 }
335 fn check_scope_lapsed(
336 &self,
337 principal: &Principal,
338 now: u64
339 ) -> Result<()> {
340 if principal.realm != self.realm || principal.issuer != self.issuer {
341 return Err(Error::InvalidToken);
342 }
343 if now < principal.claims.not_before || now < principal.claims.issued_at {
344 return Err(Error::NotYetValid);
345 }
346 Ok(())
347 }
348 fn check_session(
350 &self,
351 principal: &Principal,
352 now: u64
353 ) -> Result<()> {
354 let binding = principal.session.as_ref().ok_or(Error::InvalidToken)?;
355 {
356 let mut views = self.sessions.lock().map_err(|_| Error::PolicyUnavailable)?;
357 if let Some(view) = views.get(&binding.id) {
358 if self.table.unchanged::<P>(&view.snapshot)? {
359 if view.subject != principal.subject() {
360 return Err(Error::InvalidToken);
361 }
362 return self.check_metadata(view.snapshot.record, principal, now);
363 }
364 views.pop(&binding.id);
365 }
366 }
367 let key = self.session_key(binding.id);
368 let snapshot = self.table.lookup(key, &self.protection)?.ok_or(Error::Revoked)?;
369 self.check_record(snapshot.record, self.subject_tag(principal.subject()), principal, now)?;
370 self.sessions
371 .lock()
372 .map_err(|_| Error::PolicyUnavailable)?
373 .put(binding.id, SessionView { snapshot, subject: principal.subject().to_owned() });
374 Ok(())
375 }
376 fn check_record(
377 &self,
378 record: Record,
379 subject: [u64; 4],
380 principal: &Principal,
381 now: u64
382 ) -> Result<()> {
383 if record.subject != subject {
384 return Err(Error::InvalidToken);
385 }
386 self.check_metadata(record, principal, now)
387 }
388 fn check_live(
389 &self,
390 record: Record,
391 now: u64
392 ) -> Result<()> {
393 match record.status {
394 REVOKED => return Err(Error::Revoked),
395 SESSION => (),
396 _ => return Err(Error::PolicyUnavailable)
397 }
398 if now >= record.expires_at {
399 return Err(Error::Expired);
400 }
401 if now < record.created_at {
402 return Err(Error::NotYetValid);
403 }
404 Ok(())
405 }
406 fn check_metadata(
407 &self,
408 record: Record,
409 principal: &Principal,
410 now: u64
411 ) -> Result<()> {
412 self.check_live(record, now)?;
413 if principal.claims.issued_at < record.created_at
414 || principal.claims.expires_at > record.expires_at
415 {
416 return Err(Error::InvalidToken);
417 }
418 let binding = principal.session.as_ref().ok_or(Error::InvalidToken)?;
419 if principal.claims.purpose == Purpose::Refresh && binding.generation != record.generation {
420 return Err(Error::Replayed);
421 }
422 Ok(())
423 }
424}
425
426impl<P: StateProtection> PrincipalCache for FleetAuth<P> {
427 fn get(
428 &self,
429 key: &CacheKey
430 ) -> Option<CachedPrincipal> {
431 self.local.get(key)
432 }
433 fn insert(
434 &self,
435 key: CacheKey,
436 entry: CachedPrincipal
437 ) {
438 if key == entry.key {
439 self.local.insert(key, entry);
440 }
441 }
442}
443
444impl<P: StateProtection> ValidationHook for FleetAuth<P> {
445 fn check(
446 &self,
447 principal: &Principal,
448 now: u64
449 ) -> Result<()> {
450 self.check_scope(principal, now)?;
451 self.check_session(principal, now)
452 }
453
454 fn check_lapsed(
457 &self,
458 principal: &Principal,
459 now: u64
460 ) -> Result<()> {
461 self.check_scope_lapsed(principal, now)?;
462 self.check_session(principal, now)
463 }
464}
465
466pub struct ReplayGuard<'a, P: StateProtection>(&'a FleetAuth<P>);
467impl<P: StateProtection> ValidationHook for ReplayGuard<'_, P> {
468 fn check(
469 &self,
470 principal: &Principal,
471 now: u64
472 ) -> Result<()> {
473 let auth = self.0;
474 auth.check_scope(principal, now)?;
475 let key = StoreKey(mac(auth.state_key.as_ref(), &context(&[b"replay", &principal.jti.0])));
476 auth.table.write(|slots| {
478 if let Some(binding) = &principal.session {
479 let snapshot = find(slots, auth.session_key(binding.id), &auth.protection)?
480 .ok_or(Error::Revoked)?;
481 auth.check_record(
482 snapshot.record,
483 auth.subject_tag(principal.subject()),
484 principal,
485 now
486 )?;
487 }
488 if find(slots, key, &auth.protection)?.is_some() {
489 return Err(Error::Replayed);
490 }
491 insert(
492 slots,
493 Record {
494 key,
495 subject: [0; 4],
496 created_at: now,
497 expires_at: principal.claims.expires_at,
498 generation: 0,
499 status: CONSUMED
500 },
501 now,
502 &auth.protection
503 )
504 })
505 }
506}
507
508fn mac(
509 key: &[u8],
510 value: &[u8]
511) -> [u64; 4] {
512 let mut mac = <Hmac<Sha256> as hmac::KeyInit>::new_from_slice(key).expect("HMAC key");
513 mac.update(value);
514 let bytes = mac.finalize().into_bytes();
515 std::array::from_fn(|i| {
516 u64::from_le_bytes(bytes[i * 8..i * 8 + 8].try_into().expect("eight bytes"))
517 })
518}
519
520#[cfg(test)]
521#[path = "../tests/unit/fleet.rs"]
522mod tests;