tollgate_auth/lib.rs
1//! Credential verification for latency-critical services.
2//!
3//! On the development host (Apple M1 Pro), Tollgate's admission path costs
4//! about 107 ns. Verifying a credential to reach it costs 800 ns to 2 µs —
5//! seven to twenty times everything the rest of the stack does. Absolute
6//! values are host-dependent; the ratio is the point. A library that tunes the 107 ns while leaving the 2 µs
7//! to each embedder is tuning the wrong end, so this crate owns the
8//! credential step too.
9//!
10//! It is split the way the problem is:
11//!
12//! - **[`CredentialVerifier`]** is the seam. Credential *schemes* differ per
13//! deployment — API-key digests, PASETO, JWT, client certificates — and this
14//! crate does not pick one for you.
15//! - **[`HmacRegistry`]** is the scheme in the box: server-issued API keys as
16//! HMAC-SHA256 digests, digests at rest, constant-time comparison.
17//! - **[`SessionCredential`]** is the part worth centralising whichever scheme
18//! you use: in steady state it verifies once per session and compares
19//! thereafter, taking the per-request cost to ~16 ns. (Requests racing on a
20//! cold session may each verify; that costs the optimisation, never
21//! correctness.)
22//!
23//! The reason that split matters is drift. Getting the cache right means
24//! getting an invalidation *ordering* right — clear the old proof before
25//! verifying its replacement, never cache a failure, compare the exact bytes
26//! that were verified. Left to each embedder, that is a security convention
27//! upheld by caller discipline at many call sites, and those drift. Here it is
28//! one implementation with its own tests and its own invariant.
29//!
30//! # A cached credential proves identity, never authorization
31//!
32//! A cache hit skips the credential check and **nothing else**. The caller
33//! still runs admission against the current snapshot, so status, staleness,
34//! permissions, rate and quota are decided fresh on every request. Revocation
35//! therefore stays bounded by snapshot refresh exactly as it is without any
36//! cache — including on a long-lived session that authenticated before the
37//! revocation landed.
38//!
39//! An answer is also bounded by whatever validity the verifier attached to it,
40//! so an expiring scheme cannot outlive its own `exp` just because the session
41//! stayed open. [`HmacRegistry::install`] preserves each key's `not_after`;
42//! convenience `install_credentials` attaches no expiry. A distributed
43//! projection can further bound evidence by feed freshness, as the client's
44//! `KeyManager` does. Removing a registry entry affects new verification;
45//! cached evidence retains its original bound, with snapshot withdrawal still
46//! checked on every request.
47//!
48//! ```
49//! use jiff::Timestamp;
50//! use tollgate_auth::{HmacRegistry, SessionCredential};
51//!
52//! let registry = HmacRegistry::new(b"secret-from-the-environment");
53//! let issued = registry.install_credentials([b"demo-key-1".as_slice()])[0];
54//!
55//! // One session — a connection, a TLS session, whatever the transport calls it.
56//! let session = SessionCredential::new();
57//! // `now` comes from the caller: the request path does not read clocks.
58//! let now = Timestamp::from_second(1_755_600_000).unwrap();
59//!
60//! // First request on it verifies; the rest compare.
61//! assert_eq!(session.authenticate(Some(b"demo-key-1"), ®istry, now), Some(issued));
62//! assert_eq!(session.authenticate(Some(b"demo-key-1"), ®istry, now), Some(issued));
63//!
64//! // A different credential re-verifies, and a bad one leaves nothing behind.
65//! assert_eq!(session.authenticate(Some(b"wrong"), ®istry, now), None);
66//! assert!(!session.is_authenticated());
67//! ```
68
69#![deny(missing_docs)]
70
71mod hmac_registry;
72mod session;
73mod verifier;
74
75pub use hmac_registry::{EntropyUnavailable, HmacRegistry, MintedKey};
76pub use session::SessionCredential;
77pub use verifier::{CredentialIssuer, CredentialVerifier, Verified};
78
79// Compiles and runs the README's examples as doctests without adding them to
80// the rendered documentation, so the README cannot drift from the API.
81#[doc = include_str!("../README.md")]
82#[cfg(doctest)]
83pub struct ReadmeDoctests;