jwt 2.2.1 → 2.7.1

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/AUTHORS +79 -44
  3. data/CHANGELOG.md +271 -20
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/CONTRIBUTING.md +99 -0
  6. data/README.md +253 -35
  7. data/lib/jwt/algos/algo_wrapper.rb +26 -0
  8. data/lib/jwt/algos/ecdsa.rb +55 -14
  9. data/lib/jwt/algos/eddsa.rb +18 -8
  10. data/lib/jwt/algos/hmac.rb +57 -17
  11. data/lib/jwt/algos/hmac_rbnacl.rb +53 -0
  12. data/lib/jwt/algos/hmac_rbnacl_fixed.rb +52 -0
  13. data/lib/jwt/algos/none.rb +19 -0
  14. data/lib/jwt/algos/ps.rb +10 -12
  15. data/lib/jwt/algos/rsa.rb +9 -5
  16. data/lib/jwt/algos/unsupported.rb +7 -4
  17. data/lib/jwt/algos.rb +66 -0
  18. data/lib/jwt/claims_validator.rb +12 -8
  19. data/lib/jwt/configuration/container.rb +21 -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 +85 -17
  24. data/lib/jwt/encode.rb +30 -19
  25. data/lib/jwt/error.rb +16 -14
  26. data/lib/jwt/jwk/ec.rb +236 -0
  27. data/lib/jwt/jwk/hmac.rb +103 -0
  28. data/lib/jwt/jwk/key_base.rb +55 -0
  29. data/lib/jwt/jwk/key_finder.rb +19 -30
  30. data/lib/jwt/jwk/kid_as_key_digest.rb +15 -0
  31. data/lib/jwt/jwk/okp_rbnacl.rb +110 -0
  32. data/lib/jwt/jwk/rsa.rb +181 -25
  33. data/lib/jwt/jwk/set.rb +80 -0
  34. data/lib/jwt/jwk/thumbprint.rb +26 -0
  35. data/lib/jwt/jwk.rb +39 -15
  36. data/lib/jwt/verify.rb +18 -3
  37. data/lib/jwt/version.rb +23 -3
  38. data/lib/jwt/x5c_key_finder.rb +55 -0
  39. data/lib/jwt.rb +5 -4
  40. data/ruby-jwt.gemspec +15 -10
  41. metadata +30 -90
  42. data/.codeclimate.yml +0 -20
  43. data/.ebert.yml +0 -18
  44. data/.gitignore +0 -11
  45. data/.rspec +0 -1
  46. data/.rubocop.yml +0 -98
  47. data/.travis.yml +0 -20
  48. data/Appraisals +0 -14
  49. data/Gemfile +0 -3
  50. data/Rakefile +0 -11
  51. data/lib/jwt/default_options.rb +0 -15
  52. data/lib/jwt/security_utils.rb +0 -57
  53. data/lib/jwt/signature.rb +0 -52
data/lib/jwt/decode.rb CHANGED
@@ -2,14 +2,16 @@
2
2
 
3
3
  require 'json'
4
4
 
5
- require 'jwt/signature'
6
5
  require 'jwt/verify'
6
+ require 'jwt/x5c_key_finder'
7
+
7
8
  # JWT::Decode module
8
9
  module JWT
9
10
  # Decoding logic for JWT
10
11
  class Decode
11
12
  def initialize(jwt, key, verify, options, &keyfinder)
12
13
  raise(JWT::DecodeError, 'Nil JSON web token') unless jwt
14
+
13
15
  @jwt = jwt
14
16
  @key = key
15
17
  @options = options
@@ -22,51 +24,109 @@ module JWT
22
24
  def decode_segments
23
25
  validate_segment_count!
24
26
  if @verify
25
- decode_crypto
27
+ decode_signature
28
+ verify_algo
29
+ set_key
26
30
  verify_signature
27
31
  verify_claims
28
32
  end
29
33
  raise(JWT::DecodeError, 'Not enough or too many segments') unless header && payload
34
+
30
35
  [payload, header]
31
36
  end
32
37
 
33
38
  private
34
39
 
35
40
  def verify_signature
