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//! 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}