Device Signatures
Authkestra supports device-bound signature authentication (proof-of-possession) through the authkestra-devsig crate. This allows you to cryptographically bind sessions or API requests to a specific hardware device, significantly reducing the risk of token theft.
What are Device Signatures?
Section titled “What are Device Signatures?”Unlike traditional bearer tokens (like a standard JWT or session cookie) which can be stolen and replayed by an attacker, device signatures require the client to prove possession of a private key for every request.
The server registers the public key (the “Device Identity”) during enrollment. On subsequent requests, the client signs specific request parameters (like the URI, HTTP method, and a nonce) using their private key. The server verifies this signature using the enrolled public key.
Prerequisites
Section titled “Prerequisites”Add the authkestra-devsig crate to your dependencies:
[dependencies]authkestra-devsig = "0.9"# ...plus the `devsig` feature on whichever adapter you use:authkestra-axum = { version = "0.9", features = ["devsig"] }# authkestra-actix = { version = "0.9", features = ["devsig"] }How to use authkestra-devsig
Section titled “How to use authkestra-devsig”The easiest way to use authkestra-devsig is via the official web framework integrations (authkestra-axum and authkestra-actix). These integrations provide ready-to-use middleware that intercepts requests, buffers the body to compute the required bdh (body digest hash), and verifies the signatures before your route handlers are ever executed.
1. Building the Verifier State
Section titled “1. Building the Verifier State”To start, construct the DevSig configuration state using the unified typestate builder. You will need a VerifierConfig, an IssuerJwks (a JWKS cache of the issuers trusted to mint attestations), and a ReplayStore (to protect against replay attacks).
use authkestra_devsig::{DevSig, InMemoryReplayStore, IssuerJwks, ReplayStore, VerifierConfig};use jsonwebtoken::Algorithm;use std::sync::Arc;use std::time::Duration;
// `VerifierConfig` is `#[non_exhaustive]`, so build it with `new(...)` rather than a// struct literal. Arguments, in order:// trusted_issuers, allowed_algs, max_clock_skew, max_signature_lifetime, audiencelet config = VerifierConfig::new( ["https://op.example.test"], [Algorithm::ES256, Algorithm::EdDSA], Duration::from_secs(5), Duration::from_secs(60), "https://api.example.test",);
// The JWKS cache does no network I/O of its own — populate it with// `jwks.insert(issuer, kid, jwk).await` from wherever you fetch and refresh keys.let jwks = Arc::new(IssuerJwks::new());let replay_store: Arc<dyn ReplayStore> = Arc::new(InMemoryReplayStore::new());
let devsig = DevSig::builder() .config(config) .jwks(jwks) .replay_store(replay_store) .build();2. Wiring the Middleware
Section titled “2. Wiring the Middleware”In Axum, authkestra-axum provides the DeviceSignatureLayer middleware and an AuthDeviceSignature extractor.
use authkestra_axum::devsig::{DeviceSignatureLayer, AuthDeviceSignature};use axum::{Router, routing::post, response::IntoResponse};
// Protect the route using the layerlet app: Router<()> = Router::new() .route("/v1/transfer", post(transfer_handler)) .layer(DeviceSignatureLayer::from(devsig));
// The extractor reads the verified identity populated by the layerasync fn transfer_handler(AuthDeviceSignature(identity): AuthDeviceSignature) -> impl IntoResponse { format!("Verified request for subject: {}, device: {}", identity.subject, identity.device)}In Actix, authkestra-actix provides the equivalent DeviceSignatureAuth middleware and AuthDeviceSignature extractor.
use authkestra_actix::devsig::{DeviceSignatureAuth, AuthDeviceSignature};use actix_web::{App, HttpServer, post, Responder};
// The extractor reads the verified identity populated by the middleware#[post("/v1/transfer")]async fn transfer_handler(identity: AuthDeviceSignature) -> impl Responder { let AuthDeviceSignature(identity) = identity; format!("Verified request for subject: {}, device: {}", identity.subject, identity.device)}
// Protect the route using the wrap() middlewareHttpServer::new(move || { let auth = DeviceSignatureAuth::from(devsig.clone()); App::new().wrap(auth).service(transfer_handler)})By verifying the signature, you ensure that the request was genuinely initiated by the enrolled device, providing high-assurance authentication suitable for sensitive operations.
Testing your principal mapping
Section titled “Testing your principal mapping”The verified identity is an authkestra_devsig::DeviceIdentity, and it is #[non_exhaustive] — you cannot build one with a struct expression from your own crate. Use DeviceIdentity::new(subject, device, key_thumbprint, attributes) to unit-test the code that maps an identity onto your own principal or session type, which is where the security-relevant defaults live (for example, an absent role attribute must fall back to your least-privileged value):
use authkestra_devsig::DeviceIdentity;
let identity = DeviceIdentity::new( "usr_1".to_owned(), "vk_1".to_owned(), "jkt-1".to_owned(), serde_json::json!({}),);assert_eq!(my_mapping(&identity).role, "user");DeviceIdentity::new performs no verification at all: a value built this way is test data and carries no proof that any signature was checked. Only verify() (and therefore the layer/middleware above) produces a DeviceIdentity that means a request passed the algorithm.
Complete, runnable versions of both wirings live in the repository:
cargo run -p authkestra --example axum_devsig --all-featurescargo run -p authkestra --example actix_devsig --all-features