Class: JWT::PQ::JWK

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

Overview

JWK (JSON Web Key) import/export for ML-DSA keys.

Follows the draft-ietf-cose-dilithium conventions for the AKP ("Algorithm Key Pair") key type:

  • kty: "AKP"
  • alg: "ML-DSA-44", "ML-DSA-65", or "ML-DSA-87"
  • pub: base64url-encoded public key (no padding)
  • priv: base64url-encoded private key (optional, no padding)
  • kid: RFC 7638 thumbprint over the required members

Examples:

Export and re-import

jwk = JWT::PQ::JWK.new(key).export
restored = JWT::PQ::JWK.import(jwk)

See Also:

Constant Summary collapse

ALGORITHMS =

Algorithm names accepted in the alg field.

MlDsa::ALGORITHMS.keys.freeze
KTY =

Value of the kty field for all ML-DSA JWKs.

"AKP"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(key) ⇒ JWK

Wrap a Key for JWK operations.

Parameters:

Raises:



40
41
42
43
44
# File 'lib/jwt/pq/jwk.rb', line 40

def initialize(key)
  raise KeyError, "Expected a JWT::PQ::Key, got #{key.class}" unless key.is_a?(JWT::PQ::Key)

  @key = key
end

Instance Attribute Details

#keyJWT::PQ::Key (readonly)

Returns the wrapped key.

Returns:



34
35
36
# File 'lib/jwt/pq/jwk.rb', line 34

def key
  @key
end

Class Method Details

.compute_thumbprint(algorithm, public_key) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Compute an RFC 7638 thumbprint from algorithm + public key bytes without allocating a JWT::PQ::JWK or Key wrapper.

Parameters:

  • algorithm (String)

    canonical algorithm name.

  • public_key (String)

    raw public key bytes.

Returns:

  • (String)

    base64url-encoded SHA-256 thumbprint.



119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/jwt/pq/jwk.rb', line 119

def self.compute_thumbprint(algorithm, public_key)
  # RFC 7638 §3.2: canonical JSON over the required members in
  # lexicographic order (alg, kty, pub), no whitespace. Using
  # `JSON.generate` over an ordered Hash instead of string
  # interpolation so a future algorithm or key-byte change that
  # introduces a character needing JSON escape does not silently
  # produce a divergent thumbprint.
  pub_b64 = ::Base64.urlsafe_encode64(public_key, padding: false)
  canonical = JSON.generate({ alg: algorithm, kty: KTY, pub: pub_b64 })
  digest = OpenSSL::Digest::SHA256.digest(canonical)
  ::Base64.urlsafe_encode64(digest, padding: false)
end

.import(jwk_hash) ⇒ JWT::PQ::Key

Import a Key from a JWK hash.

Accepts string or symbol keys. Validates kty, alg, and the presence/base64url-ness of pub (and priv if present).

Parameters:

  • jwk_hash (Hash)

    a JWK object.

Returns:

  • (JWT::PQ::Key)

    a key reconstructed from the JWK — with a private component iff the JWK carried a priv field.

Raises:

  • (KeyError)

    on missing/wrong kty, missing/unsupported alg, missing pub, wrong field types, or invalid base64url in pub/priv.



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/jwt/pq/jwk.rb', line 79

def self.import(jwk_hash)
  raise KeyError, "Expected a Hash for JWK, got #{jwk_hash.class}" unless jwk_hash.is_a?(Hash)

  jwk = normalize_keys(jwk_hash)

  validate_kty!(jwk)
  alg = validate_alg!(jwk)
  raise KeyError, "Missing 'pub' in JWK" unless jwk.key?("pub")
  raise KeyError, "'pub' must be a String, got #{jwk["pub"].class}" unless jwk["pub"].is_a?(String)

  pub_bytes = decode_field(jwk, "pub")

  if jwk.key?("priv")
    raise KeyError, "'priv' must be a String, got #{jwk["priv"].class}" unless jwk["priv"].is_a?(String)

    priv_bytes = decode_field(jwk, "priv")
    Key.new(algorithm: alg, public_key: pub_bytes, private_key: priv_bytes)
  else
    Key.new(algorithm: alg, public_key: pub_bytes)
  end
end

Instance Method Details

#export(include_private: false) ⇒ Hash{Symbol=>String}

Export the key as a JWK hash.

By default, only the public material is included. Pass include_private: true to emit the priv field as well (and only when the wrapped key actually has a private component).

Parameters:

  • include_private (Boolean) (defaults to: false)

    include the priv field. Default: false.

Returns:

  • (Hash{Symbol=>String})

    a JWK with :kty, :alg, :pub, :kid, and optionally :priv.



55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/jwt/pq/jwk.rb', line 55

def export(include_private: false)
  jwk = {
    kty: KTY,
    alg: @key.algorithm,
    pub: base64url_encode(@key.public_key),
    kid: thumbprint
  }

  jwk[:priv] = base64url_encode(@key.private_key) if include_private && @key.private?

  jwk
end

#thumbprintString

Compute the JWK Thumbprint (RFC 7638) used as kid.

Delegates to Key#jwk_thumbprint, which memoizes the result on the key — repeated calls on the same key avoid recomputing the canonical JSON + SHA-256 digest.

Returns:

  • (String)

    base64url-encoded SHA-256 thumbprint.



108
109
110
# File 'lib/jwt/pq/jwk.rb', line 108

def thumbprint
  @key.jwk_thumbprint
end