Class: JWT::PQ::Key

Inherits:
Object
  • Object
show all
Defined in:
lib/jwt/pq/key.rb

Overview

Represents an ML-DSA keypair (public + optional private key). Used as the signing/verification key for JWT operations.

Constant Summary collapse

ALGORITHM_ALIASES =

rubocop:disable Metrics/ClassLength

{
  ml_dsa_44: "ML-DSA-44",
  ml_dsa_65: "ML-DSA-65",
  ml_dsa_87: "ML-DSA-87"
}.freeze
ALGORITHM_OIDS =
{
  "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 =
ALGORITHM_OIDS.invert.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(algorithm:, public_key:, private_key: nil) ⇒ Key

Returns a new instance of Key.



26
27
28
29
30
31
32
33
# File 'lib/jwt/pq/key.rb', line 26

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

  validate!
end

Instance Attribute Details

#algorithmObject (readonly)

Returns the value of attribute algorithm.



24
25
26
# File 'lib/jwt/pq/key.rb', line 24

def algorithm
  @algorithm
end

#private_keyObject (readonly)

Returns the value of attribute private_key.



24
25
26
# File 'lib/jwt/pq/key.rb', line 24

def private_key
  @private_key
end

#public_keyObject (readonly)

Returns the value of attribute public_key.



24
25
26
# File 'lib/jwt/pq/key.rb', line 24

def public_key
  @public_key
end

Class Method Details

.from_pem(pem_string) ⇒ Object

Import a Key from a PEM string (SPKI or PKCS#8).



84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/jwt/pq/key.rb', line 84

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)
  else raise KeyError, "Unsupported PEM format: #{info.format}"
  end
ensure
  info&.key&.wipe! if info&.format == :pkcs8
end

.from_pem_pair(public_pem:, private_pem:) ⇒ Object

Import a Key from separate public and private PEM strings.



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/jwt/pq/key.rb', line 98

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) ⇒ Object

Create a Key from raw public key bytes (verification only).



45
46
47
# File 'lib/jwt/pq/key.rb', line 45

def self.from_public_key(algorithm, public_key_bytes)
  new(algorithm: algorithm, public_key: public_key_bytes)
end

.generate(algorithm) ⇒ Object

Generate a new keypair for the given algorithm.



36
37
38
39
40
41
42
# File 'lib/jwt/pq/key.rb', line 36

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!Object

Zero and discard private key material from Ruby memory. After calling this, the key can only be used for verification.



68
69
70
71
72
73
74
75
76
# File 'lib/jwt/pq/key.rb', line 68

def destroy!
  if @private_key
    @private_key.replace("\0" * @private_key.bytesize)
    @private_key = nil
  end
  @sk_buffer&.clear
  @sk_buffer = nil
  true
end

#inspectObject Also known as: to_s



78
79
80
# File 'lib/jwt/pq/key.rb', line 78

def inspect
  "#<#{self.class} algorithm=#{@algorithm} private=#{private?}>"
end

#private?Boolean

Whether this key can be used for signing.

Returns:

  • (Boolean)


62
63
64
# File 'lib/jwt/pq/key.rb', line 62

def private?
  !@private_key.nil?
end

#private_to_pemObject

Export the private key as PEM (PKCS#8 format).

Raises:



123
124
125
126
127
128
129
# File 'lib/jwt/pq/key.rb', line 123

def private_to_pem
  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

#sign(data) ⇒ Object

Sign data using the private key.

Raises:



50
51
52
53
54
# File 'lib/jwt/pq/key.rb', line 50

def sign(data)
  raise KeyError, "Private key not available — cannot sign" unless @private_key

  @ml_dsa.sign_with_sk_buffer(data, sk_buffer)
end

#to_pemObject

Export the public key as PEM (SPKI format).



116
117
118
119
120
# File 'lib/jwt/pq/key.rb', line 116

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) ⇒ Object

Verify a signature using the public key.



57
58
59
# File 'lib/jwt/pq/key.rb', line 57

def verify(data, signature)
  @ml_dsa.verify_with_pk_buffer(data, signature, pk_buffer)
end