jwt 2.2.1 → 2.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/AUTHORS +79 -44
  3. data/CHANGELOG.md +305 -20
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/CONTRIBUTING.md +99 -0
  6. data/README.md +268 -40
  7. data/lib/jwt/base64.rb +16 -2
  8. data/lib/jwt/claims_validator.rb +13 -9
  9. data/lib/jwt/configuration/container.rb +32 -0
  10. data/lib/jwt/configuration/decode_configuration.rb +46 -0
  11. data/lib/jwt/configuration/jwk_configuration.rb +27 -0
  12. data/lib/jwt/configuration.rb +15 -0
  13. data/lib/jwt/decode.rb +80 -18
  14. data/lib/jwt/deprecations.rb +29 -0
  15. data/lib/jwt/encode.rb +24 -19
  16. data/lib/jwt/error.rb +17 -14
  17. data/lib/jwt/jwa/ecdsa.rb +76 -0
  18. data/lib/jwt/jwa/eddsa.rb +42 -0
  19. data/lib/jwt/jwa/hmac.rb +75 -0
  20. data/lib/jwt/jwa/hmac_rbnacl.rb +50 -0
  21. data/lib/jwt/jwa/hmac_rbnacl_fixed.rb +46 -0
  22. data/lib/jwt/jwa/none.rb +19 -0
  23. data/lib/jwt/jwa/ps.rb +30 -0
  24. data/lib/jwt/jwa/rsa.rb +25 -0
  25. data/lib/jwt/{algos → jwa}/unsupported.rb +8 -5
  26. data/lib/jwt/jwa/wrapper.rb +26 -0
  27. data/lib/jwt/jwa.rb +62 -0
  28. data/lib/jwt/jwk/ec.rb +251 -0
  29. data/lib/jwt/jwk/hmac.rb +103 -0
  30. data/lib/jwt/jwk/key_base.rb +57 -0
  31. data/lib/jwt/jwk/key_finder.rb +19 -30
  32. data/lib/jwt/jwk/kid_as_key_digest.rb +15 -0
  33. data/lib/jwt/jwk/okp_rbnacl.rb +110 -0
  34. data/lib/jwt/jwk/rsa.rb +181 -25
  35. data/lib/jwt/jwk/set.rb +80 -0
  36. data/lib/jwt/jwk/thumbprint.rb +26 -0
  37. data/lib/jwt/jwk.rb +39 -15
  38. data/lib/jwt/verify.rb +25 -6
  39. data/lib/jwt/version.rb +24 -3
  40. data/lib/jwt/x5c_key_finder.rb +52 -0
  41. data/lib/jwt.rb +6 -4
  42. data/ruby-jwt.gemspec +18 -10
  43. metadata +45 -76
  44. data/.codeclimate.yml +0 -20
  45. data/.ebert.yml +0 -18
  46. data/.gitignore +0 -11
  47. data/.rspec +0 -1
  48. data/.rubocop.yml +0 -98
  49. data/.travis.yml +0 -20
  50. data/Appraisals +0 -14
  51. data/Gemfile +0 -3
  52. data/Rakefile +0 -11
  53. data/lib/jwt/algos/ecdsa.rb +0 -35
  54. data/lib/jwt/algos/eddsa.rb +0 -23
  55. data/lib/jwt/algos/hmac.rb +0 -33
  56. data/lib/jwt/algos/ps.rb +0 -43
  57. data/lib/jwt/algos/rsa.rb +0 -19
  58. data/lib/jwt/default_options.rb +0 -15
  59. data/lib/jwt/security_utils.rb +0 -57
  60. data/lib/jwt/signature.rb +0 -52
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module Algos
5
+ module HmacRbNaCl
6
+ MAPPING = { 'HS512256' => ::RbNaCl::HMAC::SHA512256 }.freeze
7
+ SUPPORTED = MAPPING.keys
8
+ class << self
9
+ def sign(algorithm, msg, key)
10
+ Deprecations.warning("The use of the algorithm #{algorithm} is deprecated and will be removed in the next major version of ruby-jwt")
11
+ if (hmac = resolve_algorithm(algorithm))
12
+ hmac.auth(key_for_rbnacl(hmac, key).encode('binary'), msg.encode('binary'))
13
+ else
14
+ Hmac.sign(algorithm, msg, key)
15
+ end
16
+ end
17
+
18
+ def verify(algorithm, key, signing_input, signature)
19
+ Deprecations.warning("The use of the algorithm #{algorithm} is deprecated and will be removed in the next major version of ruby-jwt")
20
+ if (hmac = resolve_algorithm(algorithm))
21
+ hmac.verify(key_for_rbnacl(hmac, key).encode('binary'), signature.encode('binary'), signing_input.encode('binary'))
22
+ else
23
+ Hmac.verify(algorithm, key, signing_input, signature)
24
+ end
25
+ rescue ::RbNaCl::BadAuthenticatorError, ::RbNaCl::LengthError
26
+ false
27
+ end
28
+
29
+ private
30
+
31
+ def key_for_rbnacl(hmac, key)
32
+ key ||= ''
33
+ raise JWT::DecodeError, 'HMAC key expected to be a String' unless key.is_a?(String)
34
+
35
+ return padded_empty_key(hmac.key_bytes) if key == ''
36
+
37
+ key
38
+ end
39
+
40
+ def resolve_algorithm(algorithm)
41
+ MAPPING.fetch(algorithm)
42
+ end
43
+
44
+ def padded_empty_key(length)
45
+ Array.new(length, 0x0).pack('C*').encode('binary')
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module Algos
5
+ module HmacRbNaClFixed
6
+ MAPPING = { 'HS512256' => ::RbNaCl::HMAC::SHA512256 }.freeze
7
+ SUPPORTED = MAPPING.keys
8
+
9
+ class << self
10
+ def sign(algorithm, msg, key)
11
+ key ||= ''
12
+ Deprecations.warning("The use of the algorithm #{algorithm} is deprecated and will be removed in the next major version of ruby-jwt")
13
+ raise JWT::DecodeError, 'HMAC key expected to be a String' unless key.is_a?(String)
14
+
15
+ if (hmac = resolve_algorithm(algorithm)) && key.bytesize <= hmac.key_bytes
16
+ hmac.auth(padded_key_bytes(key, hmac.key_bytes), msg.encode('binary'))
17
+ else
18
+ Hmac.sign(algorithm, msg, key)
19
+ end
20
+ end
21
+
22
+ def verify(algorithm, key, signing_input, signature)
23
+ key ||= ''
24
+ Deprecations.warning("The use of the algorithm #{algorithm} is deprecated and will be removed in the next major version of ruby-jwt")
25
+ raise JWT::DecodeError, 'HMAC key expected to be a String' unless key.is_a?(String)
26
+
27
+ if (hmac = resolve_algorithm(algorithm)) && key.bytesize <= hmac.key_bytes
28
+ hmac.verify(padded_key_bytes(key, hmac.key_bytes), signature.encode('binary'), signing_input.encode('binary'))
29
+ else
30
+ Hmac.verify(algorithm, key, signing_input, signature)
31
+ end
32
+ rescue ::RbNaCl::BadAuthenticatorError, ::RbNaCl::LengthError
33
+ false
34
+ end
35
+
36
+ def resolve_algorithm(algorithm)
37
+ MAPPING.fetch(algorithm)
38
+ end
39
+
40
+ def padded_key_bytes(key, bytesize)
41
+ key.bytes.fill(0, key.bytesize...bytesize).pack('C*')
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWA
5
+ module None
6
+ module_function
7
+
8
+ SUPPORTED = %w[none].freeze
9
+
10
+ def sign(*)
11
+ ''
12
+ end
13
+
14
+ def verify(*)
15
+ true
16
+ end
17
+ end
18
+ end
19
+ end
data/lib/jwt/jwa/ps.rb ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWA
5
+ module Ps
6
+ # RSASSA-PSS signing algorithms
7
+
8
+ module_function
9
+
10
+ SUPPORTED = %w[PS256 PS384 PS512].freeze
11
+
12
+ def sign(algorithm, msg, key)
13
+ unless key.is_a?(::OpenSSL::PKey::RSA)
14
+ raise EncodeError, "The given key is a #{key_class}. It has to be an OpenSSL::PKey::RSA instance."
15
+ end
16
+
17
+ translated_algorithm = algorithm.sub('PS', 'sha')
18
+
19
+ key.sign_pss(translated_algorithm, msg, salt_length: :digest, mgf1_hash: translated_algorithm)
20
+ end
21
+
22
+ def verify(algorithm, public_key, signing_input, signature)
23
+ translated_algorithm = algorithm.sub('PS', 'sha')
24
+ public_key.verify_pss(translated_algorithm, signature, signing_input, salt_length: :auto, mgf1_hash: translated_algorithm)
25
+ rescue OpenSSL::PKey::PKeyError
26
+ raise JWT::VerificationError, 'Signature verification raised'
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWA
5
+ module Rsa
6
+ module_function
7
+
8
+ SUPPORTED = %w[RS256 RS384 RS512].freeze
9
+
10
+ def sign(algorithm, msg, key)
11
+ unless key.is_a?(OpenSSL::PKey::RSA)
12
+ raise EncodeError, "The given key is a #{key.class}. It has to be an OpenSSL::PKey::RSA instance"
13
+ end
14
+
15
+ key.sign(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), msg)
16
+ end
17
+
18
+ def verify(algorithm, public_key, signing_input, signature)
19
+ public_key.verify(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), signature, signing_input)
20
+ rescue OpenSSL::PKey::PKeyError
21
+ raise JWT::VerificationError, 'Signature verification raised'
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,16 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JWT
2
- module Algos
4
+ module JWA
3
5
  module Unsupported
