Skip to content
ecdsa.com

Sign an ES256 JWT · Rust

How to sign a JWT with ES256 in Rust

To sign a JWT with ES256 in Rust, the jsonwebtoken crate's encode takes a Header, a Serialize claims struct and an EncodingKey loaded from a PKCS#8 PEM. Since version 10 the crate needs an explicit crypto backend feature; the run builds with rust_crypto and checks the token with OpenSSL.

Tested with jsonwebtoken 11.1.0 (Rust 1.99.1, feature rust_crypto).

Cargo.toml — jsonwebtoken with the pure-Rust backend
[dependencies]
# jsonwebtoken 10+ needs a crypto backend: "rust_crypto" (pure Rust) or "aws_lc_rs"
jsonwebtoken = { version = "=11.1.0", features = ["rust_crypto"] }
serde = { version = "=1.0.229", features = ["derive"] }
src/main.rs — ES256 with jsonwebtoken
use std::time::{SystemTime, UNIX_EPOCH};
use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
use serde::Serialize;
#[derive(Serialize)]
struct Claims<'a> {
iss: &'a str,
sub: &'a str,
aud: &'a str,
iat: u64,
exp: u64,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// from_ec_pem wants PKCS#8 ("BEGIN PRIVATE KEY")
let key = EncodingKey::from_ec_pem(&std::fs::read("key.pem")?)?;
let mut header = Header::new(Algorithm::ES256);
header.kid = Some("2026-10".to_owned());
let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
let claims = Claims {
iss: "https://issuer.example.com",
sub: "user-42",
aud: "api://orders",
iat: now,
exp: now + 15 * 60,
};
println!("{}", encode(&header, &claims, &key)?);
Ok(())
}

How it works

  1. EncodingKey::from_ec_pem reads a BEGIN PRIVATE KEY (PKCS#8) block.
  2. Header::new(Algorithm::ES256) sets alg and typ; header.kid adds the key id.
  3. The claims are any Serialize type; iat and exp are u64 seconds from SystemTime.
  4. encode returns the compact token as a String.

Gotchas

  • Without the rust_crypto or aws_lc_rs feature, the code still compiles and then panics at the first signature: Could not automatically determine the process-level CryptoProvider from jsonwebtoken crate features.
  • from_ec_pem rejects SEC 1 keys (BEGIN EC PRIVATE KEY) with Error(InvalidKeyFormat). Convert them with openssl pkcs8 -topk8 -nocrypt -in ec.pem -out key.pem.
  • Choose exactly one backend: with both features enabled, jsonwebtoken cannot pick a provider automatically and you must install one with CryptoProvider::install_default().
  • Use a struct for claims rather than serde_json::Value with ad-hoc fields: the types catch a string exp or a missing aud at compile time.

Related recipes