Class: JWT::PQ::Key
- Inherits:
-
Object
- Object
- JWT::PQ::Key
- Defined in:
- lib/jwt/pq/key.rb
Overview
An ML-DSA keypair (public key + optional private key) used for JWT signing and verification.
Prefer the class-level constructors over new:
- Key.generate — create a fresh keypair
- Key.from_pem — import from a combined SPKI or PKCS#8 PEM
- Key.from_pem_pair — import from separate public/private PEMs
- Key.from_public_key — wrap raw public key bytes (verification only)
Constant Summary collapse
- ALGORITHM_ALIASES =
Symbol → canonical algorithm name.
{ ml_dsa_44: "ML-DSA-44", ml_dsa_65: "ML-DSA-65", ml_dsa_87: "ML-DSA-87" }.freeze
- ALGORITHM_OIDS =
Algorithm name → ASN.1 OID.
{ "ML-DSA-44" => PqcAsn1::OID::ML_DSA_44, "ML-DSA-65" => PqcAsn1::OID::ML_DSA_65, "ML-DSA-87" => PqcAsn1::OID::ML_DSA_87 }.freeze
- OID_TO_ALGORITHM =
ASN.1 OID → algorithm name.
ALGORITHM_OIDS.invert.freeze
Instance Attribute Summary collapse
-
#algorithm ⇒ String
readonly
Canonical algorithm name (
"ML-DSA-44","ML-DSA-65", or"ML-DSA-87"). -
#private_key ⇒ String?
readonly
Raw private (secret) key bytes, or nil for verification-only keys.
-
#public_key ⇒ String
readonly
Raw public key bytes.
Class Method Summary collapse
-
.from_pem(pem_string) ⇒ Key
Import a Key from a PEM string.
-
.from_pem_pair(public_pem:, private_pem:) ⇒ Key
Import a Key from separate public and private PEM strings.
-
.from_public_key(algorithm, public_key_bytes) ⇒ Key
Wrap raw public key bytes for verification-only use.
-
.generate(algorithm) ⇒ Key
Generate a new keypair for the given algorithm.
Instance Method Summary collapse
-
#destroy! ⇒ true
Zero and discard private key material from Ruby memory.
-
#initialize(algorithm:, public_key:, private_key: nil) ⇒ Key
constructor
Low-level constructor.
-
#inspect ⇒ String
(also: #to_s)
Short diagnostic string — never contains key material.
-
#jwk_thumbprint ⇒ String
RFC 7638 JWK Thumbprint for this key, memoized.
-
#private? ⇒ Boolean
True when this key has a private component and can sign.
-
#private_to_pem ⇒ String
Export the private key as a PKCS#8 PEM string.
-
#sign(data) ⇒ String
Sign data using the private key.
-
#to_pem ⇒ String
Export the public key as an SPKI PEM string.
-
#verify(data, signature) ⇒ Boolean
Verify a signature against data using the public key.
Constructor Details
#initialize(algorithm:, public_key:, private_key: nil) ⇒ Key
Low-level constructor. Prefer generate, from_pem, from_pem_pair, or from_public_key in application code.
63 64 65 66 67 68 69 70 71 72 |
# File 'lib/jwt/pq/key.rb', line 63 def initialize(algorithm:, public_key:, private_key: nil) @algorithm = resolve_algorithm(algorithm) @ml_dsa = MlDsa.new(@algorithm) @public_key = public_key @private_key = private_key @op_mutex = Mutex.new validate! init_ffi_buffers! end |
Instance Attribute Details
#algorithm ⇒ String (readonly)
Returns canonical algorithm name ("ML-DSA-44", "ML-DSA-65", or "ML-DSA-87").
43 44 45 |
# File 'lib/jwt/pq/key.rb', line 43 def algorithm @algorithm end |
#private_key ⇒ String? (readonly)
Returns raw private (secret) key bytes, or nil for verification-only keys.
50 51 52 |
# File 'lib/jwt/pq/key.rb', line 50 def private_key @private_key end |
#public_key ⇒ String (readonly)
Returns raw public key bytes.
46 47 48 |
# File 'lib/jwt/pq/key.rb', line 46 def public_key @public_key end |
Class Method Details
.from_pem(pem_string) ⇒ Key
Import a Key from a PEM string.
Accepts both SPKI (public-only) and PKCS#8 (private + embedded public) PEM documents. For a PKCS#8 PEM that does not carry the public key, use from_pem_pair with a separate public PEM instead.
191 192 193 194 195 196 197 198 199 200 201 202 203 204 |
# File 'lib/jwt/pq/key.rb', line 191 def self.from_pem(pem_string) info = PqcAsn1::DER.parse_pem(pem_string) alg_name = resolve_oid!(info.oid) case info.format when :spki then new(algorithm: alg_name, public_key: info.key) when :pkcs8 then build_from_pkcs8(info, alg_name) # :nocov: — defensive guard; PqcAsn1::DER.parse_pem only returns :spki or :pkcs8 else raise KeyError, "Unsupported PEM format: #{info.format}" # :nocov: end ensure info&.key&.wipe! if info&.format == :pkcs8 end |
.from_pem_pair(public_pem:, private_pem:) ⇒ Key
Import a Key from separate public and private PEM strings.
Use this when your private PEM is PKCS#8 without an embedded public key, or when public and private material come from different sources.
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 |
# File 'lib/jwt/pq/key.rb', line 216 def self.from_pem_pair(public_pem:, private_pem:) pub_info = PqcAsn1::DER.parse_pem(public_pem) priv_info = PqcAsn1::DER.parse_pem(private_pem) pub_alg = OID_TO_ALGORITHM[pub_info.oid] priv_alg = OID_TO_ALGORITHM[priv_info.oid] raise KeyError, "Unknown OID in public PEM: #{pub_info.oid.dotted}" unless pub_alg raise KeyError, "Unknown OID in private PEM: #{priv_info.oid.dotted}" unless priv_alg raise KeyError, "Algorithm mismatch: public=#{pub_alg}, private=#{priv_alg}" unless pub_alg == priv_alg sk_bytes = extract_secure_bytes(priv_info.key) new(algorithm: pub_alg, public_key: pub_info.key, private_key: sk_bytes) ensure priv_info&.key&.wipe! end |
.from_public_key(algorithm, public_key_bytes) ⇒ Key
Wrap raw public key bytes for verification-only use.
93 94 95 |
# File 'lib/jwt/pq/key.rb', line 93 def self.from_public_key(algorithm, public_key_bytes) new(algorithm: algorithm, public_key: public_key_bytes) end |
.generate(algorithm) ⇒ Key
Generate a new keypair for the given algorithm.
80 81 82 83 84 85 86 |
# File 'lib/jwt/pq/key.rb', line 80 def self.generate(algorithm) alg_name = resolve_algorithm(algorithm) ml_dsa = MlDsa.new(alg_name) pk, sk = ml_dsa.keypair new(algorithm: alg_name, public_key: pk, private_key: sk) end |
Instance Method Details
#destroy! ⇒ true
Zero and discard private key material from Ruby memory.
After calling this, #private? becomes false and the key can only be used for verification. Idempotent — safe to call multiple times, and on verification-only keys.
Thread-safe: serialized on a per-instance mutex shared with
#sign and #verify, so destroy! waits for any in-flight
signing or verification to complete before zeroing the buffer.
146 147 148 149 150 151 152 153 154 155 156 |
# File 'lib/jwt/pq/key.rb', line 146 def destroy! @op_mutex.synchronize do if @private_key @private_key.replace("\0" * @private_key.bytesize) @private_key = nil end @sk_buffer&.clear @sk_buffer = nil end true end |
#inspect ⇒ String Also known as: to_s
Returns short diagnostic string — never contains key material.
159 160 161 |
# File 'lib/jwt/pq/key.rb', line 159 def inspect "#<#{self.class} algorithm=#{@algorithm} private=#{private?}>" end |
#jwk_thumbprint ⇒ String
RFC 7638 JWK Thumbprint for this key, memoized.
The thumbprint depends only on the canonical JSON of
{alg, kty, pub} — all immutable for the lifetime of a Key —
so it is computed lazily on first access and cached. Useful for
callers (e.g. JWKSet) that index many keys by kid
without wanting to allocate a JWK wrapper each time.
Safe to call concurrently on a shared key: the inputs are
immutable post-construction, so a concurrent first access at
worst recomputes the same deterministic string; the ||=
assignment is a single atomic reference write on MRI.
178 179 180 |
# File 'lib/jwt/pq/key.rb', line 178 def jwk_thumbprint @jwk_thumbprint ||= JWT::PQ::JWK.compute_thumbprint(@algorithm, @public_key) end |
#private? ⇒ Boolean
Returns true when this key has a private component and can sign.
131 132 133 |
# File 'lib/jwt/pq/key.rb', line 131 def private? !@private_key.nil? end |
#private_to_pem ⇒ String
Export the private key as a PKCS#8 PEM string.
The PEM carries both the private key and the public key (so the pair can later be re-imported with from_pem alone).
Thread-safe: the read of @private_key and the DER build are
serialized against #destroy! on the per-instance mutex, so a
concurrent destroy! cannot zero the bytes mid-encode.
253 254 255 256 257 258 259 260 261 |
# File 'lib/jwt/pq/key.rb', line 253 def private_to_pem @op_mutex.synchronize do raise KeyError, "Private key not available" unless @private_key oid = ALGORITHM_OIDS[@algorithm] secure_der = PqcAsn1::DER.build_pkcs8(oid, @private_key, public_key: @public_key) secure_der.to_pem end end |
#sign(data) ⇒ String
108 109 110 111 112 113 114 |
# File 'lib/jwt/pq/key.rb', line 108 def sign(data) @op_mutex.synchronize do raise KeyError, "Private key not available — cannot sign" unless @sk_buffer @ml_dsa.sign_with_sk_buffer(data, @sk_buffer) end end |
#to_pem ⇒ String
Export the public key as an SPKI PEM string.
236 237 238 239 240 |
# File 'lib/jwt/pq/key.rb', line 236 def to_pem oid = ALGORITHM_OIDS[@algorithm] der = PqcAsn1::DER.build_spki(oid, @public_key) PqcAsn1::PEM.encode(der, "PUBLIC KEY") end |
#verify(data, signature) ⇒ Boolean
124 125 126 127 128 |
# File 'lib/jwt/pq/key.rb', line 124 def verify(data, signature) @op_mutex.synchronize do @ml_dsa.verify_with_pk_buffer(data, signature, @pk_buffer) end end |