Skip to main content

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