36
- @key = find_key(&@keyfinder) if @keyfinder
37
- @key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks]).key_for(header['kid']) if @options[:jwks]
41
+ return unless @key || @verify
38
42
 
43
+ return if none_algorithm?
44
+
45
+ raise JWT::DecodeError, 'No verification key available' unless @key
46
+
47
+ return if Array(@key).any? { |key| verify_signature_for?(key) }
48
+
49
+ raise(JWT::VerificationError, 'Signature verification failed')
50
+ end
51
+
52
+ def verify_algo
39
53
  raise(JWT::IncorrectAlgorithm, 'An algorithm must be specified') if allowed_algorithms.empty?
40
- raise(JWT::IncorrectAlgorithm, 'Expected a different algorithm') unless options_includes_algo_in_header?
54
+ raise(JWT::IncorrectAlgorithm, 'Token is missing alg header') unless alg_in_header
55
+ raise(JWT::IncorrectAlgorithm, 'Expected a different algorithm') if allowed_and_valid_algorithms.empty?
56
+ end
57
+
58
+ def set_key
59
+ @key = find_key(&@keyfinder) if @keyfinder
60
+ @key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks], allow_nil_kid: @options[:allow_nil_kid]).key_for(header['kid']) if @options[:jwks]
61
+ if (x5c_options = @options[:x5c])
62
+ @key = X5cKeyFinder.new(x5c_options[:root_certificates], x5c_options[:crls]).from(header['x5c'])
63
+ end
64
+ end
65
+
66
+ def verify_signature_for?(key)
67
+ allowed_and_valid_algorithms.any? do |alg|
68
+ alg.verify(data: signing_input, signature: @signature, verification_key: key)
69
+ end
70
+ end
41
71
 
42
- Signature.verify(header['alg'], @key, signing_input, @signature)
72
+ def allowed_and_valid_algorithms
73
+ @allowed_and_valid_algorithms ||= allowed_algorithms.select { |alg| alg.valid_alg?(alg_in_header) }
43
74
  end
44
75
 
45
- def options_includes_algo_in_header?
46
- allowed_algorithms.include? header['alg']
76
+ # Order is very important - first check for string keys, next for symbols
77
+ ALGORITHM_KEYS = ['algorithm',
78
+ :algorithm,
79
+ 'algorithms',
80
+ :algorithms].freeze
81
+
82
+ def given_algorithms
83
+ ALGORITHM_KEYS.each do |alg_key|
84
+ alg = @options[alg_key]
85
+ return Array(alg) if alg
86
+ end
87
+ []
47
88
  end
48
89
 
49
90
  def allowed_algorithms
50
- if @options.key?(:algorithm)
51
- [@options[:algorithm]]
52
- else
53
- @options[:algorithms] || []
91
+ @allowed_algorithms ||= resolve_allowed_algorithms
92
+ end
93
+
94
+ def resolve_allowed_algorithms
95
+ algs = given_algorithms.map do |alg|
96
+ if Algos.implementation?(alg)
97
+ alg
98
+ else
99
+ Algos.create(alg)
100
+ end
54
101
  end
102
+
103
+ sort_by_alg_header(algs)
104
+ end
105
+
106
+ # Move algorithms matching the JWT alg header to the beginning of the list
107
+ def sort_by_alg_header(algs)
108
+ return algs if algs.size <= 1
109
+
110
+ algs.partition { |alg| alg.valid_alg?(alg_in_header) }.flatten
55
111
  end
56
112
 
57
113
  def find_key(&keyfinder)
58
114
  key = (keyfinder.arity == 2 ? yield(header, payload) : yield(header))
59
- raise JWT::DecodeError, 'No verification key available' unless key
60
- key
115
+ # key can be of type [string, nil, OpenSSL::PKey, Array]
116
+ return key if key && !Array(key).empty?
117
+
118
+ raise JWT::DecodeError, 'No verification key available'
61
119
  end
62
120
 
63
121
  def verify_claims
64
122
  Verify.verify_claims(payload, @options)
123
+ Verify.verify_required_claims(payload, @options)
65
124
  end
66
125
 
67
126
  def validate_segment_count!