4
6
  module_function
5
7
 
6
- SUPPORTED = Object.new.tap { |object| object.define_singleton_method(:include?) { |*| true } }
7
- def verify(*)
8
- raise JWT::VerificationError, 'Algorithm not supported'
9
- end
8
+ SUPPORTED = [].freeze
10
9
 
11
10
  def sign(*)
12
11
  raise NotImplementedError, 'Unsupported signing method'
13
12
  end
13
+
14
+ def verify(*)
15
+ raise JWT::VerificationError, 'Algorithm not supported'
16
+ end
14
17
  end
15
18
  end
16
19
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWA
5
+ class Wrapper
6
+ attr_reader :alg, :cls
7
+
8
+ def initialize(alg, cls)
9
+ @alg = alg
10
+ @cls = cls
11
+ end
12
+
13
+ def valid_alg?(alg_to_check)
14
+ alg&.casecmp(alg_to_check)&.zero? == true
15
+ end
16
+
17
+ def sign(data:, signing_key:)
18
+ cls.sign(alg, data, signing_key)
19
+ end
20
+
21
+ def verify(data:, signature:, verification_key:)
22
+ cls.verify(alg, verification_key, data, signature)
23
+ end
24
+ end
25
+ end
26
+ end
data/lib/jwt/jwa.rb ADDED
@@ -0,0 +1,62 @@
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/hmac'
12
+ require_relative 'jwa/eddsa'
13
+ require_relative 'jwa/ecdsa'
14
+ require_relative 'jwa/rsa'
15
+ require_relative 'jwa/ps'
16
+ require_relative 'jwa/none'
17
+ require_relative 'jwa/unsupported'
18
+ require_relative 'jwa/wrapper'
19
+
20
+ module JWT
21
+ module JWA
22
+ ALGOS = [Hmac, Ecdsa, Rsa, Eddsa, Ps, None, Unsupported].tap do |l|
23
+ if ::JWT.rbnacl_6_or_greater?
24
+ require_relative 'jwa/hmac_rbnacl'
25
+ l << Algos::HmacRbNaCl
26
+ elsif ::JWT.rbnacl?
27
+ require_relative 'jwa/hmac_rbnacl_fixed'
28
+ l << Algos::HmacRbNaClFixed
29
+ end
30
+ end.freeze
31
+
32
+ class << self
33
+ def find(algorithm)
34
+ indexed[algorithm&.downcase]
35
+ end
36
+
37
+ def create(algorithm)
38
+ return algorithm if JWA.implementation?(algorithm)
39
+
40
+ Wrapper.new(*find(algorithm))
41
+ end
42
+
43
+ def implementation?(algorithm)
44
+ (algorithm.respond_to?(:valid_alg?) && algorithm.respond_to?(:verify)) ||
45
+ (algorithm.respond_to?(:alg) && algorithm.respond_to?(:sign))
46
+ end
47
+
48
+ private
49
+
50
+ def indexed
51
+ @indexed ||= begin
52
+ fallback = [nil, Unsupported]
53
+ ALGOS.each_with_object(Hash.new(fallback)) do |cls, hash|
54
+ cls.const_get(:SUPPORTED).each do |alg|
55
+ hash[alg.downcase] = [alg, cls]
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
data/lib/jwt/jwk/ec.rb ADDED
@@ -0,0 +1,251 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module JWT
6
+ module JWK
7
+ class EC < KeyBase # rubocop:disable Metrics/ClassLength
8
+ KTY = 'EC'
9
+ KTYS = [KTY, OpenSSL::PKey::EC, JWT::JWK::EC].freeze
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)
21
+
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
29
+
30
+ def keypair
31
+ ec_key
32
+ end
33
+
34
+ def private?
35
+ ec_key.private_key?
36
+ end
37
+
38
+ def signing_key
39
+ ec_key
40
+ end
41
+
42
+ def verify_key
43
+ ec_key
44
+ end
45
+
46
+ def public_key
47
+ ec_key
48
+ end
49
+
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
58
+ end
59
+
60
+ def key_digest
61
+ _crv, x_octets, y_octets = keypair_components(ec_key)
62
+ sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(x_octets, BINARY)),
63
+ OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(y_octets, BINARY))])
64
+ OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
65
+ end
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
+
101
+ def keypair_components(ec_keypair)
102
+ encoded_point = ec_keypair.public_key.to_bn.to_s(BINARY)
103
+ case ec_keypair.group.curve_name
104
+ when 'prime256v1'
105
+ crv = 'P-256'
106
+ x_octets, y_octets = encoded_point.unpack('xa32a32')
107
+ when 'secp256k1'
108
+ crv = 'P-256K'
109
+ x_octets, y_octets = encoded_point.unpack('xa32a32')
110
+ when 'secp384r1'
111
+ crv = 'P-384'
112
+ x_octets, y_octets = encoded_point.unpack('xa48a48')
113
+ when 'secp521r1'
114
+ crv = 'P-521'
115
+ x_octets, y_octets = encoded_point.unpack('xa66a66')
116
+ else
117
+ raise JWT::JWKError, "Unsupported curve '#{ec_keypair.group.curve_name}'"
118
+ end
119
+ [crv, x_octets, y_octets]
120
+ end
121
+
122
+ def encode_octets(octets)
123
+ return unless octets
124
+
125
+ ::JWT::Base64.url_encode(octets)
126
+ end
127
+
128
+ def encode_open_ssl_bn(key_part)
129
+ ::JWT::Base64.url_encode(key_part.to_s(BINARY))
130
+ end
131
+
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
143
+
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)
149
+
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
+ )
154
+
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)
178
+ end
179
+ else
180
+ def create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d)
181
+ curve = EC.to_openssl_curve(jwk_crv)
182
+
183
+ x_octets = decode_octets(jwk_x)
184
+ y_octets = decode_octets(jwk_y)
185
+
186
+ key = OpenSSL::PKey::EC.new(curve)
187
+
188
+ # The details of the `Point` instantiation are covered in:
189
+ # - https://docs.ruby-lang.org/en/2.4.0/OpenSSL/PKey/EC.html
190
+ # - https://www.openssl.org/docs/manmaster/man3/EC_POINT_new.html
191
+ # - https://tools.ietf.org/html/rfc5480#section-2.2
192
+ # - https://www.secg.org/SEC1-Ver-1.0.pdf
193
+ # Section 2.3.3 of the last of these references specifies that the
194
+ # encoding of an uncompressed point consists of the byte `0x04` followed
195
+ # by the x value then the y value.
196
+ point = OpenSSL::PKey::EC::Point.new(
197
+ OpenSSL::PKey::EC::Group.new(curve),
198
+ OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2)
199
+ )
200
+
201
+ key.public_key = point
202
+ key.private_key = OpenSSL::BN.new(decode_octets(jwk_d), 2) if jwk_d
203
+
204
+ key
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
230
+
231
+ class << self
232
+ def import(jwk_data)
233
+ new(jwk_data)
234
+ end
235
+
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
247
+ end
248
+ end
249
+ end
250
+ end
251
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWK
5
+ class HMAC < KeyBase
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
11
+
12
+ def initialize(key, params = nil, options = {})
13
+ params ||= {}
14
+
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
28
+ end
29
+
30
+ def private?
31
+ true
32
+ end
33
+
34
+ def public_key
35
+ nil
36
+ end
37
+
38
+ def verify_key
39
+ secret
40
+ end
41
+
42
+ def signing_key
43
+ secret
44
+ end
45
+
46
+ # See https://tools.ietf.org/html/rfc7517#appendix-A.3
47
+ def export(options = {})
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
62
+
63
+ def []=(key, value)
64
+ if HMAC_KEY_ELEMENTS.include?(key.to_sym)
65
+ raise ArgumentError, 'cannot overwrite cryptographic key attributes'
66
+ end
67
+
68
+ super(key, value)
69
+ end
70
+
71
+ private
72
+
73
+ def secret
74
+ self[:k]
75
+ end
76
+
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
89
+
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
95
+
96
+ class << self
97
+ def import(jwk_data)
98
+ new(jwk_data)
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWK
5
+ class KeyBase
6
+ def self.inherited(klass)
7
+ super
8
+ ::JWT::JWK.classes << klass
9
+ end
10
+
11
+ def initialize(options, params = {})
12
+ options ||= {}
13
+
14
+ @parameters = params.transform_keys(&:to_sym) # Uniform interface
15
+
16
+ # For backwards compatibility, kid_generator may be specified in the parameters
17
+ options[:kid_generator] ||= @parameters.delete(:kid_generator)
18
+
19
+ # Make sure the key has a kid
20
+ kid_generator = options[:kid_generator] || ::JWT.configuration.jwk.kid_generator
21
+ self[:kid] ||= kid_generator.new(self).generate
22
+ end
23
+
24
+ def kid
25
+ self[:kid]
26
+ end
27
+
28
+ def hash
29
+ self[:kid].hash
30
+ end
31
+
32
+ def [](key)
33
+ @parameters[key.to_sym]
34
+ end
35
+
36
+ def []=(key, value)
37
+ @parameters[key.to_sym] = value
38
+ end
39
+
40
+ def ==(other)
41
+ other.is_a?(::JWT::JWK::KeyBase) && self[:kid] == other[:kid]
42
+ end
43
+
44
+ alias eql? ==
45
+
46
+ def <=>(other)
47
+ return nil unless other.is_a?(::JWT::JWK::KeyBase)
48
+
49
+ self[:kid] <=> other[:kid]
50
+ end
51
+
52
+ private
53
+
54
+ attr_reader :parameters
55
+ end
56
+ end
57
+ end