jwt 2.4.1 → 2.9.3

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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +177 -14
  3. data/CONTRIBUTING.md +7 -7
  4. data/README.md +180 -37
  5. data/lib/jwt/base64.rb +33 -0
  6. data/lib/jwt/claims/audience.rb +20 -0
  7. data/lib/jwt/claims/decode_verifier.rb +40 -0
  8. data/lib/jwt/claims/expiration.rb +22 -0
  9. data/lib/jwt/claims/issued_at.rb +15 -0
  10. data/lib/jwt/claims/issuer.rb +24 -0
  11. data/lib/jwt/claims/jwt_id.rb +25 -0
  12. data/lib/jwt/claims/not_before.rb +22 -0
  13. data/lib/jwt/claims/numeric.rb +55 -0
  14. data/lib/jwt/claims/required.rb +23 -0
  15. data/lib/jwt/claims/subject.rb +20 -0
  16. data/lib/jwt/claims/verifier.rb +62 -0
  17. data/lib/jwt/claims.rb +82 -0
  18. data/lib/jwt/claims_validator.rb +3 -24
  19. data/lib/jwt/configuration/container.rb +32 -0
  20. data/lib/jwt/configuration/decode_configuration.rb +46 -0
  21. data/lib/jwt/configuration/jwk_configuration.rb +27 -0
  22. data/lib/jwt/configuration.rb +15 -0
  23. data/lib/jwt/decode.rb +54 -41
  24. data/lib/jwt/deprecations.rb +48 -0
  25. data/lib/jwt/encode.rb +21 -21
  26. data/lib/jwt/error.rb +1 -0
  27. data/lib/jwt/jwa/compat.rb +29 -0
  28. data/lib/jwt/jwa/ecdsa.rb +93 -0
  29. data/lib/jwt/jwa/eddsa.rb +34 -0
  30. data/lib/jwt/jwa/hmac.rb +83 -0
  31. data/lib/jwt/jwa/hmac_rbnacl.rb +49 -0
  32. data/lib/jwt/jwa/hmac_rbnacl_fixed.rb +46 -0
  33. data/lib/jwt/jwa/none.rb +23 -0
  34. data/lib/jwt/jwa/ps.rb +36 -0
  35. data/lib/jwt/jwa/rsa.rb +36 -0
  36. data/lib/jwt/jwa/signing_algorithm.rb +60 -0
  37. data/lib/jwt/jwa/unsupported.rb +19 -0
  38. data/lib/jwt/jwa/wrapper.rb +43 -0
  39. data/lib/jwt/jwa.rb +50 -0
  40. data/lib/jwt/jwk/ec.rb +162 -65
  41. data/lib/jwt/jwk/hmac.rb +69 -24
  42. data/lib/jwt/jwk/key_base.rb +45 -7
  43. data/lib/jwt/jwk/key_finder.rb +19 -35
  44. data/lib/jwt/jwk/kid_as_key_digest.rb +15 -0
  45. data/lib/jwt/jwk/okp_rbnacl.rb +110 -0
  46. data/lib/jwt/jwk/rsa.rb +141 -54
  47. data/lib/jwt/jwk/set.rb +80 -0
  48. data/lib/jwt/jwk/thumbprint.rb +26 -0
  49. data/lib/jwt/jwk.rb +14 -11
  50. data/lib/jwt/verify.rb +10 -89
  51. data/lib/jwt/version.rb +24 -2
  52. data/lib/jwt/x5c_key_finder.rb +3 -6
  53. data/lib/jwt.rb +12 -4
  54. data/ruby-jwt.gemspec +11 -4
  55. metadata +59 -31
  56. data/.codeclimate.yml +0 -8
  57. data/.github/workflows/coverage.yml +0 -27
  58. data/.github/workflows/test.yml +0 -66
  59. data/.gitignore +0 -13
  60. data/.reek.yml +0 -22
  61. data/.rspec +0 -2
  62. data/.rubocop.yml +0 -67
  63. data/.sourcelevel.yml +0 -17
  64. data/Appraisals +0 -13
  65. data/Gemfile +0 -7
  66. data/Rakefile +0 -16
  67. data/lib/jwt/algos/ecdsa.rb +0 -64
  68. data/lib/jwt/algos/eddsa.rb +0 -33
  69. data/lib/jwt/algos/hmac.rb +0 -36
  70. data/lib/jwt/algos/none.rb +0 -17
  71. data/lib/jwt/algos/ps.rb +0 -43
  72. data/lib/jwt/algos/rsa.rb +0 -22
  73. data/lib/jwt/algos/unsupported.rb +0 -19
  74. data/lib/jwt/algos.rb +0 -44
  75. data/lib/jwt/default_options.rb +0 -18
  76. data/lib/jwt/security_utils.rb +0 -59
  77. data/lib/jwt/signature.rb +0 -35