68
127
  return if segment_length == 3
69
128
  return if !@verify && segment_length == 2 # If no verifying required, the signature is not needed
129
+ return if segment_length == 2 && none_algorithm?
70
130
 
71
131
  raise(JWT::DecodeError, 'Not enough or too many segments')
72
132
  end
@@ -75,8 +135,16 @@ module JWT
75
135
  @segments.count
76
136
  end
77
137
 
78
- def decode_crypto
79
- @signature = JWT::Base64.url_decode(@segments[2])
138
+ def none_algorithm?
139
+ alg_in_header == 'none'
140
+ end
141
+
142
+ def decode_signature
143
+ @signature = ::JWT::Base64.url_decode(@segments[2] || '')
144
+ end
145
+
146
+ def alg_in_header
147
+ header['alg']
80
148
  end
81
149
 
82
150
  def header
@@ -92,7 +160,7 @@ module JWT
92
160
  end
93
161
 
94
162
  def parse_and_decode(segment)
95
- JWT::JSON.parse(JWT::Base64.url_decode(segment))
163
+ JWT::JSON.parse(::JWT::Base64.url_decode(segment))
96
164
  rescue ::JSON::ParserError
97
165
  raise JWT::DecodeError, 'Invalid segment encoding'
98
166
  end
data/lib/jwt/encode.rb CHANGED
@@ -1,27 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative './claims_validator'
3
+ require_relative 'algos'
4
+ require_relative 'claims_validator'
4
5
 
5
6
  # JWT::Encode module
6
7
  module JWT
7
8
  # Encoding logic for JWT
8
9
  class Encode
9
- ALG_NONE = 'none'.freeze
10
- ALG_KEY = 'alg'.freeze
10
+ ALG_KEY = 'alg'
11
11
 
12
12
  def initialize(options)
13
- @payload = options[:payload]
14
- @key = options[:key]
15
- @algorithm = options[:algorithm]
16
- @headers = options[:headers].each_with_object({}) { |(key, value), headers| headers[key.to_s] = value }
13
+ @payload = options[:payload]
14
+ @key = options[:key]
15
+ @algorithm = resolve_algorithm(options[:algorithm])
16
+ @headers = options[:headers].transform_keys(&:to_s)
17
+ @headers[ALG_KEY] = @algorithm.alg
17
18
  end
18
19
 
19
20
  def segments
20
- @segments ||= combine(encoded_header_and_payload, encoded_signature)
21
+ validate_claims!
22
+ combine(encoded_header_and_payload, encoded_signature)
21
23
  end
22
24
 
23
25
  private
24
26
 
27
+ def resolve_algorithm(algorithm)
28
+ return algorithm if Algos.implementation?(algorithm)
29
+
30
+ Algos.create(algorithm)
31
+ end
32
+
25
33
  def encoded_header
26
34
  @encoded_header ||= encode_header
27
35
  end
@@ -39,26 +47,29 @@ module JWT
39
47
  end
40
48
 
41
49
  def encode_header
42
- @headers[ALG_KEY] = @algorithm
43
- encode(@headers)
50
+ encode_data(@headers)
44
51
  end
45
52
 
46
53
  def encode_payload
47
- if @payload && @payload.is_a?(Hash)
48
- ClaimsValidator.new(@payload).validate!
49
- end
54
+ encode_data(@payload)
55
+ end
50
56
 
51
- encode(@payload)
57
+ def signature
58
+ @algorithm.sign(data: encoded_header_and_payload, signing_key: @key)
52
59
  end
53
60
 
54
- def encode_signature
55
- return '' if @algorithm == ALG_NONE
61
+ def validate_claims!
62
+ return unless @payload.is_a?(Hash)
63
+
64
+ ClaimsValidator.new(@payload).validate!
65
+ end
56
66
 
57
- JWT::Base64.url_encode(JWT::Signature.sign(@algorithm, encoded_header_and_payload, @key))
67
+ def encode_signature
68
+ ::JWT::Base64.url_encode(signature)
58
69
  end
59
70
 
