1#![deny(unsafe_op_in_unsafe_fn)]
2#![doc = include_str!("../README.md")]
3
4mod cache;
5mod error;
6mod fleet;
7mod keyring;
8mod principal;
9mod protection;
10mod table;
11
12use std::borrow::Cow;
13use std::collections::BTreeMap;
14use std::fmt;
15use std::sync::Arc;
16use std::time::Duration;
17
18use base64::Engine;
19use base64::engine::general_purpose::URL_SAFE_NO_PAD;
20pub use cache::{CacheKey, CachedPrincipal, MemoryCache, PrincipalCache};
21use chacha20poly1305::aead::{Aead, Payload};
22use chacha20poly1305::{KeyInit, XChaCha20Poly1305, XNonce};
23pub use error::{Error, Result};
24pub use fleet::{FleetAuth, ReplayGuard, Session, SessionId};
25use hmac::{Hmac, Mac};
26pub use keyring::{KeyId, Keyring, SecretKey};
27use keyring::{context, derive};
28pub use principal::{
29 AllowReusable, Audience, Capability, Claims, MAX_CAPABILITIES, MAX_LABEL_LEN, Principal,
30 Purpose, TokenId, Validation, ValidationHook
31};
32pub use protection::{Blake3State, EncryptedState, StateProtection, UnprotectedState};
33use sha2::{Digest, Sha256};
34pub use table::{AUTH_STATE_CAPACITY, AUTH_STATE_KIND};
35use zeroize::Zeroizing;
36
37pub const MAX_TOKEN_LEN: usize = 16 * 1024;
39const PREFIX: &str = "oa1.";
40const HEADER_LEN: usize = 4 + 24;
41const TAG_LEN: usize = 16;
42
43pub struct Token(Zeroizing<String>);
45
46impl Token {
47 pub fn expose(&self) -> &str {
49 &self.0
50 }
51}
52
53impl fmt::Debug for Token {
54 fn fmt(
55 &self,
56 f: &mut fmt::Formatter<'_>
57 ) -> fmt::Result {
58 f.write_str("Token([REDACTED])")
59 }
60}
61
62pub struct Authority {
65 realm: String,
66 issuer: String,
67 keyring: Keyring,
68 cache_seed: Zeroizing<[u8; 32]>
69}
70
71impl Authority {
72 pub fn new(
73 realm: impl Into<String>,
74 issuer: impl Into<String>,
75 keyring: Keyring
76 ) -> Result<Self> {
77 let realm = realm.into();
78 let issuer = issuer.into();
79 if !principal::valid_label(&realm) || !principal::valid_label(&issuer) {
80 return Err(Error::InvalidInput);
81 }
82 let mut hash = Sha256::new();
83 hash.update(b"orbit-auth/v1/cache-keyring");
84 for (id, key) in &keyring.keys {
85 let info = context(&[b"cache", realm.as_bytes(), issuer.as_bytes(), &id.to_be_bytes()]);
86 let cache_key = derive(key, &info);
87 hash.update(id.to_be_bytes());
88 hash.update(cache_key.as_ref());
89 }
90 let cache_seed = Zeroizing::new(hash.finalize().into());
91 Ok(Self { realm, issuer, keyring, cache_seed })
92 }
93
94 pub fn issue(
96 &self,
97 claims: Claims
98 ) -> Result<Token> {
99 self.issue_bound(claims, None)
100 }
101
102 pub fn issue_session(
105 &self,
106 claims: Claims,
107 session: &Session
108 ) -> Result<Token> {
109 session.check_issuance(self, &claims)?;
110 self.issue_bound(claims, Some(session.binding()))
111 }
112
113 fn issue_bound(
114 &self,
115 claims: Claims,
116 session: Option<fleet::SessionBinding>
117 ) -> Result<Token> {
118 claims.check_shape()?;
119 let mut jti = [0; 16];
120 let mut nonce = [0; 24];
121 getrandom::fill(&mut jti).map_err(|_| Error::Randomness)?;
122 getrandom::fill(&mut nonce).map_err(|_| Error::Randomness)?;
123 let id = self.keyring.active;
124 let info = self.scope(&claims.audience, claims.purpose, id);
125 let key = derive(&self.keyring.keys[&id], &info);
126 let principal = Principal {
127 realm: self.realm.clone(),
128 issuer: self.issuer.clone(),
129 jti: TokenId(jti),
130 session,
131 claims
132 };
133 let plaintext =
134 Zeroizing::new(serde_json::to_vec(&principal).map_err(|_| Error::InvalidInput)?);
135 let wire_len = PREFIX.len() + ((HEADER_LEN + TAG_LEN + plaintext.len()) * 4).div_ceil(3);
136 if wire_len > MAX_TOKEN_LEN {
137 return Err(Error::TokenTooLarge);
138 }
139 let cipher = XChaCha20Poly1305::new_from_slice(key.as_ref()).expect("256-bit key");
140 let ciphertext = cipher
141 .encrypt(XNonce::from_slice(&nonce), Payload { msg: &plaintext, aad: &info })
142 .map_err(|_| Error::InvalidToken)?;
143 let mut wire = Vec::with_capacity(HEADER_LEN + ciphertext.len());
144 wire.extend_from_slice(&id.to_be_bytes());
145 wire.extend_from_slice(&nonce);
146 wire.extend_from_slice(&ciphertext);
147 Ok(Token(Zeroizing::new(format!("{PREFIX}{}", URL_SAFE_NO_PAD.encode(wire)))))
148 }
149
150 pub fn validator(
152 &self,
153 validation: Validation
154 ) -> Validator<'_> {
155 let keys = self
156 .keyring
157 .keys
158 .iter()
159 .map(|(id, key)| {
160 let info = self.scope(&validation.audience, validation.purpose, *id);
161 (*id, derive(key, &info))
162 })
163 .collect();
164 let mut mac = <Hmac<Sha256> as hmac::KeyInit>::new_from_slice(self.cache_seed.as_ref())
165 .expect("HMAC key");
166 mac.update(&context(&[
167 b"orbit-auth/v1/cache-policy",
168 validation.audience.as_str().as_bytes(),
169 validation.purpose.as_bytes()
170 ]));
171 for capability in &validation.required_capabilities {
172 mac.update(&context(&[capability.as_str().as_bytes()]));
173 }
174 let cache_secret = Zeroizing::new(mac.finalize().into_bytes().into());
175 Validator {
176 realm: Cow::Borrowed(&self.realm),
177 issuer: Cow::Borrowed(&self.issuer),
178 validation,
179 keys,
180 cache_secret
181 }
182 }
183
184 fn scope(
185 &self,
186 audience: &Audience,
187 purpose: Purpose,
188 id: KeyId
189 ) -> Vec<u8> {
190 token_scope(&self.realm, &self.issuer, audience, purpose, id)
191 }
192}
193
194pub struct Validator<'a> {
196 realm: Cow<'a, str>,
197 issuer: Cow<'a, str>,
198 validation: Validation,
199 keys: BTreeMap<KeyId, Zeroizing<[u8; 32]>>,
200 cache_secret: Zeroizing<[u8; 32]>
201}
202
203pub type OwnedValidator = Validator<'static>;
206
207impl Validator<'_> {
208 pub fn into_owned(self) -> OwnedValidator {
213 Validator {
214 realm: Cow::Owned(self.realm.into_owned()),
215 issuer: Cow::Owned(self.issuer.into_owned()),
216 validation: self.validation,
217 keys: self.keys,
218 cache_secret: self.cache_secret
219 }
220 }
221
222 pub fn validate(
223 &self,
224 token: &str,
225 now: u64,
226 hook: &dyn ValidationHook
227 ) -> Result<Arc<Principal>> {
228 let principal = self.decrypt(token)?;
229 self.check(&principal, now)?;
230 hook.check(&principal, now)?;
231 Ok(Arc::new(principal))
232 }
233
234 pub fn validate_lapsed(
241 &self,
242 token: &str,
243 now: u64,
244 hook: &dyn ValidationHook
245 ) -> Result<Arc<Principal>> {
246 let principal = self.decrypt(token)?;
247 self.check_lapsed(&principal, now)?;
248 hook.check_lapsed(&principal, now)?;
249 Ok(Arc::new(principal))
250 }
251
252 pub fn validate_cached(
259 &self,
260 token: &str,
261 now: u64,
262 cache: &dyn PrincipalCache,
263 ttl: Duration,
264 hook: &dyn ValidationHook
265 ) -> Result<Arc<Principal>> {
266 let ttl = ttl.as_secs();
267 if ttl == 0 {
268 return self.validate(token, now, hook);
269 }
270 let key = self.cache_key(token)?;
271 if let Some(entry) = cache.get(&key) {
272 if entry.key == key
274 && entry.cached_at <= now
275 && now < entry.valid_until
276 && now < entry.cached_at.saturating_add(ttl)
277 {
278 self.check(&entry.principal, now)?;
279 hook.check(&entry.principal, now)?;
280 return Ok(entry.principal);
281 }
282 }
283 let principal = self.validate(token, now, hook)?;
284 let valid_until = now.saturating_add(ttl).min(principal.claims.expires_at);
285 cache.insert(
286 key,
287 CachedPrincipal { key, principal: principal.clone(), cached_at: now, valid_until }
288 );
289 Ok(principal)
290 }
291
292 pub fn cache_key(
294 &self,
295 token: &str
296 ) -> Result<CacheKey> {
297 check_size(token)?;
298 let mut mac = <Hmac<Sha256> as hmac::KeyInit>::new_from_slice(self.cache_secret.as_ref())
299 .expect("HMAC key");
300 mac.update(token.as_bytes());
301 Ok(CacheKey(mac.finalize().into_bytes().into()))
302 }
303
304 fn decrypt(
305 &self,
306 token: &str
307 ) -> Result<Principal> {
308 check_size(token)?;
309 let encoded = token.strip_prefix(PREFIX).ok_or(Error::InvalidToken)?;
310 let wire = URL_SAFE_NO_PAD.decode(encoded).map_err(|_| Error::InvalidToken)?;
311 if wire.len() < HEADER_LEN + TAG_LEN {
312 return Err(Error::InvalidToken);
313 }
314 let id = KeyId::from_be_bytes(wire[..4].try_into().expect("checked header length"));
315 let key = self.keys.get(&id).ok_or(Error::InvalidToken)?;
316 let info = token_scope(
317 &self.realm,
318 &self.issuer,
319 &self.validation.audience,
320 self.validation.purpose,
321 id
322 );
323 let cipher = XChaCha20Poly1305::new_from_slice(key.as_ref()).expect("256-bit key");
324 let plaintext = Zeroizing::new(
325 cipher
326 .decrypt(
327 XNonce::from_slice(&wire[4..HEADER_LEN]),
328 Payload { msg: &wire[HEADER_LEN..], aad: &info }
329 )
330 .map_err(|_| Error::InvalidToken)?
331 );
332 serde_json::from_slice(&plaintext).map_err(|_| Error::InvalidToken)
333 }
334
335 fn check(
336 &self,
337 principal: &Principal,
338 now: u64
339 ) -> Result<()> {
340 self.check_lapsed(principal, now)?;
341 if now >= principal.claims.expires_at {
342 return Err(Error::Expired);
343 }
344 Ok(())
345 }
346
347 fn check_lapsed(
349 &self,
350 principal: &Principal,
351 now: u64
352 ) -> Result<()> {
353 let claims = &principal.claims;
354 claims.check_shape().map_err(|_| Error::InvalidToken)?;
355 if principal.realm != self.realm
356 || principal.issuer != self.issuer
357 || claims.audience != self.validation.audience
358 || claims.purpose != self.validation.purpose
359 {
360 return Err(Error::InvalidToken);
361 }
362 if now < claims.not_before || now < claims.issued_at {
363 return Err(Error::NotYetValid);
364 }
365 if !self.validation.required_capabilities.is_subset(&claims.capabilities) {
366 return Err(Error::MissingCapability);
367 }
368 Ok(())
369 }
370}
371
372fn check_size(token: &str) -> Result<()> {
373 if token.len() > MAX_TOKEN_LEN {
374 return Err(Error::TokenTooLarge);
375 }
376 if token.is_empty() {
377 return Err(Error::InvalidToken);
378 }
379 Ok(())
380}
381
382fn token_scope(
383 realm: &str,
384 issuer: &str,
385 audience: &Audience,
386 purpose: Purpose,
387 id: KeyId
388) -> Vec<u8> {
389 context(&[
390 b"orbit-auth/v1/xchacha20poly1305",
391 realm.as_bytes(),
392 issuer.as_bytes(),
393 audience.as_str().as_bytes(),
394 purpose.as_bytes(),
395 &id.to_be_bytes()
396 ])
397}