authkestra_devsig/lib.rs
1//! `authkestra-devsig` — device-bound signature authentication.
2//!
3//! This is the fourth request-authentication family, alongside OAuth/OIDC, sessions, and Basic:
4//! **per-request proof of possession of a device-bound private key, with the public key and its
5//! identity binding travelling in the request itself.** Verification needs no session store, no
6//! token introspection, and no per-request network call — any service holding one cached Issuer
7//! JWKS can verify a request independently.
8//!
9//! ## The two credentials
10//!
11//! Every request carries two JWSes that do different jobs:
12//!
13//! - **`X-Signature`** — short-lived, single-use, signed by the *device*. Proves possession of
14//! the private key for *this* request.
15//! - **`X-Attestation`** — long-lived, reusable, public, signed by the *Issuer*. Binds that key's
16//! RFC 7638 thumbprint (`cnf.jkt`) to an identity and its attributes. This is what
17//! `authkestra-op` mints at enrolment (tracked separately in
18//! [authkestra#136](https://github.com/marcjazz/authkestra/issues/136)); this crate only
19//! verifies attestations, it does not mint them.
20//!
21//! Neither is sufficient alone: the signature without the attestation is an anonymous key, and
22//! the attestation without the signature is exactly the bearer-token pattern this design exists
23//! to avoid — see [`VerifyError::MissingCredential`]. Only together, and only after the
24//! thumbprint binding is checked, do they authenticate anything. See [`verify`]'s docs for why
25//! that check is the one step in the algorithm that cannot be skipped, reordered, or inferred
26//! from the others.
27//!
28//! ## Entry point
29//!
30//! [`verify`] is a plain async function, deliberately decoupled from any HTTP framework:
31//!
32//! ```ignore
33//! let identity = authkestra_devsig::verify(&request, &config, &jwks, &replay_store).await?;
34//! ```
35//!
36//! ## Framework integration — lives in the adapter crates, per `AGENTS.md`
37//!
38//! Two extension points exist in `authkestra-engine` today: `AuthMethod` (roadmapped, used only
39//! in tests, and its `AuthInput` carries no HTTP request context at all) and
40//! `AuthenticationStrategy<I>` (what `Guard<I>`/`JwtStrategy` actually chain, but
41//! `authenticate(&self, parts: &Parts)` has no body — and this scheme needs the raw body bytes
42//! for the `bdh` check). Neither can express a body-aware verifier today, and deciding which one
43//! should grow that capability — or whether a new trait should — is the framework's own
44//! architecture call, tracked in
45//! [authkestra#137](https://github.com/marcjazz/authkestra/issues/137).
46//!
47//! This crate stays framework-agnostic (per `AGENTS.md`'s "Framework Agnostic" rule) and exposes
48//! only the plain [`verify`] function plus the types it needs. Framework wiring lives in the
49//! adapter crates instead:
50//!
51//! - `authkestra-axum`'s `devsig` feature (see its `devsig` module) ships a `tower::Layer` that
52//! buffers and hashes the body ahead of axum's extraction and injects a verified
53//! [`DeviceIdentity`] into request extensions, plus a thin `FromRequestParts` extractor that
54//! reads it back out.
55//! - `authkestra-actix`'s `devsig` feature (see its `devsig` module) ships the equivalent
56//! `actix_web::dev::Transform` middleware and `FromRequest` extractor.
57//!
58//! Both exist specifically because they need neither `authkestra-engine` trait to exist yet, and
59//! migrating to whichever trait the maintainer lands on is a matter of swapping which caller
60//! builds a [`SignedRequest`] and calls [`verify`] — the algorithm itself is unaffected either
61//! way. See each adapter crate's `devsig` module docs for why an `AuthenticationStrategy<I>`
62//! impl is deliberately *not* provided today.
63
64mod attestation;
65mod config;
66mod error;
67mod identity;
68mod jwks;
69mod jws_util;
70mod replay;
71mod request;
72mod signature;
73mod verify;
74
75pub use config::VerifierConfig;
76pub use error::VerifyError;
77pub use identity::DeviceIdentity;
78pub use jwks::IssuerJwks;
79pub use replay::{InMemoryReplayStore, ReplayError, ReplayStore, UnavailableReplayStore};
80pub use request::SignedRequest;
81pub use verify::verify;
82pub mod builder;
83pub use builder::{DevSig, DevSigBuilder};