pq_crypto-jwt 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,61 +6,50 @@ module JWT
6
6
  module JWK
7
7
  class AKP < KeyBase
8
8
  KTY = "AKP".freeze
9
- KTYS = [
10
- KTY,
11
- PQCrypto::Signature::PublicKey,
12
- JWT::JWK::AKP,
13
- ].freeze
9
+ KTYS = [KTY, PQCrypto::Signature::PublicKey, JWT::JWK::AKP].freeze
14
10
  AKP_KEY_ELEMENTS = %i[kty alg pub priv].freeze
11
+ PRIVATE_EXPORT_OPTIONS = %i[private include_private].freeze
15
12
 
16
13
  class NullKidGenerator
17
14
  def initialize(_jwk); end
18
-
19
15
  def generate = nil
20
16
  end
21
17
 
22
18
  def initialize(key, params = nil, options = {})
23
- params ||= {}
24
- options ||= {}
25
- options = { kid_generator: NullKidGenerator }.merge(options)
26
- params = { kid: params } if params.is_a?(String)
27
- key_params = extract_key_params(key)
19
+ params = params.is_a?(String) ? { kid: params } : (params || {})
28
20
  params = params.transform_keys(&:to_sym)
29
- check_jwk_params!(key_params, params)
30
- super(options, key_params.merge(params))
21
+ key_params = extract_key_params(key)
22
+ @checked_public_key, @checked_secret_key = check_jwk_params!(key_params, params)
23
+ super({ kid_generator: NullKidGenerator }.merge(options || {}), key_params.merge(params))
31
24
  end
32
25
 
33
- def private?
34
- false
35
- end
26
+ def private? = parameters.key?(:priv) && !parameters[:priv].nil?
27
+ def verify_key = public_key
36
28
 
37
29
  def public_key
38
- @public_key ||= PQCrypto::JWT::JWK.public_key_from_jwk(string_export)
30
+ @public_key ||= @checked_public_key || PQCrypto::JWT::JWK.public_key_from_jwk(string_export)
39
31
  end
40
32
 
41
- def signing_key
42
- public_key
43
- end
33
+ def signing_key = private? ? secret_key : public_key
44
34
 
45
- def verify_key
46
- public_key
47
- end
35
+ def secret_key
36
+ raise JWT::JWKError, "AKP JWK does not contain private material" unless private?
48
37
 
49
- def export(_options = {})
50
- parameters.clone.tap { |exported| exported.delete(:priv) }
38
+ @secret_key ||= @checked_secret_key || PQCrypto::JWT::JWK.secret_key_from_jwk(string_export(include_private: true))
51
39
  end
52
40
 
53
- def members
54
- %i[alg kty pub].each_with_object({}) { |key, out| out[key] = self[key] }
41
+ def export(options = {})
42
+ include_private = PRIVATE_EXPORT_OPTIONS.any? { |key| (options || {})[key] }
43
+ parameters.clone.tap { |exported| exported.delete(:priv) unless include_private }
55
44
  end
56
45
 
57
- def key_digest
58
- PQCrypto::JWT::JWK.thumbprint(string_export)
46
+ def members
47
+ keys = private? ? %i[alg kty pub priv] : %i[alg kty pub]
48
+ keys.each_with_object({}) { |key, out| out[key] = self[key] }
59
49
  end
60
50
 
61
- def jwa
62
- PQCrypto::JWT.algorithm_for(self[:alg]) || super
63
- end
51
+ def key_digest = PQCrypto::JWT::JWK.thumbprint(string_export)
52
+ def jwa = PQCrypto::JWT.algorithm_for(self[:alg]) || super
64
53
 
65
54
  def []=(key, value)
66
55
  raise ArgumentError, "cannot overwrite cryptographic key attributes" if AKP_KEY_ELEMENTS.include?(key.to_sym)
@@ -70,23 +59,25 @@ module JWT
70
59
 
71
60
  private
72
61
 
73
- def string_export
74
- export.transform_keys(&:to_s)
62
+ def string_export(include_private: false)
63
+ export(include_private: include_private).transform_keys(&:to_s)
75
64
  end