data/lib/jwt/jwa/ps.rb ADDED
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWA
5
+ class Ps
6
+ include JWT::JWA::SigningAlgorithm
7
+
8
+ def initialize(alg)
9
+ @alg = alg
10
+ @digest_algorithm = alg.sub('PS', 'sha')
11
+ end
12
+
13
+ def sign(data:, signing_key:)
14
+ unless signing_key.is_a?(::OpenSSL::PKey::RSA)
15
+ raise_sign_error!("The given key is a #{signing_key.class}. It has to be an OpenSSL::PKey::RSA instance.")
16
+ end
17
+
18
+ signing_key.sign_pss(digest_algorithm, data, salt_length: :digest, mgf1_hash: digest_algorithm)
19
+ end
20
+
21
+ def verify(data:, signature:, verification_key:)
22
+ verification_key.verify_pss(digest_algorithm, signature, data, salt_length: :auto, mgf1_hash: digest_algorithm)
23
+ rescue OpenSSL::PKey::PKeyError
24
+ raise JWT::VerificationError, 'Signature verification raised'
25
+ end
26
+
27
+ register_algorithm(new('PS256'))
28
+ register_algorithm(new('PS384'))
29
+ register_algorithm(new('PS512'))
30
+
31
+ private
32
+
33
+ attr_reader :digest_algorithm
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWA
5
+ class Rsa
6
+ include JWT::JWA::SigningAlgorithm
7
+
8
+ def initialize(alg)
9
+ @alg = alg
10
+ @digest = OpenSSL::Digest.new(alg.sub('RS', 'SHA'))
11
+ end
12
+
13
+ def sign(data:, signing_key:)
14
+ unless signing_key.is_a?(OpenSSL::PKey::RSA)
15
+ raise_sign_error!("The given key is a #{signing_key.class}. It has to be an OpenSSL::PKey::RSA instance")
16
+ end
17
+
18
+ signing_key.sign(digest, data)
19
+ end
20
+
21
+ def verify(data:, signature:, verification_key:)
22
+ verification_key.verify(digest, signature, data)
23
+ rescue OpenSSL::PKey::PKeyError
24
+ raise JWT::VerificationError, 'Signature verification raised'
25
+ end
26
+
27
+ register_algorithm(new('RS256'))
28
+ register_algorithm(new('RS384'))
29
+ register_algorithm(new('RS512'))
30
+
31
+ private
32
+
33
+ attr_reader :digest
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWA
5
+ module SigningAlgorithm
6
+ module ClassMethods
7
+ def register_algorithm(algo)
8
+ ::JWT::JWA.register_algorithm(algo)
9
+ end
10
+ end
11
+
12
+ def self.included(klass)
13
+ klass.extend(ClassMethods)
14
+ klass.include(JWT::JWA::Compat)
15
+ end
16
+
17
+ attr_reader :alg
18
+
19
+ def valid_alg?(alg_to_check)
20
+ alg&.casecmp(alg_to_check)&.zero? == true
21
+ end
22
+
23
+ def header(*)
24
+ { 'alg' => alg }
25
+ end
26
+
27
+ def sign(*)
28
+ raise_sign_error!('Algorithm implementation is missing the sign method')
29
+ end
30
+
31
+ def verify(*)
32
+ raise_verify_error!('Algorithm implementation is missing the verify method')
33
+ end
34
+
35
+ def raise_verify_error!(message)
36
+ raise(DecodeError.new(message).tap { |e| e.set_backtrace(caller(1)) })
37
+ end
38
+
39
+ def raise_sign_error!(message)
40
+ raise(EncodeError.new(message).tap { |e| e.set_backtrace(caller(1)) })
41
+ end
42
+ end
43
+
44
+ class << self
45
+ def register_algorithm(algo)
46
+ algorithms[algo.alg.to_s.downcase] = algo
47
+ end
48
+
49
+ def find(algo)
50
+ algorithms.fetch(algo.to_s.downcase, Unsupported)
51
+ end
52
+
53
+ private
54
+
55
+ def algorithms
56
+ @algorithms ||= {}
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWA
5
+ module Unsupported
6
+ class << self
7
+ include JWT::JWA::SigningAlgorithm
8
+
9
+ def sign(*)
10
+ raise_sign_error!('Unsupported signing method')
11
+ end
12
+
13
+ def verify(*)
14
+ raise JWT::VerificationError, 'Algorithm not supported'
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWA
5
+ class Wrapper
6
+ include SigningAlgorithm
7
+
8
+ def initialize(algorithm)
9
+ @algorithm = algorithm
10
+ end
11
+
12
+ def alg
13
+ return @algorithm.alg if @algorithm.respond_to?(:alg)
14
+
15
+ super
16
+ end
17
+
18
+ def valid_alg?(alg_to_check)
19
+ return @algorithm.valid_alg?(alg_to_check) if @algorithm.respond_to?(:valid_alg?)
20
+
21
+ super
22
+ end
23
+
24
+ def header(*args, **kwargs)
25
+ return @algorithm.header(*args, **kwargs) if @algorithm.respond_to?(:header)
26
+
27
+ super
28
+ end
29
+
30
+ def sign(*args, **kwargs)
31
+ return @algorithm.sign(*args, **kwargs) if @algorithm.respond_to?(:sign)
32
+
33
+ super
34
+ end
35
+
36
+ def verify(*args, **kwargs)
37
+ return @algorithm.verify(*args, **kwargs) if @algorithm.respond_to?(:verify)
38
+
39
+ super
40
+ end
41
+ end
42
+ end
43
+ end
data/lib/jwt/jwa.rb ADDED
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+
5
+ begin
6
+ require 'rbnacl'
7
+ rescue LoadError
8
+ raise if defined?(RbNaCl)
9
+ end
10
+
11
+ require_relative 'jwa/compat'
12
+ require_relative 'jwa/signing_algorithm'
13
+ require_relative 'jwa/ecdsa'
14
+ require_relative 'jwa/hmac'
15
+ require_relative 'jwa/none'
16
+ require_relative 'jwa/ps'
17
+ require_relative 'jwa/rsa'
18
+ require_relative 'jwa/unsupported'
19
+ require_relative 'jwa/wrapper'
20
+
21
+ if JWT.rbnacl?
22
+ require_relative 'jwa/eddsa'
23
+ end
24
+
25
+ if JWT.rbnacl_6_or_greater?
26
+ require_relative 'jwa/hmac_rbnacl'
27
+ elsif JWT.rbnacl?
28
+ require_relative 'jwa/hmac_rbnacl_fixed'
29
+ end
30
+
31
+ module JWT
32
+ module JWA
33
+ class << self
34
+ def resolve(algorithm)
35
+ return find(algorithm) if algorithm.is_a?(String) || algorithm.is_a?(Symbol)
36
+
37
+ unless algorithm.is_a?(SigningAlgorithm)
38
+ Deprecations.warning('Custom algorithms are required to include JWT::JWA::SigningAlgorithm. Custom algorithms that do not include this module may stop working in the next major version of ruby-jwt.')
39
+ return Wrapper.new(algorithm)
40
+ end
41
+
42
+ algorithm
43
+ end
44
+
45
+ def create(algorithm)
46
+ resolve(algorithm)
47
+ end
48
+ end
49
+ end
50
+ end
data/lib/jwt/jwk/ec.rb CHANGED
@@ -4,55 +4,100 @@ require 'forwardable'
4
4
 