60
- def encode(data)
61
- JWT::Base64.url_encode(JWT::JSON.generate(data))
71
+ def encode_data(data)
72
+ ::JWT::Base64.url_encode(JWT::JSON.generate(data))
62
73
  end
63
74
 
64
75
  def combine(*parts)
data/lib/jwt/error.rb CHANGED
@@ -1,20 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JWT
4
- EncodeError = Class.new(StandardError)
5
- DecodeError = Class.new(StandardError)
6
- RequiredDependencyError = Class.new(StandardError)
4
+ class EncodeError < StandardError; end
5
+ class DecodeError < StandardError; end
6
+ class RequiredDependencyError < StandardError; end
7
7
 
8
- VerificationError = Class.new(DecodeError)
9
- ExpiredSignature = Class.new(DecodeError)
10
- IncorrectAlgorithm = Class.new(DecodeError)
11
- ImmatureSignature = Class.new(DecodeError)
12
- InvalidIssuerError = Class.new(DecodeError)
13
- InvalidIatError = Class.new(DecodeError)
14
- InvalidAudError = Class.new(DecodeError)
15
- InvalidSubError = Class.new(DecodeError)
16
- InvalidJtiError = Class.new(DecodeError)
17
- InvalidPayload = Class.new(DecodeError)
8
+ class VerificationError < DecodeError; end
9
+ class ExpiredSignature < DecodeError; end
10
+ class IncorrectAlgorithm < DecodeError; end
11
+ class ImmatureSignature < DecodeError; end
12
+ class InvalidIssuerError < DecodeError; end
13
+ class UnsupportedEcdsaCurve < IncorrectAlgorithm; end
14
+ class InvalidIatError < DecodeError; end
15
+ class InvalidAudError < DecodeError; end
16
+ class InvalidSubError < DecodeError; end
17
+ class InvalidJtiError < DecodeError; end
18
+ class InvalidPayload < DecodeError; end
19
+ class MissingRequiredClaim < DecodeError; end
18
20
 
19
- JWKError = Class.new(DecodeError)
21
+ class JWKError < DecodeError; end
20
22
  end