76
65
 
77
66
  def extract_key_params(key)
78
67
  case key
79
- when JWT::JWK::AKP
80
- key.export
81
- when Hash
82
- key.transform_keys(&:to_sym)
68
+ when JWT::JWK::AKP then key.export(include_private: key.private?)
69
+ when Hash then key.transform_keys(&:to_sym)
83
70
  when PQCrypto::Signature::PublicKey
84
71
  PQCrypto::JWT::JWK.from_public_key(key).transform_keys(&:to_sym)
85
72
  when PQCrypto::Signature::Keypair, PQCrypto::Signature::SecretKey
86
- raise JWT::JWKError, "AKP private JWK export is not supported in the first release"
73
+ raise JWT::JWKError,
74
+ "AKP private JWK export from SecretKey requires public seed-export APIs in pq_crypto; " \
75
+ "use PQCrypto::JWT::JWK.from_seed(seed, alg:, public_key:) instead"
87
76
  else
88
- raise ArgumentError, "key must be a public AKP JWK Hash or PQCrypto::Signature::PublicKey"
77
+ raise ArgumentError, "key must be an AKP JWK Hash or PQCrypto::Signature::PublicKey"
89
78
  end
79
+ rescue PQCrypto::JWT::Error => e
80
+ raise JWT::JWKError, e.message
90
81
  end
91
82
 
92
83
  def check_jwk_params!(key_params, params)
@@ -94,11 +85,16 @@ module JWT
94
85
  raise JWT::JWKError, "Incorrect 'kty' value: #{key_params[:kty]}, expected #{KTY}" unless key_params[:kty] == KTY
95
86
  raise JWT::JWKError, "AKP JWK alg is required" unless key_params[:alg]
96
87
  raise JWT::JWKError, "AKP JWK pub is required" unless key_params[:pub]
97
- raise JWT::JWKError, "AKP private JWK import is not supported in the first release" if key_params[:priv]
98
88
  raise JWT::JWKError, "Unsupported AKP JWK alg: #{key_params[:alg].inspect}" unless PQCrypto::JWT.algorithm_for(key_params[:alg])
89
+
90
+ parsed = key_params.transform_keys(&:to_s)
91
+ [
92
+ PQCrypto::JWT::JWK.public_key_from_jwk(parsed),
93
+ key_params[:priv] ? PQCrypto::JWT::JWK.secret_key_from_jwk(parsed) : nil
94
+ ]
95
+ rescue PQCrypto::JWT::Error => e
96
+ raise JWT::JWKError, e.message
99
97
  end
100
98
  end
101
99
  end
102
100
  end
103
-
104
- JWT::JWK.classes.delete(JWT::JWK::AKP)
@@ -10,39 +10,72 @@ module PQCrypto
10
10
  module_function
11
11
 
12
12
  KTY = "AKP".freeze
13
+ SEED_BYTES = defined?(PQCrypto::PKCS8::ML_DSA_SEED_BYTES) ? PQCrypto::PKCS8::ML_DSA_SEED_BYTES : 32
13
14
 
14
- def from_public_key(public_key, kid: nil)
15
- validate_public_key!(public_key)
16
- base_public_jwk(public_key.algorithm, public_key.to_bytes, kid: kid).freeze
15
+ def from_public_key(public_key, kid: nil, use: nil, key_ops: nil)
16
+ validate_key!(public_key, PQCrypto::Signature::PublicKey)
17
+ public_jwk(public_key.algorithm, public_key.to_bytes, kid: kid, use: use, key_ops: key_ops).freeze
17
18
  end
18
19
 
