agent_uri_attestation_wellknown/lib.rs
1//! Well-known key discovery for `agent://` attestation.
2//!
3//! [`agent-uri-attestation`](agent_uri_attestation) verifies a token against
4//! the keys it is given, and until now those keys had to arrive by hand. That
5//! is workable for the roots an operator already deals with and impossible for
6//! the ones they do not: there was no way to bootstrap trust in a root nobody
7//! had exchanged a key with out of band.
8//!
9//! [Specification](https://github.com/Govcraft/agent-uri-rs/blob/main/SPECIFICATION.md)
10//! section 7.2 answers that by having every trust root publish its keys:
11//!
12//! ```text
13//! GET https://{trust-root}/.well-known/agent-keys.json
14//! ```
15//!
16//! This crate fetches that document, caches it, and turns it into a
17//! [`Verifier`](agent_uri_attestation::Verifier).
18//!
19//! # Why this is a separate crate
20//!
21//! An HTTP client with a TLS stack is a large dependency, and the attestation
22//! crate is useful without one. A caller that verifies tokens against keys from
23//! a config file, a secrets manager, or a deployment artefact should not
24//! inherit a networking stack to do it.
25//!
26//! The document *format* stays in the attestation crate, as
27//! [`KeyDocument`](agent_uri_attestation::KeyDocument), because reading it is
28//! arithmetic rather than I/O. This crate is the part that goes and gets it.
29//!
30//! # Example
31//!
32//! ```no_run
33//! use agent_uri_attestation_wellknown::KeyDiscovery;
34//!
35//! # async fn example(token: &str) -> Result<(), Box<dyn std::error::Error>> {
36//! let discovery = KeyDiscovery::new();
37//!
38//! // No key was exchanged with acme.com beforehand.
39//! let verifier = discovery.verifier_for("acme.com").await?;
40//! let claims = verifier.verify(token)?;
41//! assert_eq!(claims.iss, "acme.com");
42//! # Ok(())
43//! # }
44//! ```
45//!
46//! # What a fetched document does and does not establish
47//!
48//! It establishes which keys an authority stands behind, because it came from
49//! that authority over TLS. Everything this crate refuses follows from
50//! protecting that one claim:
51//!
52//! - **HTTPS only, and no redirects.** A redirect is an instruction to go and
53//! ask something else. Following one would let whoever wrote the `Location`
54//! header decide whose keys a caller ends up trusting.
55//! - **The trust root is parsed before it is put in a URL.** The endpoint is
56//! built by concatenation, so a string carrying `/`, `@`, or a scheme would
57//! aim the request elsewhere while reading as a lookup of the named root.
58//! - **The document must name the root whose endpoint served it.** Otherwise
59//! any authority could publish keys for anyone else's namespace.
60//! - **The response is capped.** Both the declared length and what actually
61//! arrives, because the header is the server's claim rather than a fact.
62//!
63//! What it does **not** establish is that the authority deserves to be trusted.
64//! Fetching `evil.example`'s keys tells you exactly which keys `evil.example`
65//! signs with, and nothing about whether to believe anything it says. Deciding
66//! which roots matter is the deployment's job and always was.
67//!
68//! # Staleness
69//!
70//! A cached document is served for [`DEFAULT_TTL`] before it is fetched again,
71//! which is also the longest this crate can go on not knowing that a key was
72//! added or withdrawn. It matches the default token lifetime: no token
73//! outlives the staleness of the key list that would have refused it by more
74//! than one lifetime. [`KeyDiscovery::refresh`] short-circuits the wait when a
75//! rotation is announced some other way.
76//!
77//! A failed refresh keeps whatever was cached before. A root that is briefly
78//! unreachable should not cost a verifier the keys it already had, and the
79//! entry ages out on its own if the outage lasts.
80
81#![deny(missing_docs)]
82#![deny(clippy::all)]
83#![deny(clippy::pedantic)]
84
85mod discovery;
86mod error;
87
88pub use discovery::{DEFAULT_TIMEOUT, DEFAULT_TTL, KeyDiscovery, WELL_KNOWN_PATH, empty_verifier};
89pub use error::DiscoveryError;