aauth_protocol/lib.rs
1//! AAuth ([draft-hardt-oauth-aauth-protocol]) for Rust: how an agent proves
2//! who it is, and what a person has allowed it to do, on every request.
3//!
4//! Every AAuth request is signed with an RFC 9421 HTTP Message Signature
5//! under the AAuth profile, and names the signer's key in `Signature-Key`
6//! ([draft-hardt-httpbis-signature-key]). Usually the key is a JWT whose
7//! `cnf.jwk` is the signing key: an **agent token** from the agent's
8//! provider, a **person token** from the person's Person Server, or an
9//! **auth token** the Person Server issued for one resource. This crate is
10//! the part of the protocol every role shares:
11//!
12//! | Module | What it does |
13//! | --- | --- |
14//! | [`keys`], [`jwk`], [`jwt`] | Ed25519 signing keys, public JWKs and RFC 7638 thumbprints, compact JWS |
15//! | [`httpsig`] | Sign a request; verify one and learn which key signed it |
16//! | [`token`] | Mint tokens; verify one against its issuer's keys and the request's signer |
17//! | [`discovery`] | Find an issuer's keys through its metadata document, over a [`discovery::Fetch`] you choose |
18//! | [`resource`] | A resource's side: [`resource::authenticate`] a request, mint resource tokens, challenge |
19//! | [`requirement`] | The `AAuth-Requirement` header |
20//! | [`mission`], [`budget`] | What a person approved, and what it may spend |
21//! | [`client`] | The agent's side of a Person Server conversation, and an SSRF guard (feature `client`) |
22//!
23//! The Person Server itself is the [`aauth-person-server`] crate.
24//!
25//! # A request, end to end
26//!
27//! An agent provider vouches for an agent's key; the agent signs a request
28//! with it; a resource verifies the signature and the token.
29//!
30//! ```
31//! use std::sync::Arc;
32//!
33//! use aauth_protocol::discovery::{JwksResolver, StaticFetch};
34//! use aauth_protocol::httpsig::{header_map, sign_jwt, Incoming, Outgoing, VerifyOptions};
35//! use aauth_protocol::resource::authenticate;
36//! use aauth_protocol::token::{mint, TokenKind};
37//! use aauth_protocol::SigningKey;
38//! use serde_json::json;
39//!
40//! # #[tokio::main(flavor = "current_thread")]
41//! # async fn main() -> Result<(), aauth_protocol::Error> {
42//! // The agent provider publishes its keys and vouches for the agent's.
43//! let provider = SigningKey::generate();
44//! let agent = SigningKey::generate();
45//! let now = aauth_protocol::now_unix();
46//! let agent_token = mint(&provider, TokenKind::Agent, &json!({
47//! "iss": "https://agents.example",
48//! "dwk": "aauth-agent.json",
49//! "sub": "aauth:travel@agents.example",
50//! "iat": now,
51//! "exp": now + 3600,
52//! "cnf": { "jwk": agent.public_jwk() },
53//! }));
54//!
55//! // The agent signs its request.
56//! let body = br#"{"offer_id":"fl-qf1"}"#;
57//! let signed = sign_jwt(&agent, &agent_token, &Outgoing {
58//! method: "POST",
59//! url: "https://api.booking.example/v1/reserve",
60//! content_type: None,
61//! body: Some(body),
62//! created: now,
63//! })?;
64//!
65//! // The resource verifies it, finding the provider's keys by discovery.
66//! // `StaticFetch` stands in for the network here; use `ReqwestFetch`.
67//! let documents = StaticFetch::new();
68//! documents.insert(
69//! "https://agents.example/.well-known/aauth-agent.json",
70//! json!({ "issuer": "https://agents.example", "jwks": provider.jwks() }),
71//! );
72//! let resolver = JwksResolver::new(Arc::new(documents));
73//! let headers = header_map(&signed)?;
74//! let request = Incoming {
75//! method: "POST",
76//! authority: "api.booking.example",
77//! path: "/v1/reserve",
78//! query: None,
79//! headers: &headers,
80//! body: Some(body),
81//! };
82//! let authenticated = authenticate(
83//! &request,
84//! "https://api.booking.example",
85//! &[TokenKind::Agent],
86//! &VerifyOptions::default(),
87//! &resolver,
88//! )
89//! .await?;
90//! assert_eq!(authenticated.token.sub, "aauth:travel@agents.example");
91//! assert_eq!(authenticated.signature.thumbprint, agent.thumbprint());
92//! # Ok(())
93//! # }
94//! ```
95//!
96//! # Features
97//!
98//! - `client` (default): [`client`], and [`discovery::ReqwestFetch`]. Without
99//! it the crate makes no network calls of its own and pulls in no HTTP
100//! client or async runtime.
101//!
102//! # Status
103//!
104//! This implements `-11` of the protocol draft. Signing is Ed25519;
105//! verification accepts Ed25519 and ES256. The `jwt`, `jwks_uri` and `hwk`
106//! `Signature-Key` schemes are supported, `jkt-jwt` and `x509` are not, and
107//! revocation is not checked. Until 1.0 the API may change between minor
108//! versions.
109//!
110//! [draft-hardt-oauth-aauth-protocol]: https://github.com/dickhardt/AAuth
111//! [draft-hardt-httpbis-signature-key]: https://github.com/dickhardt/signature-key
112//! [`aauth-person-server`]: https://docs.rs/aauth-person-server
113
114#![deny(missing_docs)]
115#![forbid(unsafe_code)]
116#![cfg_attr(docsrs, feature(doc_cfg))]
117
118pub mod budget;
119pub mod discovery;
120pub mod error;
121pub mod httpsig;
122pub mod identifier;
123pub mod jwk;
124pub mod jwt;
125pub mod keys;
126pub mod metadata;
127pub mod mission;
128pub mod requirement;
129pub mod resource;
130pub mod token;
131
132#[cfg(feature = "client")]
133#[cfg_attr(docsrs, doc(cfg(feature = "client")))]
134pub mod client;
135
136mod sf;
137
138pub use budget::Budget;
139pub use error::{Error, Result, SignatureError, TokenError};
140pub use jwk::Jwk;
141pub use keys::SigningKey;
142pub use requirement::Requirement;
143pub use token::{TokenKind, VerifiedToken};
144
145/// The protocol draft this crate implements.
146pub const PROTOCOL_DRAFT: &str = "draft-hardt-oauth-aauth-protocol-11";
147
148/// `BASE64URL(SHA-256(bytes))`, unpadded: the encoding every `*_s256` value uses.
149pub fn s256(bytes: impl AsRef<[u8]>) -> String {
150 use base64::Engine;
151 use sha2::{Digest, Sha256};
152 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(Sha256::digest(bytes.as_ref()))
153}
154
155/// 128 random bits, base64url: for `jti`s and other unguessable identifiers.
156pub(crate) fn random_id() -> String {
157 use base64::Engine;
158 let mut bytes = [0u8; 16];
159 getrandom::fill(&mut bytes).expect("the operating system provides randomness");
160 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes)
161}
162
163/// Whether `a` equals `b`, in time that depends on their lengths alone:
164/// for a secret a peer presents, such as a thumbprint or a bearer token.
165pub fn constant_time_eq(a: &str, b: &str) -> bool {
166 a.len() == b.len()
167 && a.bytes()
168 .zip(b.bytes())
169 .fold(0u8, |acc, (x, y)| acc | (x ^ y))
170 == 0
171}
172
173/// Seconds since the Unix epoch.
174pub fn now_unix() -> i64 {
175 std::time::SystemTime::now()
176 .duration_since(std::time::UNIX_EPOCH)
177 .map(|d| d.as_secs() as i64)
178 .unwrap_or(0)
179}