19
- def from_secret_key(*, **)
20
- raise PQCrypto::JWT::UnsupportedAlgorithm,
21
- "Private AKP JWK export is not supported in the first release; use PEM/PKCS#8 for signing keys"
20
+ def from_seed(seed, alg:, kid: nil, public_key: nil, use: nil, key_ops: nil, verify_public: false)
21
+ algorithm = pq_algorithm_from_jose!(alg)
22
+ seed_bytes = validate_seed!(seed, algorithm)
23
+ derived = derive_public_key(algorithm, seed_bytes) if public_key.nil? || verify_public
24
+ public_key ||= derived
25
+ unless public_key
26
+ raise PQCrypto::JWT::UnsupportedFeature,
27
+ "AKP JWK export from seed requires pq_crypto public_key_from_seed/keypair_from_seed or public_key:"
28
+ end
29
+
30
+ validate_key!(public_key, PQCrypto::Signature::PublicKey)
31
+ unless public_key.algorithm == algorithm
32
+ raise PQCrypto::JWT::KeyTypeError, "public_key algorithm mismatch: expected #{algorithm.inspect}, got #{public_key.algorithm.inspect}"
33
+ end
34
+ check_seed_matches_public!(public_key, derived) if verify_public
35
+
36
+ public_jwk(algorithm, public_key.to_bytes, kid: kid, use: use, key_ops: key_ops)
37
+ .merge!("priv" => base64url(seed_bytes))
38
+ .freeze
39
+ end
40
+
41
+ def from_secret_key(secret_key, kid: nil, public_key: nil, use: nil, key_ops: nil)
42
+ if secret_key.is_a?(PQCrypto::Signature::Keypair)
43
+ public_key ||= secret_key.public_key
44
+ secret_key = secret_key.secret_key
45
+ end
46
+ validate_key!(secret_key, PQCrypto::Signature::SecretKey)
47
+
48
+ from_seed(seed_from_secret_key!(secret_key),
49
+ alg: jose_alg_for!(secret_key.algorithm),
50
+ kid: kid, public_key: public_key, use: use, key_ops: key_ops)
22
51
  end
23
52
 
24
53
  def public_key_from_jwk(hash)
25
54
  jwk = normalize_hash!(hash)
26
- reject_private_material!(jwk)
27
55
  algorithm = algorithm_from_jwk!(jwk)
28
- public_bytes = decode_required_key_bytes!(jwk, "pub", algorithm, :public_key_bytes)
29
- PQCrypto::Signature.public_key_from_bytes(algorithm, public_bytes)
30
- rescue ArgumentError => e
56
+ PQCrypto::Signature.public_key_from_bytes(algorithm, decode_field!(jwk, "pub", algorithm))
57
+ rescue ArgumentError, PQCrypto::Error => e
31
58
  raise PQCrypto::JWT::Error, e.message
32
59
  end
33
60
 
34
- def secret_key_from_jwk(*)
35
- raise PQCrypto::JWT::UnsupportedAlgorithm,
36
- "Private AKP JWK import is not supported in the first release; use PEM/PKCS#8 for signing keys"
61
+ def secret_key_from_jwk(hash, verify_public: false)
62
+ jwk = normalize_hash!(hash)
63
+ algorithm = algorithm_from_jwk!(jwk)
64
+ decode_field!(jwk, "pub", algorithm)
65
+ seed = validate_seed!(base64url_decode(jwk.fetch("priv") { raise PQCrypto::JWT::Error, "JWK priv is required" }), algorithm)
66
+ check_seed_matches_public!(public_key_from_jwk(jwk), derive_public_key(algorithm, seed)) if verify_public
67
+
68
+ PQCrypto::Signature.secret_key_from_seed(algorithm, seed)
69
+ rescue ArgumentError, PQCrypto::Error => e
70
+ raise PQCrypto::JWT::Error, e.message
37
71
  end
38
72
 
39
73
  def thumbprint(jwk_hash)
40
74
  jwk = normalize_hash!(jwk_hash)
41
- reject_private_material!(jwk)
42
75
  algorithm_from_jwk!(jwk)
43
76
  raise PQCrypto::JWT::Error, "JWK pub is required" unless jwk.key?("pub")
44
77
 
45
- canonical = JSON.generate({ "alg" => jwk.fetch("alg"), "kty" => KTY, "pub" => jwk.fetch("pub") })
78
+ canonical = JSON.generate("alg" => jwk.fetch("alg"), "kty" => KTY, "pub" => jwk.fetch("pub"))
46
79
  base64url(Digest::SHA256.digest(canonical.b))
