Module: PQCrypto::JWT::JWK

Defined in:
lib/pq_crypto/jwt/jwk.rb

Constant Summary collapse

KTY =
"AKP".freeze
SEED_BYTES =
defined?(PQCrypto::PKCS8::ML_DSA_SEED_BYTES) ? PQCrypto::PKCS8::ML_DSA_SEED_BYTES : 32
THUMBPRINT_URI_PREFIX =
"urn:ietf:params:oauth:jwk-thumbprint:sha-256:".freeze

Class Method Summary collapse

Class Method Details

.algorithm_from_jwk!(jwk) ⇒ Object



104
105
106
107
108
# File 'lib/pq_crypto/jwt/jwk.rb', line 104

def algorithm_from_jwk!(jwk)
  raise PQCrypto::JWT::Error, "Unsupported JWK kty: #{jwk['kty'].inspect}" unless jwk["kty"] == KTY

  pq_algorithm_from_jose!(jwk["alg"])
end

.base64url(bytes) ⇒ Object



87
88
89
# File 'lib/pq_crypto/jwt/jwk.rb', line 87

def base64url(bytes)
  Base64.urlsafe_encode64(String(bytes).b, padding: false)
end

.base64url_decode(value) ⇒ Object



91
92
93
94
95
96
# File 'lib/pq_crypto/jwt/jwk.rb', line 91

def base64url_decode(value)
  raw = String(value)
  Base64.urlsafe_decode64(raw + ("=" * ((4 - raw.bytesize % 4) % 4)))
rescue ArgumentError => e
  raise PQCrypto::JWT::Error, "Invalid base64url value: #{e.message}"
end

.from_public_key(public_key, kid: nil, use: nil, key_ops: nil) ⇒ Object



16
17
18
19
# File 'lib/pq_crypto/jwt/jwk.rb', line 16

def from_public_key(public_key, kid: nil, use: nil, key_ops: nil)
  validate_key!(public_key, PQCrypto::Signature::PublicKey)
  public_jwk(public_key.algorithm, public_key.to_bytes, kid: kid, use: use, key_ops: key_ops).freeze
end

.from_secret_key(secret_key, kid: nil, public_key: nil, use: nil, key_ops: nil) ⇒ Object



42
43
44
45
46
47
48
49
50
51
52
# File 'lib/pq_crypto/jwt/jwk.rb', line 42

def from_secret_key(secret_key, kid: nil, public_key: nil, use: nil, key_ops: nil)
  if secret_key.is_a?(PQCrypto::Signature::Keypair)
    public_key ||= secret_key.public_key
    secret_key = secret_key.secret_key
  end
  validate_key!(secret_key, PQCrypto::Signature::SecretKey)

  from_seed(seed_from_secret_key!(secret_key),
            alg: jose_alg_for!(secret_key.algorithm),
            kid: kid, public_key: public_key, use: use, key_ops: key_ops)
end

.from_seed(seed, alg:, kid: nil, public_key: nil, use: nil, key_ops: nil, verify_public: false) ⇒ Object



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/pq_crypto/jwt/jwk.rb', line 21

def from_seed(seed, alg:, kid: nil, public_key: nil, use: nil, key_ops: nil, verify_public: false)
  algorithm = pq_algorithm_from_jose!(alg)
  seed_bytes = validate_seed!(seed, algorithm)
  derived = derive_public_key(algorithm, seed_bytes) if public_key.nil? || verify_public
  public_key ||= derived
  unless public_key
    raise PQCrypto::JWT::UnsupportedFeature,
          "AKP JWK export from seed requires pq_crypto public_key_from_seed/keypair_from_seed or public_key:"
  end

  validate_key!(public_key, PQCrypto::Signature::PublicKey)
  unless public_key.algorithm == algorithm
    raise PQCrypto::JWT::KeyTypeError, "public_key algorithm mismatch: expected #{algorithm.inspect}, got #{public_key.algorithm.inspect}"
  end
  check_seed_matches_public!(public_key, derived) if verify_public

  public_jwk(algorithm, public_key.to_bytes, kid: kid, use: use, key_ops: key_ops)
    .merge!("priv" => base64url(seed_bytes))
    .freeze
end

.normalize_hash!(hash) ⇒ Object



98
99
100
101
102
# File 'lib/pq_crypto/jwt/jwk.rb', line 98

def normalize_hash!(hash)
  raise PQCrypto::JWT::Error, "JWK must be a Hash-like object" unless hash.respond_to?(:to_hash)

  hash.to_hash.each_with_object({}) { |(key, value), out| out[String(key)] = value }
end

.public_key_from_jwk(hash) ⇒ Object



54
55
56
57
58
59
60
# File 'lib/pq_crypto/jwt/jwk.rb', line 54

def public_key_from_jwk(hash)
  jwk = normalize_hash!(hash)
  algorithm = algorithm_from_jwk!(jwk)
  PQCrypto::Signature.public_key_from_bytes(algorithm, decode_field!(jwk, "pub", algorithm))
rescue ArgumentError, PQCrypto::Error => e
  raise PQCrypto::JWT::Error, e.message
end

.secret_key_from_jwk(hash, verify_public: false) ⇒ Object



62
63
64
65
66
67
68
69
70
71
72
# File 'lib/pq_crypto/jwt/jwk.rb', line 62

def secret_key_from_jwk(hash, verify_public: false)
  jwk = normalize_hash!(hash)
  algorithm = algorithm_from_jwk!(jwk)
  decode_field!(jwk, "pub", algorithm)
  seed = validate_seed!(base64url_decode(jwk.fetch("priv") { raise PQCrypto::JWT::Error, "JWK priv is required" }), algorithm)
  check_seed_matches_public!(public_key_from_jwk(jwk), derive_public_key(algorithm, seed)) if verify_public

  PQCrypto::Signature.secret_key_from_seed(algorithm, seed)
rescue ArgumentError, PQCrypto::Error => e
  raise PQCrypto::JWT::Error, e.message
end

.thumbprint(jwk_hash) ⇒ Object



74
75
76
77
78
79
80
81
# File 'lib/pq_crypto/jwt/jwk.rb', line 74

def thumbprint(jwk_hash)
  jwk = normalize_hash!(jwk_hash)
  algorithm = algorithm_from_jwk!(jwk)
  decode_field!(jwk, "pub", algorithm)

  canonical = JSON.generate("alg" => jwk.fetch("alg"), "kty" => KTY, "pub" => jwk.fetch("pub"))
  base64url(Digest::SHA256.digest(canonical.b))
end

.thumbprint_uri(jwk_hash) ⇒ Object



83
84
85
# File 'lib/pq_crypto/jwt/jwk.rb', line 83

def thumbprint_uri(jwk_hash)
  "#{THUMBPRINT_URI_PREFIX}#{thumbprint(jwk_hash)}"
end