data/lib/jwt/jwk/ec.rb ADDED
@@ -0,0 +1,236 @@
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
+
15
+ def initialize(key, params = nil, options = {})
16
+ params ||= {}
17
+
18
+ # For backwards compatibility when kid was a String
19
+ params = { kid: params } if params.is_a?(String)
20
+
21
+ key_params = extract_key_params(key)
22
+
23
+ params = params.transform_keys(&:to_sym)
24
+ check_jwk_params!(key_params, params)
25
+
26
+ super(options, key_params.merge(params))
27
+ end
28
+
29
+ def keypair
30
+ ec_key
31
+ end
32
+
33
+ def private?
34
+ ec_key.private_key?
35
+ end
36
+
37
+ def signing_key
38
+ ec_key
39
+ end
40
+
41
+ def verify_key
42
+ ec_key
43
+ end
44
+
45
+ def public_key
46
+ ec_key
47
+ end
48
+
49
+ def members
50
+ EC_PUBLIC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] }
51
+ end
52
+
53
+ def export(options = {})
54
+ exported = parameters.clone
55
+ exported.reject! { |k, _| EC_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true
56
+ exported
57
+ end
58
+
59
+ def key_digest
60
+ _crv, x_octets, y_octets = keypair_components(ec_key)
61
+ sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(x_octets, BINARY)),
62
+ OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(y_octets, BINARY))])
63
+ OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
64
+ end
65
+
66
+ def []=(key, value)
67
+ if EC_KEY_ELEMENTS.include?(key.to_sym)
68
+ raise ArgumentError, 'cannot overwrite cryptographic key attributes'
69
+ end
70
+
71
+ super(key, value)
72
+ end
73
+
74
+ private
75
+
76
+ def ec_key
77
+ @ec_key ||= create_ec_key(self[:crv], self[:x], self[:y], self[:d])
78
+ end
79
+
80
+ def extract_key_params(key)
81
+ case key
82
+ when JWT::JWK::EC
83
+ key.export(include_private: true)
84
+ when OpenSSL::PKey::EC # Accept OpenSSL key as input
85
+ @ec_key = key # Preserve the object to avoid recreation
86
+ parse_ec_key(key)
87
+ when Hash
88
+ key.transform_keys(&:to_sym)
89
+ else
90
+ raise ArgumentError, 'key must be of type OpenSSL::PKey::EC or Hash with key parameters'
91
+ end
92
+ end
93
+
94
+ def check_jwk_params!(key_params, params)
95
+ raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (EC_KEY_ELEMENTS & params.keys).empty?
96
+ raise JWT::JWKError, "Incorrect 'kty' value: #{key_params[:kty]}, expected #{KTY}" unless key_params[:kty] == KTY
97
+ raise JWT::JWKError, 'Key format is invalid for EC' unless key_params[:crv] && key_params[:x] && key_params[:y]
98
+ end
99
+
100
+ def keypair_components(ec_keypair)
101
+ encoded_point = ec_keypair.public_key.to_bn.to_s(BINARY)
102
+ case ec_keypair.group.curve_name
103
+ when 'prime256v1'
104
+ crv = 'P-256'
105
+ x_octets, y_octets = encoded_point.unpack('xa32a32')
106
+ when 'secp256k1'
107
+ crv = 'P-256K'
108
+ x_octets, y_octets = encoded_point.unpack('xa32a32')
109
+ when 'secp384r1'
110
+ crv = 'P-384'
111
+ x_octets, y_octets = encoded_point.unpack('xa48a48')
112
+ when 'secp521r1'
113
+ crv = 'P-521'
114
+ x_octets, y_octets = encoded_point.unpack('xa66a66')
115
+ else
116
+ raise JWT::JWKError, "Unsupported curve '#{ec_keypair.group.curve_name}'"
117
+ end
118
+ [crv, x_octets, y_octets]
119
+ end
120
+
121
+ def encode_octets(octets)
122
+ return unless octets
123
+
124
+ ::JWT::Base64.url_encode(octets)
125
+ end
126
+
127
+ def encode_open_ssl_bn(key_part)
128
+ ::JWT::Base64.url_encode(key_part.to_s(BINARY))
129
+ end
130
+
131
+ def parse_ec_key(key)
132
+ crv, x_octets, y_octets = keypair_components(key)
133
+ octets = key.private_key&.to_bn&.to_s(BINARY)
134
+ {
135
+ kty: KTY,
136
+ crv: crv,
137
+ x: encode_octets(x_octets),
138
+ y: encode_octets(y_octets),
139
+ d: encode_octets(octets)
140
+ }.compact
141
+ end
142
+
143
+ if ::JWT.openssl_3?
144
+ def create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d) # rubocop:disable Metrics/MethodLength
145
+ curve = EC.to_openssl_curve(jwk_crv)
146
+
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(jwk_data)
209
+ ::JWT::Base64.url_decode(jwk_data)
210
+ end
211
+
212
+ def decode_open_ssl_bn(jwk_data)
213
+ OpenSSL::BN.new(::JWT::Base64.url_decode(jwk_data), BINARY)
214
+ end
215
+
216
+ class << self
217
+ def import(jwk_data)
218
+ new(jwk_data)
219
+ end
220
+
221
+ def to_openssl_curve(crv)
222
+ # The JWK specs and OpenSSL use different names for the same curves.
223
+ # See https://tools.ietf.org/html/rfc5480#section-2.1.1.1 for some
224
+ # pointers on different names for common curves.
225
+ case crv
226
+ when 'P-256' then 'prime256v1'
227
+ when 'P-384' then 'secp384r1'
228
+ when 'P-521' then 'secp521r1'
229
+ when 'P-256K' then 'secp256k1'
230
+ else raise JWT::JWKError, 'Invalid curve provided'
231
+ end
232
+ end
233
+ end
234
+ end
235
+ end
236
+ 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,55 @@
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
+ self[:kid] == other[:kid]
42
+ end
43
+
44
+ alias eql? ==
45
+
46
+ def <=>(other)
47
+ self[:kid] <=> other[:kid]
48
+ end
49
+
50
+ private
51
+
52
+ attr_reader :parameters
53
+ end
54
+ end
55
+ end