47
80
  end
48
81
 
@@ -51,90 +84,113 @@ module PQCrypto
51
84
  end
52
85
 
53
86
  def base64url_decode(value)
54
- Base64.urlsafe_decode64(String(value))
87
+ raw = String(value)
88
+ Base64.urlsafe_decode64(raw + ("=" * ((4 - raw.bytesize % 4) % 4)))
55
89
  rescue ArgumentError => e
56
90
  raise PQCrypto::JWT::Error, "Invalid base64url value: #{e.message}"
57
91
  end
58
92
 
59
93
  def normalize_hash!(hash)
60
- unless hash.respond_to?(:to_hash)
61
- raise PQCrypto::JWT::Error, "JWK must be a Hash-like object"
62
- end
94
+ raise PQCrypto::JWT::Error, "JWK must be a Hash-like object" unless hash.respond_to?(:to_hash)
63
95
 
64
- hash.to_hash.each_with_object({}) do |(key, value), normalized|
65
- normalized[String(key)] = value
66
- end
96
+ hash.to_hash.each_with_object({}) { |(key, value), out| out[String(key)] = value }
67
97
  end
68
98
 
69
- def reject_private_material!(jwk)
70
- return unless jwk.key?("priv")
99
+ def algorithm_from_jwk!(jwk)
100
+ raise PQCrypto::JWT::Error, "Unsupported JWK kty: #{jwk['kty'].inspect}" unless jwk["kty"] == KTY
71
101
 
72
- raise PQCrypto::JWT::UnsupportedAlgorithm,
73
- "Private AKP JWK material is not supported in the first release"
102
+ pq_algorithm_from_jose!(jwk["alg"])
74
103
  end
75
- private_class_method :reject_private_material!
76
104
 
77
- def algorithm_from_jwk!(jwk)
78
- unless jwk.fetch("kty", nil) == KTY
79
- raise PQCrypto::JWT::Error, "Unsupported JWK kty: #{jwk.fetch('kty', nil).inspect}"
80
- end
81
-
82
- alg = jwk.fetch("alg", nil)
105
+ def pq_algorithm_from_jose!(alg)
83
106
  algorithm = PQCrypto::JWT.algorithm_for(alg)
84
- raise PQCrypto::JWT::UnsupportedAlgorithm, "Unsupported JWK alg: #{alg.inspect}" unless algorithm
85
- raise PQCrypto::JWT::UnsupportedAlgorithm, "Unsupported JWK alg: #{alg.inspect}" unless algorithm.key_kind == :signature
107
+ raise PQCrypto::JWT::UnsupportedAlgorithm, "Unsupported JWK alg: #{alg.inspect}" unless algorithm&.key_kind == :signature
86
108
 
87
109
  algorithm.pq_crypto_algorithm
88
110
  end
111
+ private_class_method :pq_algorithm_from_jose!
89
112
 
90
- def alg_for_algorithm!(algorithm)
91
- match = PQCrypto::JWT.signing_algorithms.find { |candidate| candidate.pq_crypto_algorithm == algorithm }
92
- raise PQCrypto::JWT::UnsupportedAlgorithm, "Unsupported pq_crypto signature algorithm: #{algorithm.inspect}" unless match
113
+ def jose_alg_for!(pq_algorithm)
114
+ match = PQCrypto::JWT.signing_algorithms.find { |candidate| candidate.pq_crypto_algorithm == pq_algorithm }
115
+ raise PQCrypto::JWT::UnsupportedAlgorithm, "Unsupported pq_crypto signature algorithm: #{pq_algorithm.inspect}" unless match
93
116
 
94
117
  match.alg
95
118
  end
119
+ private_class_method :jose_alg_for!
96
120
 
