aauth-protocol 0.1.0

AAuth (draft-hardt-oauth-aauth-protocol): HTTP message signatures, agent, person and auth tokens, and an agent-side Person Server client
Documentation

aauth-protocol

AAuth (draft-hardt-oauth-aauth-protocol-11) for Rust. AAuth is how an agent proves who it is, and what a person has allowed it to do, on every request it makes.

Each request carries an RFC 9421 HTTP Message Signature under the AAuth profile. The signer's key is named in Signature-Key, usually as a token that binds it: an agent token from the agent's provider, a person token, or an auth token from the person's Person Server.

This crate is the part of the protocol every role shares.

Module What it does
keys, jwk, jwt Ed25519 signing keys, public JWKs and RFC 7638 thumbprints, compact JWS
httpsig Sign a request, or verify one and learn which key signed it
token Mint tokens, or verify one against its issuer's keys and the request's signer
discovery Find an issuer's keys through its metadata document, over an HTTP client you choose
resource The resource's side: authenticate a request, mint resource tokens, challenge
requirement The AAuth-Requirement header
mission, budget What a person approved, and what it may spend
client The agent's Person Server client, and an SSRF-guarded HTTP client (client feature, on by default)

The Person Server itself is the aauth-person-server crate.

Sign as an agent

use aauth_protocol::httpsig::{sign_jwt, Outgoing};

let headers = sign_jwt(&session_key, &agent_token, &Outgoing {
    method: "POST",
    url: "https://api.booking.example/v1/reserve",
    content_type: None,
    body: Some(br#"{"offer_id":"fl-qf1"}"#),
    created: aauth_protocol::now_unix(),
})?;
// content-digest, content-type, signature-key, signature-input, signature

Verify as a resource

use aauth_protocol::resource::authenticate;
use aauth_protocol::token::TokenKind;

let authenticated = authenticate(
    &request,                         // httpsig::Incoming: method, authority, path, headers, body
    "https://api.booking.example",    // this resource's identifier
    &[TokenKind::Auth],
    &VerifyOptions::default(),
    &resolver,                        // discovery::JwksResolver over a guarded ReqwestFetch
).await?;
authenticated.token.scope();

authenticate checks the signature first, then that the token's cnf.jwk is the key that signed. It checks the token against its issuer's published keys last. Nothing a token claims is believed until that last check passes.

Only the token's issuer is ever looked up, and only when it is a server identifier. A Signature-Key: sig=jwks_uri from an unauthenticated caller is refused, not fetched. Use ReqwestFetch::guarded for discovery: it calls https only, refuses IP literals and localhost, and its resolver refuses private addresses.

Talk to a Person Server as an agent

use aauth_protocol::client::{guarded_client, Agent, Answer, DeniedNetworks, Person, PersonServerClient, Reach};

let client = PersonServerClient::new(guarded_client(DeniedNetworks::default())?, Reach::default());
let metadata = client.metadata("https://ps.acme.example").await?;
let agent = Agent { agent_token: &agent_token, key: &session_key };
let sam = Person { login_hint: Some("user:sam@acme.example"), tenant: None };
match client.propose_mission(&agent, &metadata, &json!({ "description": "Sydney, 19-23 October" }), &sam).await? {
    Answer::Pending { location, retry_after } => { /* poll `location` */ }
    Answer::Granted(mission) => { /* approved already */ }
    Answer::Denied { reason, .. } => { /* refused */ }
    _ => {}
}

Interop

The tests pin this crate's signatures to @hellocoop/httpsig, by the spec's author, with fixed vectors in tests/vectors/hellocoop.json: requests it signed verify here, and what this crate signs is exactly what it verified.

Status

The crate implements protocol draft -11.

  • Algorithms: signing is Ed25519; verification accepts Ed25519 and ES256.
  • Signature-Key schemes: jwt, jwks_uri and hwk are supported; jkt-jwt and x509 are not.
  • Revocation: not checked.
  • @query: follows RFC 9421 and includes the leading ?. @hellocoop/httpsig 2.6 leaves the ? out, so the two disagree on any signed request that has a query string.

Until 1.0 the API may change between minor versions.

License

Apache-2.0