5
5
  module JWT
6
6
  module JWK
7
- class EC < KeyBase
8
- extend Forwardable
9
- def_delegators :@keypair, :public_key
10
-
7
+ class EC < KeyBase # rubocop:disable Metrics/ClassLength
11
8
  KTY = 'EC'
12
- KTYS = [KTY, OpenSSL::PKey::EC].freeze
9
+ KTYS = [KTY, OpenSSL::PKey::EC, JWT::JWK::EC].freeze
13
10
  BINARY = 2
11
+ EC_PUBLIC_KEY_ELEMENTS = %i[kty crv x y].freeze
12
+ EC_PRIVATE_KEY_ELEMENTS = %i[d].freeze
13
+ EC_KEY_ELEMENTS = (EC_PRIVATE_KEY_ELEMENTS + EC_PUBLIC_KEY_ELEMENTS).freeze
14
+ ZERO_BYTE = "\0".b.freeze
15
+
16
+ def initialize(key, params = nil, options = {})
17
+ params ||= {}
18
+
19
+ # For backwards compatibility when kid was a String
20
+ params = { kid: params } if params.is_a?(String)
14
21
 
15
- def initialize(keypair, kid = nil)
16
- raise ArgumentError, 'keypair must be of type OpenSSL::PKey::EC' unless keypair.is_a?(OpenSSL::PKey::EC)
22
+ key_params = extract_key_params(key)
23
+
24
+ params = params.transform_keys(&:to_sym)
25
+ check_jwk_params!(key_params, params)
26
+
27
+ super(options, key_params.merge(params))
28
+ end
17
29
 