97
- def base_public_jwk(algorithm, public_bytes, kid: nil)
98
- jwk = {
99
- "kty" => KTY,
100
- "alg" => alg_for_algorithm!(algorithm),
101
- "pub" => base64url(public_bytes),
102
- }
121
+ def public_jwk(pq_algorithm, public_bytes, kid:, use:, key_ops:)
122
+ jwk = { "kty" => KTY, "alg" => jose_alg_for!(pq_algorithm), "pub" => base64url(public_bytes) }
103
123
  jwk["kid"] = String(kid) unless kid.nil?
124
+ jwk["use"] = String(use) unless use.nil?
125
+ jwk["key_ops"] = Array(key_ops).map(&:to_s) unless key_ops.nil?
104
126
  jwk
105
127
  end
106
- private_class_method :base_public_jwk
128
+ private_class_method :public_jwk
107
129
 
108
- def decode_required_key_bytes!(jwk, field, algorithm, detail_key)
130
+ def decode_field!(jwk, field, algorithm)
109
131
  raise PQCrypto::JWT::Error, "JWK #{field} is required" unless jwk.key?(field)
110
132
 
111
133
  decoded = base64url_decode(jwk.fetch(field))
112
- details = PQCrypto::Signature.details(algorithm)
113
- expected = details.fetch(detail_key) { details.fetch(detail_key.to_s) }
114
- unless decoded.bytesize == expected
115
- raise PQCrypto::JWT::Error,
116
- "Invalid #{field} length for #{algorithm.inspect}: expected #{expected}, got #{decoded.bytesize}"
117
- end
134
+ expected = PQCrypto::Signature.details(algorithm).fetch(:public_key_bytes)
135
+ raise PQCrypto::JWT::Error, "Invalid #{field} length for #{algorithm.inspect}: expected #{expected}, got #{decoded.bytesize}" unless decoded.bytesize == expected
118
136
 
119
137
  decoded.b
120
138
  end
121
- private_class_method :decode_required_key_bytes!
139
+ private_class_method :decode_field!
140
+
141
+ def validate_seed!(seed, algorithm)
142
+ bytes = String(seed).b
143
+ return bytes if bytes.bytesize == SEED_BYTES
144
+
145
+ raise PQCrypto::JWT::Error, "Invalid priv seed length for #{algorithm.inspect}: expected #{SEED_BYTES}, got #{bytes.bytesize}"
146
+ end
147
+ private_class_method :validate_seed!
148
+
149
+ def validate_key!(key, klass)
150
+ raise PQCrypto::JWT::KeyTypeError, "Expected #{klass}" unless key.is_a?(klass)
151
+ return if PQCrypto::JWT.signing_algorithms.any? { |c| c.pq_crypto_algorithm == key.algorithm }
152
+
153
+ raise PQCrypto::JWT::KeyTypeError, "Unsupported signature algorithm: #{key.algorithm.inspect}"
154
+ end
155
+ private_class_method :validate_key!
156
+
157
+ def derive_public_key(algorithm, seed)
158
+ return PQCrypto::Signature.public_key_from_seed(algorithm, seed) if PQCrypto::Signature.respond_to?(:public_key_from_seed)
159
+ return PQCrypto::Signature.keypair_from_seed(algorithm, seed).public_key if PQCrypto::Signature.respond_to?(:keypair_from_seed)
160
+
161
+ nil
162
+ end
163
+ private_class_method :derive_public_key
122
164
 
123
- def validate_public_key!(public_key)
124
- unless public_key.is_a?(PQCrypto::Signature::PublicKey)
125
- raise PQCrypto::JWT::KeyTypeError, "Expected PQCrypto::Signature::PublicKey"
165
+ def check_seed_matches_public!(public_key, derived)
166
+ unless derived
167
+ raise PQCrypto::JWT::UnsupportedFeature,
168
+ "AKP JWK pub/priv consistency verification requires pq_crypto public_key_from_seed/keypair_from_seed"
126
169
  end
170
+ return if public_key.to_bytes == derived.to_bytes
127
171
 
128
- validate_algorithm!(public_key.algorithm)
172
+ raise PQCrypto::JWT::Error, "AKP JWK public_key does not match priv seed"
129
173
  end
