defense in depth · transitional

Hybrid EdDSA + ML-DSA.

Sign with Ed25519 and ML-DSA in the same pass. Both must verify. An attacker has to break both algorithms to forge the token.

Why hybrid

ML-DSA is young. It is a NIST standard, it has been thoroughly analysed, and it is the right long-term bet — but it has not yet absorbed the decades of cryptanalysis that RSA and elliptic curves have. Ed25519, meanwhile, is rock-solid against classical adversaries but falls to a cryptographically-relevant quantum computer.

A hybrid signature requires both halves to verify. It is only forgeable by an attacker who can break both algorithms — the smaller of two unlikely events. Treat it as transitional: run it while the industry gains confidence in ML-DSA alone. Dropping to pure ML-DSA later is a single-field change in the JWT header.

Installation

# Gemfile
gem "jwt-pq"
gem "jwt-eddsa"   # required for hybrid mode

require "jwt/pq"  # registers EdDSA+ML-DSA-{44,65,87}

Usage

hybrid_key = JWT::PQ::HybridKey.generate(:ml_dsa_65)

token = JWT.encode({ sub: "u-1" }, hybrid_key, "EdDSA+ML-DSA-65")

payload, header = JWT.decode(
  token, hybrid_key, true, algorithms: ["EdDSA+ML-DSA-65"]
)
header["alg"]     # => "EdDSA+ML-DSA-65"
header["pq_alg"]  # => "ML-DSA-65"

Wire format

A hybrid JWT's signature section is the concatenation of the Ed25519 signature (exactly 64 bytes) followed by the ML-DSA signature for the chosen parameter set, stored in the standard JWT signature field. Both signatures cover the same signing input — base64url(header) + "." + base64url(payload) — and both must verify.

alg Ed25519 ML-DSA Combined
EdDSA+ML-DSA-4464 B2,420 B2,484 B
EdDSA+ML-DSA-6564 B3,309 B3,373 B
EdDSA+ML-DSA-8764 B4,627 B4,691 B

Cost

Token size is dominated by the ML-DSA portion — Ed25519 adds a flat 64 bytes. CPU cost is dominated by ML-DSA too; Ed25519 verification is a fraction of an ML-DSA verify. Expect hybrid EdDSA+ML-DSA-65 to run at roughly 4,700 sign/s and 3,900 verify/s on commodity hardware (vs 5,970 / 9,300 for pure ML-DSA-65).

Header convention

The alg header uses a ClassicAlg+PQAlg convention. jwt-pq also writes a pq_alg header naming the ML-DSA parameter set directly, to simplify key selection at the verifier.

The IETF draft draft-ietf-cose-dilithium is still evolving. The alg string for hybrid mode may change as the draft finalizes. See SPEC.md for the tracked-specs table and the compatibility policy for draft transitions.