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_cached(
241 &self,
242 token: &str,
243 now: u64,
244 cache: &dyn PrincipalCache,
245 ttl: Duration,
246 hook: &dyn ValidationHook
247 ) -> Result<Arc<Principal>> {
248 let ttl = ttl.as_secs();
249 if ttl == 0 {
250 return self.validate(token, now, hook);
251 }
252 let key = self.cache_key(token)?;
253 if let Some(entry) = cache.get(&key) {
254 if entry.key == key
256 && entry.cached_at <= now
257 && now < entry.valid_until
258 && now < entry.cached_at.saturating_add(ttl)
259 {
260 self.check(&entry.principal, now)?;
261 hook.check(&entry.principal, now)?;
262 return Ok(entry.principal);
263 }
264 }
265 let principal = self.validate(token, now, hook)?;
266 let valid_until = now.saturating_add(ttl).min(principal.claims.expires_at);
267 cache.insert(
268 key,
269 CachedPrincipal { key, principal: principal.clone(), cached_at: now, valid_until }
270 );
271 Ok(principal)
272 }
273
274 pub fn cache_key(
276 &self,
277 token: &str
278 ) -> Result<CacheKey> {
279 check_size(token)?;
280 let mut mac = <Hmac<Sha256> as hmac::KeyInit>::new_from_slice(self.cache_secret.as_ref())
281 .expect("HMAC key");
282 mac.update(token.as_bytes());
283 Ok(CacheKey(mac.finalize().into_bytes().into()))
284 }
285
286 fn decrypt(
287 &self,
288 token: &str
289 ) -> Result<Principal> {
290 check_size(token)?;
291 let encoded = token.strip_prefix(PREFIX).ok_or(Error::InvalidToken)?;
292 let wire = URL_SAFE_NO_PAD.decode(encoded).map_err(|_| Error::InvalidToken)?;
293 if wire.len() < HEADER_LEN + TAG_LEN {
294 return Err(Error::InvalidToken);
295 }
296 let id = KeyId::from_be_bytes(wire[..4].try_into().expect("checked header length"));
297 let key = self.keys.get(&id).ok_or(Error::InvalidToken)?;
298 let info = token_scope(
299 &self.realm,
300 &self.issuer,
301 &self.validation.audience,
302 self.validation.purpose,
303 id
304 );
305 let cipher = XChaCha20Poly1305::new_from_slice(key.as_ref()).expect("256-bit key");
306 let plaintext = Zeroizing::new(
307 cipher
308 .decrypt(
309 XNonce::from_slice(&wire[4..HEADER_LEN]),
310 Payload { msg: &wire[HEADER_LEN..], aad: &info }
311 )
312 .map_err(|_| Error::InvalidToken)?
313 );
314 serde_json::from_slice(&plaintext).map_err(|_| Error::InvalidToken)
315 }
316
317 fn check(
318 &self,
319 principal: &Principal,
320 now: u64
321 ) -> Result<()> {
322 let claims = &principal.claims;
323 claims.check_shape().map_err(|_| Error::InvalidToken)?;
324 if principal.realm != self.realm
325 || principal.issuer != self.issuer
326 || claims.audience != self.validation.audience
327 || claims.purpose != self.validation.purpose
328 {
329 return Err(Error::InvalidToken);
330 }
331 if now >= claims.expires_at {
332 return Err(Error::Expired);
333 }
334 if now < claims.not_before || now < claims.issued_at {
335 return Err(Error::NotYetValid);
336 }
337 if !self.validation.required_capabilities.is_subset(&claims.capabilities) {
338 return Err(Error::MissingCapability);
339 }
340 Ok(())
341 }
342}
343
344fn check_size(token: &str) -> Result<()> {
345 if token.len() > MAX_TOKEN_LEN {
346 return Err(Error::TokenTooLarge);
347 }
348 if token.is_empty() {
349 return Err(Error::InvalidToken);
350 }
351 Ok(())
352}
353
354fn token_scope(
355 realm: &str,
356 issuer: &str,
357 audience: &Audience,
358 purpose: Purpose,
359 id: KeyId
360) -> Vec<u8> {
361 context(&[
362 b"orbit-auth/v1/xchacha20poly1305",
363 realm.as_bytes(),
364 issuer.as_bytes(),
365 audience.as_str().as_bytes(),
366 purpose.as_bytes(),
367 &id.to_be_bytes()
368 ])
369}