18
- kid ||= generate_kid(keypair)
19
- super(keypair, kid)
30
+ def keypair
31
+ ec_key
20
32
  end
21
33
 
22
34
  def private?
23
- @keypair.private_key?
35
+ ec_key.private_key?
24
36
  end
25
37
 
26
- def export(options = {})
27
- crv, x_octets, y_octets = keypair_components(keypair)
28
- exported_hash = {
29
- kty: KTY,
30
- crv: crv,
31
- x: encode_octets(x_octets),
32
- y: encode_octets(y_octets),
33
- kid: kid
34
- }
35
- return exported_hash unless private? && options[:include_private] == true
38
+ def signing_key
39
+ ec_key
40
+ end
36
41
 
37
- append_private_parts(exported_hash)
42
+ def verify_key
43
+ ec_key
38
44
  end
39
45
 
40
- private
46
+ def public_key
47
+ ec_key
48
+ end
41
49
 
42
- def append_private_parts(the_hash)
43
- octets = keypair.private_key.to_bn.to_s(BINARY)
44
- the_hash.merge(
45
- d: encode_octets(octets)
46
- )
50
+ def members
51
+ EC_PUBLIC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] }
52
+ end
53
+
54
+ def export(options = {})
55
+ exported = parameters.clone
56
+ exported.reject! { |k, _| EC_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true
57
+ exported
47
58
  end
48
59
 
49
- def generate_kid(ec_keypair)
50
- _crv, x_octets, y_octets = keypair_components(ec_keypair)
60
+ def key_digest
61
+ _crv, x_octets, y_octets = keypair_components(ec_key)
51
62
  sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(x_octets, BINARY)),
52
63
  OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(y_octets, BINARY))])
53
64
  OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
54
65
  end
55
66
 
