Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

akamu-jose — JWK/JWS Primitives

akamu-jose is a standalone Rust crate that provides RFC 7517 JWK key handling and RFC 7515 JWS signing and verification. It supports both classical and post-quantum algorithms and has no HTTP, database, or server dependencies.

What it provides

  • Parsing RFC 7517 public keys from JSON (JwkPublic)
  • Computing RFC 7638 JWK thumbprints
  • Converting public keys to SPKI DER for use with synta-certificate
  • Signing and verifying RFC 7515 JWS flattened serialization (JwsFlattened)
  • Decoding JWS protected headers (JwsProtectedHeader)
  • All classical algorithms: ES256/ES384/ES512, PS256/PS384/PS512, EdDSA
  • Post-quantum algorithms: ML-DSA-44, ML-DSA-65, ML-DSA-87 (draft-ietf-cose-dilithium-11)

What it does NOT provide

  • HTTP requests of any kind
  • ACME protocol logic
  • Database access
  • Certificate issuance or CSR handling

For those, use akamu-client.

JwkPublic

JwkPublic represents an RFC 7517 public key. It can be constructed from a JSON JWK, from a synta-certificate BackendPublicKey, or converted to SPKI DER.

Parsing a JWK

#![allow(unused)]
fn main() {
use akamu_jose::JwkPublic;

let json = r#"{"kty":"EC","crv":"P-256","x":"...","y":"..."}"#;
let jwk: JwkPublic = serde_json::from_str(json)?;
}

Computing a thumbprint

The thumbprint is a SHA-256 digest of the canonical JWK representation (RFC 7638). It is used in ACME key authorizations.

#![allow(unused)]
fn main() {
let thumb = jwk.thumbprint()?;  // returns String (base64url, no padding)
}

Converting to SPKI DER

to_spki_der is needed when you want to pass the public key to synta-certificate for signature verification or certificate issuance.

#![allow(unused)]
fn main() {
let spki_der: Vec<u8> = jwk.to_spki_der()?;
}

Constructing from a public key

If you have a BackendPublicKey from synta-certificate, convert it to a JwkPublic:

#![allow(unused)]
fn main() {
let jwk = JwkPublic::from_public_key(&backend_public_key)?;
}

JwsFlattened

JwsFlattened is the RFC 7515 flattened JSON serialization. It is the format used by all ACME POST requests.

Signing

JwsFlattened::sign takes a protected header (already base64url-encoded JSON), a payload (already base64url-encoded), and a BackendPrivateKey.

#![allow(unused)]
fn main() {
use akamu_jose::{JwsFlattened, JwsProtectedHeader, JwsKeyRef};

// Sign with ES256 (P-256 key)
let protected_b64 = base64url_encode(&serde_json::to_vec(&header)?);
let payload_b64   = base64url_encode(payload_json.as_bytes());

let jws = JwsFlattened::sign(&protected_b64, &payload_b64, &private_key)?;
}

For ML-DSA-87, the call is identical — the algorithm is determined by the key type, not by a separate parameter:

#![allow(unused)]
fn main() {
// private_key is a BackendPrivateKey for an ML-DSA-87 key
let jws = JwsFlattened::sign(&protected_b64, &payload_b64, &private_key)?;
}

ML-DSA signatures are produced over the raw signing input using an empty context string as required by draft-ietf-cose-dilithium-11 §4.

Verifying

verify checks the signature against a provided SPKI DER:

#![allow(unused)]
fn main() {
let spki_der = jwk.to_spki_der()?;
jws.verify(&spki_der)?;   // returns Ok(()) or Err(JoseError)
}

Decoding header and payload

#![allow(unused)]
fn main() {
let header: JwsProtectedHeader = jws.decode_header()?;
let payload_bytes: Vec<u8> = jws.decode_payload()?;
}

JwsProtectedHeader

The decoded ACME JWS protected header. Fields:

FieldTypeMeaning
algStringAlgorithm string (e.g. "ES256", "ML-DSA-87")
nonceStringACME anti-replay nonce
urlStringTarget URL (must match the request URL)
key_refJwsKeyRefKey identification: embedded JWK or account kid

JwsKeyRef

JwsKeyRef indicates how the signing key is identified in the JWS header.

#![allow(unused)]
fn main() {
pub enum JwsKeyRef {
    Jwk { jwk: JwkPublic },  // embedded public key (first request; new-account, key-change)
    Kid { kid: String },      // account URL (all subsequent requests)
}
}

Use Jwk when the request is made before an account exists (new-account) or for key-change operations where the new key must be embedded. Use Kid for all other ACME requests once an account URL is known.

Algorithm support

Familyalg stringJWK kty / crvNotes
ECDSAES256EC / P-256SHA-256
ECDSAES384EC / P-384SHA-384
ECDSAES512EC / P-521SHA-512
RSASSA-PSSPS256RSASHA-256 / MGF1-SHA-256
RSASSA-PSSPS384RSASHA-384 / MGF1-SHA-384
RSASSA-PSSPS512RSASHA-512 / MGF1-SHA-512
EdDSAEdDSAOKP / Ed25519 or Ed448RFC 8037
ML-DSAML-DSA-44LWE (draft)draft-ietf-cose-dilithium-11
ML-DSAML-DSA-65LWE (draft)draft-ietf-cose-dilithium-11
ML-DSAML-DSA-87LWE (draft)draft-ietf-cose-dilithium-11

JoseError

#![allow(unused)]
fn main() {
pub enum JoseError {
    BadRequest(String),           // malformed input (missing field, wrong format)
    Crypto(String),               // signature failure or key operation error
    UnsupportedAlgorithm(String), // alg string not recognized
    Base64(String),               // base64url decode failure
    Json(String),                 // JSON parse failure
}
}

When akamu-jose is used inside the server, From<JoseError> for AcmeError converts these automatically:

  • BadRequestAcmeError::BadRequest
  • CryptoAcmeError::Crypto
  • UnsupportedAlgorithmAcmeError::BadSignatureAlgorithm

Full example: generate P-256 key, compute thumbprint, sign a JWS

#![allow(unused)]
fn main() {
use akamu_jose::{JwkPublic, JwsFlattened};
use synta_certificate::BackendPrivateKey;
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};

fn base64url(data: &[u8]) -> String {
    URL_SAFE_NO_PAD.encode(data)
}

// 1. Generate a P-256 private key via synta-certificate
let private_key = BackendPrivateKey::generate("ec:P-256")?;
let public_key  = private_key.to_public_key()?;

// 2. Wrap as JwkPublic and compute the thumbprint
let jwk   = JwkPublic::from_public_key(&public_key)?;
let thumb = jwk.thumbprint()?;
println!("Thumbprint: {thumb}");

// 3. Build an ACME-style protected header (illustrative; real code uses serde)
let header_json = serde_json::json!({
    "alg":   "ES256",
    "nonce": "some-nonce",
    "url":   "https://acme.example.com/acme/new-account",
    "jwk":   serde_json::to_value(&jwk)?,
});
let protected_b64 = base64url(&serde_json::to_vec(&header_json)?);

// 4. Encode the payload
let payload_json  = serde_json::json!({"termsOfServiceAgreed": true});
let payload_b64   = base64url(&serde_json::to_vec(&payload_json)?);

// 5. Sign
let jws = JwsFlattened::sign(&protected_b64, &payload_b64, &private_key)?;

// 6. Verify round-trip
let spki_der = jwk.to_spki_der()?;
jws.verify(&spki_der)?;
println!("Signature verified");
}