130
- private_class_method :validate_public_key!
174
+ private_class_method :check_seed_matches_public!
175
+
176
+ def seed_from_secret_key!(secret_key)
177
+ if secret_key.respond_to?(:seed?) && !secret_key.seed?
178
+ raise PQCrypto::JWT::UnsupportedFeature,
179
+ "SecretKey was not created from a seed; cannot export RFC 9964 AKP JWK"
180
+ end
181
+ unless secret_key.respond_to?(:to_seed)
182
+ raise PQCrypto::JWT::UnsupportedFeature,
183
+ "SecretKey seed export requires a public pq_crypto #to_seed API; use from_seed(..., public_key:) instead"
184
+ end
131
185
 
132
- def validate_algorithm!(algorithm)
133
- return if PQCrypto::JWT.signing_algorithms.any? { |candidate| candidate.pq_crypto_algorithm == algorithm }
186
+ seed = secret_key.to_seed
187
+ raise PQCrypto::JWT::UnsupportedFeature, "SecretKey #to_seed returned nil" if seed.nil?
134
188
 
135
- raise PQCrypto::JWT::KeyTypeError, "Unsupported signature algorithm: #{algorithm.inspect}"
189
+ validate_seed!(seed, secret_key.algorithm)
190
+ rescue PQCrypto::Error => e
191
+ raise PQCrypto::JWT::UnsupportedFeature, "SecretKey seed export failed: #{e.message}"
136
192
  end
137
- private_class_method :validate_algorithm!
193
+ private_class_method :seed_from_secret_key!
138
194
  end
139
195
  end
140
196
  end
@@ -5,50 +5,51 @@ module PQCrypto
5
5
  module JWKS
6
6
  module_function
7
7
 
8
+ CACHE_EMPTY = Object.new.freeze
9
+
8
10
  def from_keys(public_keys, kids: nil)
9
11
  keys = Array(public_keys).each_with_index.map do |public_key, index|
10
- kid = kids&.fetch(index, nil)
11
- PQCrypto::JWT::JWK.from_public_key(public_key, kid: kid)
12
+ PQCrypto::JWT::JWK.from_public_key(public_key, kid: kids&.fetch(index, nil))
12
13
  end
13
14
  { "keys" => keys }.freeze
14
15
  end
15
16
 
16
- def find(jwks, kid: nil, alg: nil)
17
- keys_from(jwks).find do |key|
18
- (kid.nil? || value_for(key, "kid") == kid) &&
19
- (alg.nil? || value_for(key, "alg") == alg)
20
- end
17
+ def find(jwks, kid: nil, alg: nil, thumbprint: nil)
18
+ keys_from(jwks).find { |key| match?(key, kid, alg, thumbprint) }
19
+ end
20
+
21
+ def find_all(jwks, kid: nil, alg: nil, thumbprint: nil)
22
+ keys_from(jwks).select { |key| match?(key, kid, alg, thumbprint) }
21
23
  end
22
24
 
23
25
  def loader(jwks_hash_or_callable)
24
- cached = nil
26
+ cached = CACHE_EMPTY
25
27
  lambda do |options = {}|
26
- if jwks_hash_or_callable.respond_to?(:call)
27
- cached = nil if options && options[:invalidate]
28
- cached ||= jwks_hash_or_callable.call(options || {})
29
- else
30
- cached = nil if options && options[:invalidate]
31
- cached ||= jwks_hash_or_callable
28
+ options ||= {}
29
+ cached = CACHE_EMPTY if options[:invalidate]
30
+ if cached.equal?(CACHE_EMPTY)
31
+ cached = jwks_hash_or_callable.respond_to?(:call) ? jwks_hash_or_callable.call(options) : jwks_hash_or_callable
32
32
  end
33
+ cached
33
34
  end
34
35
  end
35
36
 
36
37
  def keys_from(jwks)
37
38
  source = jwks.respond_to?(:to_hash) ? jwks.to_hash : jwks