67
+ def []=(key, value)
68
+ if EC_KEY_ELEMENTS.include?(key.to_sym)
69
+ raise ArgumentError, 'cannot overwrite cryptographic key attributes'
70
+ end
71
+
72
+ super(key, value)
73
+ end
74
+
75
+ private
76
+
77
+ def ec_key
78
+ @ec_key ||= create_ec_key(self[:crv], self[:x], self[:y], self[:d])
79
+ end
80
+
81
+ def extract_key_params(key)
82
+ case key
83
+ when JWT::JWK::EC
84
+ key.export(include_private: true)
85
+ when OpenSSL::PKey::EC # Accept OpenSSL key as input
86
+ @ec_key = key # Preserve the object to avoid recreation
87
+ parse_ec_key(key)
88
+ when Hash
89
+ key.transform_keys(&:to_sym)
90
+ else
91
+ raise ArgumentError, 'key must be of type OpenSSL::PKey::EC or Hash with key parameters'
92
+ end
93
+ end
94
+
95
+ def check_jwk_params!(key_params, params)
96
+ raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (EC_KEY_ELEMENTS & params.keys).empty?
97
+ raise JWT::JWKError, "Incorrect 'kty' value: #{key_params[:kty]}, expected #{KTY}" unless key_params[:kty] == KTY
98
+ raise JWT::JWKError, 'Key format is invalid for EC' unless key_params[:crv] && key_params[:x] && key_params[:y]
99
+ end
100
+
56
101
  def keypair_components(ec_keypair)
57
102
  encoded_point = ec_keypair.public_key.to_bn.to_s(BINARY)
58
103
  case ec_keypair.group.curve_name
@@ -75,47 +120,65 @@ module JWT
75
120
  end
76
121
 
77
122
  def encode_octets(octets)
78
- Base64.urlsafe_encode64(octets, padding: false)
123
+ return unless octets
124
+
125
+ ::JWT::Base64.url_encode(octets)
79
126
  end
80
127
 
81
128
  def encode_open_ssl_bn(key_part)
82
- Base64.urlsafe_encode64(key_part.to_s(BINARY), padding: false)
129
+ ::JWT::Base64.url_encode(key_part.to_s(BINARY))
83
130
  end
84
131
 
85
- class << self
86
- def import(jwk_data)
87
- # See https://tools.ietf.org/html/rfc7518#section-6.2.1 for an
88
- # explanation of the relevant parameters.
89
-
90
- jwk_crv, jwk_x, jwk_y, jwk_d, jwk_kid = jwk_attrs(jwk_data, %i[crv x y d kid])
91
- raise JWT::JWKError, 'Key format is invalid for EC' unless jwk_crv && jwk_x && jwk_y
92
-
93
- new(ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d), jwk_kid)
94
- end
132
+ def parse_ec_key(key)
133
+ crv, x_octets, y_octets = keypair_components(key)
134
+ octets = key.private_key&.to_bn&.to_s(BINARY)
135
+ {
136
+ kty: KTY,
137
+ crv: crv,
138
+ x: encode_octets(x_octets),
139
+ y: encode_octets(y_octets),
140
+ d: encode_octets(octets)
141
+ }.compact
142
+ end
95
143
 
96
- def to_openssl_curve(crv)
97
- # The JWK specs and OpenSSL use different names for the same curves.
98
- # See https://tools.ietf.org/html/rfc5480#section-2.1.1.1 for some
99
- # pointers on different names for common curves.
100
- case crv
101
- when 'P-256' then 'prime256v1'
102
- when 'P-384' then 'secp384r1'
103
- when 'P-521' then 'secp521r1'
104
- when 'P-256K' then 'secp256k1'
105
- else raise JWT::JWKError, 'Invalid curve provided'
106
- end
107
- end
144
+ if ::JWT.openssl_3?
145
+ def create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d) # rubocop:disable Metrics/MethodLength
146
+ curve = EC.to_openssl_curve(jwk_crv)
147
+ x_octets = decode_octets(jwk_x)
148
+ y_octets = decode_octets(jwk_y)
108
149
 
109
- private
150
+ point = OpenSSL::PKey::EC::Point.new(
151
+ OpenSSL::PKey::EC::Group.new(curve),
152
+ OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2)
153
+ )
110
154
 
