Class: JWT::PQ::HybridKey
- Inherits:
-
Object
- Object
- JWT::PQ::HybridKey
- Defined in:
- lib/jwt/pq/hybrid_key.rb
Overview
A composite key that pairs an Ed25519 keypair with an ML-DSA keypair
for hybrid EdDSA+ML-DSA-* JWT signatures.
Hybrid mode concatenates the two signatures (ed25519 || ml_dsa) so
that a verifier only accepts the token if both signatures are
valid. The classical half remains secure against today's attackers
while the post-quantum half resists a future cryptographically
relevant quantum computer.
Requires the ed25519 gem (or jwt-eddsa, which depends on it).
Use hybrid_available? to probe availability.
Instance Attribute Summary collapse
-
#ed25519_signing_key ⇒ Ed25519::SigningKey?
readonly
Ed25519 signing key, or nil for verification-only.
-
#ed25519_verify_key ⇒ Ed25519::VerifyKey
readonly
Ed25519 verification key.
-
#ml_dsa_key ⇒ JWT::PQ::Key
readonly
ML-DSA keypair (public-only or full).
Class Method Summary collapse
-
.generate(ml_dsa_algorithm = :ml_dsa_65) ⇒ HybridKey
Generate a fresh hybrid keypair.
Instance Method Summary collapse
-
#algorithm ⇒ String
The ML-DSA algorithm name (e.g.
"ML-DSA-65"). -
#destroy! ⇒ true
Zero and discard private key material from both halves.
-
#hybrid_algorithm ⇒ String
The hybrid JWT algorithm name (e.g.
"EdDSA+ML-DSA-65"). -
#initialize(ed25519:, ml_dsa:) ⇒ HybridKey
constructor
Build a hybrid key from existing Ed25519 and ML-DSA components.
-
#inspect ⇒ String
(also: #to_s)
Short diagnostic string — never contains key material.
-
#private? ⇒ Boolean
True when both halves have private components and the key can be used for signing.
-
#sign(data) ⇒ String
Produce a hybrid signature (Ed25519 ‖ ML-DSA) over
data.
Constructor Details
#initialize(ed25519:, ml_dsa:) ⇒ HybridKey
Build a hybrid key from existing Ed25519 and ML-DSA components.
Pass an Ed25519::SigningKey for a full signing key, or an
Ed25519::VerifyKey for verification-only.
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
# File 'lib/jwt/pq/hybrid_key.rb', line 46 def initialize(ed25519:, ml_dsa:) require_eddsa_dependency! @ml_dsa_key = ml_dsa @op_mutex = Mutex.new case ed25519 when Ed25519::SigningKey @ed25519_signing_key = ed25519 @ed25519_verify_key = ed25519.verify_key when Ed25519::VerifyKey @ed25519_signing_key = nil @ed25519_verify_key = ed25519 else raise KeyError, "Expected Ed25519::SigningKey or Ed25519::VerifyKey, got #{ed25519.class}" end end |
Instance Attribute Details
#ed25519_signing_key ⇒ Ed25519::SigningKey? (readonly)
Returns Ed25519 signing key, or nil for verification-only.
29 30 31 |
# File 'lib/jwt/pq/hybrid_key.rb', line 29 def ed25519_signing_key @ed25519_signing_key end |
#ed25519_verify_key ⇒ Ed25519::VerifyKey (readonly)
Returns Ed25519 verification key.
32 33 34 |
# File 'lib/jwt/pq/hybrid_key.rb', line 32 def ed25519_verify_key @ed25519_verify_key end |
#ml_dsa_key ⇒ JWT::PQ::Key (readonly)
Returns ML-DSA keypair (public-only or full).
35 36 37 |
# File 'lib/jwt/pq/hybrid_key.rb', line 35 def ml_dsa_key @ml_dsa_key end |
Class Method Details
.generate(ml_dsa_algorithm = :ml_dsa_65) ⇒ HybridKey
Generate a fresh hybrid keypair.
Creates both an Ed25519 SigningKey and an ML-DSA keypair of the
requested parameter set.
73 74 75 76 77 78 79 80 |
# File 'lib/jwt/pq/hybrid_key.rb', line 73 def self.generate(ml_dsa_algorithm = :ml_dsa_65) require_eddsa_dependency! ed_key = Ed25519::SigningKey.generate ml_key = Key.generate(ml_dsa_algorithm) new(ed25519: ed_key, ml_dsa: ml_key) end |
Instance Method Details
#algorithm ⇒ String
Returns the ML-DSA algorithm name (e.g. "ML-DSA-65").
89 90 91 |
# File 'lib/jwt/pq/hybrid_key.rb', line 89 def algorithm @ml_dsa_key.algorithm end |
#destroy! ⇒ true
Zero and discard private key material from both halves.
After calling this, #private? becomes false and the key can only be used for verification. Idempotent — safe on verification-only keys.
Thread-safe: serialized on the hybrid key's own mutex (which also
guards #sign) and internally delegates to
Key#destroy!, which uses its own mutex. A concurrent
#sign will block until destroy! completes and then raise
KeyError, never observing a half-destroyed state.
Ed25519 wipe: Ed25519::SigningKey stores the private seed in two
separate Strings — @seed (32 bytes) returned by #to_bytes, and
@keypair (64 bytes = seed || public_key) returned by #keypair.
Both are attr_reader-backed and hand out the internal String by
reference, so we must zero both in place — wiping only @seed
leaves the first 32 bytes of @keypair holding the seed until GC.
143 144 145 146 147 148 149 150 151 152 153 154 155 |
# File 'lib/jwt/pq/hybrid_key.rb', line 143 def destroy! @op_mutex.synchronize do @ml_dsa_key.destroy! if @ed25519_signing_key seed = @ed25519_signing_key.to_bytes seed.replace("\0" * seed.bytesize) keypair = @ed25519_signing_key.keypair keypair.replace("\0" * keypair.bytesize) @ed25519_signing_key = nil end end true end |
#hybrid_algorithm ⇒ String
Returns the hybrid JWT algorithm name
(e.g. "EdDSA+ML-DSA-65").
95 96 97 |
# File 'lib/jwt/pq/hybrid_key.rb', line 95 def hybrid_algorithm "EdDSA+#{@ml_dsa_key.algorithm}" end |
#inspect ⇒ String Also known as: to_s
Returns short diagnostic string — never contains key material.
158 159 160 |
# File 'lib/jwt/pq/hybrid_key.rb', line 158 def inspect "#<#{self.class} algorithm=#{hybrid_algorithm} private=#{private?}>" end |
#private? ⇒ Boolean
Returns true when both halves have private components and the key can be used for signing.
84 85 86 |
# File 'lib/jwt/pq/hybrid_key.rb', line 84 def private? !@ed25519_signing_key.nil? && @ml_dsa_key.private? end |
#sign(data) ⇒ String
Produce a hybrid signature (Ed25519 ‖ ML-DSA) over data.
Thread-safe: both component signatures are taken under the hybrid
key's own mutex, and #destroy! contends on the same mutex. That
guarantees a concurrent destroy! cannot zero the Ed25519 seed
while libsodium is mid-sign, and cannot produce a half-signed
output (Ed25519 succeeds, ML-DSA fails because the buffer was
just zeroed). Lock order is hybrid mutex → ML-DSA mutex; callers
must not invoke any Key method while holding another lock that
might be taken by destroy!.
114 115 116 117 118 119 120 121 122 |
# File 'lib/jwt/pq/hybrid_key.rb', line 114 def sign(data) @op_mutex.synchronize do raise KeyError, "Ed25519 private key not available — cannot sign" unless @ed25519_signing_key ed_sig = @ed25519_signing_key.sign(data) ml_sig = @ml_dsa_key.sign(data) ed_sig + ml_sig end end |