38
- keys = source["keys"] || source[:keys] if source.respond_to?(:[])
39
- Array(keys).map { |key| stringify_hash(key) }
39
+ keys = source.respond_to?(:[]) ? (source["keys"] || source[:keys]) : nil
40
+ Array(keys).map { |key| key.to_hash.each_with_object({}) { |(k, v), out| out[String(k)] = v } }
40
41
  end
41
42
  private_class_method :keys_from
42
43
 
43
- def stringify_hash(hash)
44
- hash.to_hash.each_with_object({}) { |(key, value), out| out[String(key)] = value }
45
- end
46
- private_class_method :stringify_hash
44
+ def value_for(hash, key) = hash[key] || hash[key.to_sym]
45
+ private_class_method :value_for
47
46
 
48
- def value_for(hash, key)
49
- hash[key] || hash[key.to_sym]
47
+ def match?(key, kid, alg, thumbprint)
48
+ (kid.nil? || value_for(key, "kid") == kid) &&
49
+ (alg.nil? || value_for(key, "alg") == alg) &&
50
+ (thumbprint.nil? || PQCrypto::JWT::JWK.thumbprint(key) == thumbprint)
50
51
  end
51
- private_class_method :value_for
52
+ private_class_method :match?
52
53
  end
53
54
  end
54
55
  end
@@ -1,15 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "base64"
4
- require "openssl"
5
- require "set"
6
-
7
3
  module PQCrypto
8
4
  module JWT
9
5
  module Keys
10
6
  module_function
11
7
 
12
8
  EXPECT_VALUES = [:auto, :signature].freeze
9
+ LOAD_ERRORS = [PQCrypto::Error, ArgumentError].freeze
13
10
 
14
11
  def generate(alg)
15
12
  algorithm = PQCrypto::JWT.algorithm_for(alg)
@@ -21,79 +18,68 @@ module PQCrypto
21
18
 
22
19
  def public_from_pem(pem, expect: :auto)
23
20
  validate_expect!(expect)
24
- return PQCrypto::Signature.public_key_from_spki_pem(pem) if expect == :signature
25
21
 
26
- dispatch_public_from_pem(pem)
22
+ key = begin
23
+ expect == :signature ? PQCrypto::Signature.public_key_from_spki_pem(pem) : PQCrypto::Key.from_pem(pem)
24
+ rescue *LOAD_ERRORS => e
25
+ raise PQCrypto::JWT::Error, e.message
26
+ end
27
+ as_public!(key)
27
28
  end
28
29
 
29
- def secret_from_pem(pem, expect: :auto)
30
+ def public_from_der(der, expect: :auto)
30
31
  validate_expect!(expect)
31
- return PQCrypto::Signature.secret_key_from_pkcs8_pem(pem) if expect == :signature
32
-
33
- dispatch_secret_from_pem(pem)
34
- end
35
-
36
- def validate_expect!(expect)
37
- return if EXPECT_VALUES.include?(expect)
38
32
 
39
- raise ArgumentError, "expect: must be one of #{EXPECT_VALUES.map(&:inspect).join(', ')}"
33
+ key = begin
34
+ expect == :signature ? PQCrypto::Signature.public_key_from_spki_der(der) : PQCrypto::Key.from_der(der)
35
+ rescue *LOAD_ERRORS => e
36
+ raise PQCrypto::JWT::Error, e.message
37
+ end
38
+ as_public!(key)
40
39
  end
41
- private_class_method :validate_expect!
42
40
 
43
- def dispatch_public_from_pem(pem)
44
- oid = spki_oid_from_pem(pem)
45
- return PQCrypto::Signature.public_key_from_spki_pem(pem) if signature_oids.include?(oid.to_s)
41
+ def secret_from_pem(pem, expect: :auto, passphrase: nil)
42
+ validate_expect!(expect)
46
43
 
47
- raise PQCrypto::JWT::Error, "Unknown or unsupported PQCrypto SPKI algorithm OID: #{oid.inspect}"
44
+ key = begin
45
+ expect == :signature ? PQCrypto::Signature.secret_key_from_pkcs8_pem(pem, passphrase: passphrase) : PQCrypto::Key.from_pem(pem, passphrase: passphrase)
46
+ rescue *LOAD_ERRORS => e
47
+ raise PQCrypto::JWT::Error, e.message
48
+ end
49
+ as_secret!(key)
48
50
  end