111
- def jwk_attrs(jwk_data, attrs)
112
- attrs.map do |attr|
113
- jwk_data[attr] || jwk_data[attr.to_s]
114
- end
155
+ sequence = if jwk_d
156
+ # https://datatracker.ietf.org/doc/html/rfc5915.html
157
+ # ECPrivateKey ::= SEQUENCE {
158
+ # version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1),
159
+ # privateKey OCTET STRING,
160
+ # parameters [0] ECParameters {{ NamedCurve }} OPTIONAL,
161
+ # publicKey [1] BIT STRING OPTIONAL
162
+ # }
163
+
164
+ OpenSSL::ASN1::Sequence([
165
+ OpenSSL::ASN1::Integer(1),
166
+ OpenSSL::ASN1::OctetString(OpenSSL::BN.new(decode_octets(jwk_d), 2).to_s(2)),
167
+ OpenSSL::ASN1::ObjectId(curve, 0, :EXPLICIT),
168
+ OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed), 1, :EXPLICIT)
169
+ ])
170
+ else
171
+ OpenSSL::ASN1::Sequence([
172
+ OpenSSL::ASN1::Sequence([OpenSSL::ASN1::ObjectId('id-ecPublicKey'), OpenSSL::ASN1::ObjectId(curve)]),
173
+ OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed))
174
+ ])
175
+ end
176
+
177
+ OpenSSL::PKey::EC.new(sequence.to_der)
115
178
  end
116
-
117
- def ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d)
118
- curve = to_openssl_curve(jwk_crv)
179
+ else
180
+ def create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d)
181
+ curve = EC.to_openssl_curve(jwk_crv)
119
182
 
120
183
  x_octets = decode_octets(jwk_x)
121
184
  y_octets = decode_octets(jwk_y)
@@ -140,13 +203,47 @@ module JWT
140
203
 
141
204
  key
142
205
  end
206
+ end
207
+
208
+ def decode_octets(base64_encoded_coordinate)
209
+ bytes = ::JWT::Base64.url_decode(base64_encoded_coordinate)
210
+ # Some base64 encoders on some platform omit a single 0-byte at
211
+ # the start of either Y or X coordinate of the elliptic curve point.
212
+ # This leads to an encoding error when data is passed to OpenSSL BN.
213
+ # It is know to have happend to exported JWKs on a Java application and
214
+ # on a Flutter/Dart application (both iOS and Android). All that is
215
+ # needed to fix the problem is adding a leading 0-byte. We know the
216
+ # required byte is 0 because with any other byte the point is no longer
217
+ # on the curve - and OpenSSL will actually communicate this via another
218
+ # exception. The indication of a stripped byte will be the fact that the
219
+ # coordinates - once decoded into bytes - should always be an even
220
+ # bytesize. For example, with a P-521 curve, both x and y must be 66 bytes.
221
+ # With a P-256 curve, both x and y must be 32 and so on. The simplest way
222
+ # to check for this truncation is thus to check whether the number of bytes
223
+ # is odd, and restore the leading 0-byte if it is.
224
+ if bytes.bytesize.odd?
225
+ ZERO_BYTE + bytes
226
+ else
227
+ bytes
228
+ end
229
+ end
143
230
 
144
- def decode_octets(jwk_data)
145
- Base64.urlsafe_decode64(jwk_data)
231
+ class << self
232
+ def import(jwk_data)
233
+ new(jwk_data)
146
234
  end
147
235
 
148
- def decode_open_ssl_bn(jwk_data)
149
- OpenSSL::BN.new(Base64.urlsafe_decode64(jwk_data), BINARY)
236
+ def to_openssl_curve(crv)
237
+ # The JWK specs and OpenSSL use different names for the same curves.
238
+ # See https://tools.ietf.org/html/rfc5480#section-2.1.1.1 for some
239
+ # pointers on different names for common curves.
240
+ case crv
241
+ when 'P-256' then 'prime256v1'
242
+ when 'P-384' then 'secp384r1'
243
+ when 'P-521' then 'secp521r1'
244
+ when 'P-256K' then 'secp256k1'
245
+ else raise JWT::JWKError, 'Invalid curve provided'
246
+ end
150
247
  end
151
248
  end
152
249
  end
data/lib/jwt/jwk/hmac.rb CHANGED
@@ -3,14 +3,28 @@
3
3
  module JWT
4
4
  module JWK
5
5
  class HMAC < KeyBase
