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