49
- private_class_method :dispatch_public_from_pem
50
51
 
51
- def dispatch_secret_from_pem(pem)
52
- oid = pkcs8_oid_from_pem(pem)
53
- return PQCrypto::Signature.secret_key_from_pkcs8_pem(pem) if signature_oids.include?(oid.to_s)
52
+ def secret_from_der(der, expect: :auto, passphrase: nil)
53
+ validate_expect!(expect)
54
54
 
55
- raise PQCrypto::JWT::Error, "Unknown or unsupported PQCrypto PKCS#8 algorithm OID: #{oid.inspect}"
55
+ key = begin
56
+ expect == :signature ? PQCrypto::Signature.secret_key_from_pkcs8_der(der, passphrase: passphrase) : PQCrypto::Key.from_der(der, passphrase: passphrase)
57
+ rescue *LOAD_ERRORS => e
58
+ raise PQCrypto::JWT::Error, e.message
59
+ end
60
+ as_secret!(key)
56
61
  end
57
- private_class_method :dispatch_secret_from_pem
58
62
 
59
- def spki_oid_from_pem(pem)
60
- sequence = OpenSSL::ASN1.decode(pem_to_der(pem))
61
- sequence.value.fetch(0).value.fetch(0).oid
62
- rescue StandardError => e
63
- raise PQCrypto::JWT::Error, "Unable to read SPKI algorithm OID: #{e.message}"
64
- end
65
- private_class_method :spki_oid_from_pem
63
+ def validate_expect!(expect)
64
+ return if EXPECT_VALUES.include?(expect)
66
65
 
67
- def pkcs8_oid_from_pem(pem)
68
- sequence = OpenSSL::ASN1.decode(pem_to_der(pem))
69
- sequence.value.fetch(1).value.fetch(0).oid
70
- rescue StandardError => e
71
- raise PQCrypto::JWT::Error, "Unable to read PKCS#8 algorithm OID: #{e.message}"
66
+ raise ArgumentError, "expect: must be one of #{EXPECT_VALUES.map(&:inspect).join(', ')}"
72
67
  end
73
- private_class_method :pkcs8_oid_from_pem
68
+ private_class_method :validate_expect!
74
69
 
75
- def pem_to_der(pem)
76
- body = pem.to_s.lines.reject { |line| line.start_with?("-----") }.join
77
- Base64.decode64(body)
78
- end
79
- private_class_method :pem_to_der
70
+ def as_public!(key)
71
+ return key if key.is_a?(PQCrypto::Signature::PublicKey)
80
72
 
81
- def signature_oids
82
- @signature_oids ||= PQCrypto::JWT.signing_algorithms.filter_map do |algorithm|
83
- oid_for_algorithm(algorithm.pq_crypto_algorithm)
84
- end.to_set
73
+ raise PQCrypto::JWT::KeyTypeError, "Expected PQCrypto::Signature::PublicKey, got #{key.class}"
85
74
  end
86
- private_class_method :signature_oids
75
+ private_class_method :as_public!
87
76
 
88
- def oid_for_algorithm(algorithm)
89
- return unless PQCrypto.const_defined?(:AlgorithmRegistry)
77
+ def as_secret!(key)
78
+ return key if key.is_a?(PQCrypto::Signature::SecretKey)
90
79
 
91
- oid = PQCrypto::AlgorithmRegistry.standard_oid(algorithm)
92
- oid&.to_s
93
- rescue StandardError
94
- nil
80
+ raise PQCrypto::JWT::KeyTypeError, "Expected PQCrypto::Signature::SecretKey, got #{key.class}"
95
81
  end
96
- private_class_method :oid_for_algorithm
82
+ private_class_method :as_secret!
97
83
  end
98
84
  end
99
85
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module PQCrypto
4
4
  module JWT
5
- VERSION = "0.1.2"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end