jwt 2.4.1 → 2.9.3

Sign up to get free protection for your applications and to get access to all the features.
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