6
- KTY = 'oct'
7
- KTYS = [KTY, String].freeze
6
+ KTY = 'oct'
7
+ KTYS = [KTY, String, JWT::JWK::HMAC].freeze
8
+ HMAC_PUBLIC_KEY_ELEMENTS = %i[kty].freeze
9
+ HMAC_PRIVATE_KEY_ELEMENTS = %i[k].freeze
10
+ HMAC_KEY_ELEMENTS = (HMAC_PRIVATE_KEY_ELEMENTS + HMAC_PUBLIC_KEY_ELEMENTS).freeze
8
11
 
9
- def initialize(keypair, kid = nil)
10
- raise ArgumentError, 'keypair must be of type String' unless keypair.is_a?(String)
12
+ def initialize(key, params = nil, options = {})
13
+ params ||= {}
11
14
 
12
- super
13
- @kid = kid || generate_kid
15
+ # For backwards compatibility when kid was a String
16
+ params = { kid: params } if params.is_a?(String)
17
+
18
+ key_params = extract_key_params(key)
19
+
20
+ params = params.transform_keys(&:to_sym)
21
+ check_jwk(key_params, params)
22
+
23
+ super(options, key_params.merge(params))
24
+ end
25
+
26
+ def keypair
27
+ secret
14
28
  end
15
29
 
16
30
  def private?
@@ -21,36 +35,67 @@ module JWT
21
35
  nil
22
36
  end
23
37
 
38
+ def verify_key
39
+ secret
40
+ end
41
+
42
+ def signing_key
43
+ secret
44
+ end
45
+
24
46
  # See https://tools.ietf.org/html/rfc7517#appendix-A.3
25
47
  def export(options = {})
26
- exported_hash = {
27
- kty: KTY,
28
- kid: kid
29
- }
48
+ exported = parameters.clone
49
+ exported.reject! { |k, _| HMAC_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true
50
+ exported
51
+ end
52
+
53
+ def members
54
+ HMAC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] }
55
+ end
56
+
57
+ def key_digest
58
+ sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::UTF8String.new(signing_key),
59
+ OpenSSL::ASN1::UTF8String.new(KTY)])
60
+ OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
61
+ end
30
62
 
31
- return exported_hash unless private? && options[:include_private] == true
63
+ def []=(key, value)
64
+ if HMAC_KEY_ELEMENTS.include?(key.to_sym)
65
+ raise ArgumentError, 'cannot overwrite cryptographic key attributes'
66
+ end
32
67
 
33
- exported_hash.merge(
34
- k: keypair
35
- )
68
+ super(key, value)
36
69
  end
37
70
 
38
71
  private
39
72
 
40
- def generate_kid
41
- sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::UTF8String.new(keypair),
42
- OpenSSL::ASN1::UTF8String.new(KTY)])
43
- OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
73
+ def secret
74
+ self[:k]
44
75
  end
45
76
 
46
- class << self
47
- def import(jwk_data)
48
- jwk_k = jwk_data[:k] || jwk_data['k']
49
- jwk_kid = jwk_data[:kid] || jwk_data['kid']
77
+ def extract_key_params(key)
78
+ case key
79
+ when JWT::JWK::HMAC
80
+ key.export(include_private: true)
81
+ when String # Accept String key as input
82
+ { kty: KTY, k: key }
83
+ when Hash
84
+ key.transform_keys(&:to_sym)
85
+ else
86
+ raise ArgumentError, 'key must be of type String or Hash with key parameters'
87
+ end
88
+ end
50
89
 
51
- raise JWT::JWKError, 'Key format is invalid for HMAC' unless jwk_k
90
+ def check_jwk(keypair, params)
91
+ raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (HMAC_KEY_ELEMENTS & params.keys).empty?
92
+ raise JWT::JWKError, "Incorrect 'kty' value: #{keypair[:kty]}, expected #{KTY}" unless keypair[:kty] == KTY
93
+ raise JWT::JWKError, 'Key format is invalid for HMAC' unless keypair[:k]
94
+ end
52
95
 
53
- new(jwk_k, jwk_kid)
96
+ class << self
97
+ def import(jwk_data)
98
+ new(jwk_data)
54
99
  end
55
100
  end
56
101
  end