jwt 2.2.1 → 2.6.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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/AUTHORS +79 -44
  3. data/CHANGELOG.md +248 -20
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/CONTRIBUTING.md +99 -0
  6. data/README.md +250 -35
  7. data/lib/jwt/algos/algo_wrapper.rb +30 -0
  8. data/lib/jwt/algos/ecdsa.rb +39 -12
  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 +6 -8
  15. data/lib/jwt/algos/rsa.rb +7 -5
  16. data/lib/jwt/algos/unsupported.rb +7 -4
  17. data/lib/jwt/algos.rb +67 -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 +84 -16
  24. data/lib/jwt/encode.rb +30 -19
  25. data/lib/jwt/error.rb +16 -14
  26. data/lib/jwt/jwk/ec.rb +223 -0
  27. data/lib/jwt/jwk/hmac.rb +93 -0
  28. data/lib/jwt/jwk/key_base.rb +55 -0
  29. data/lib/jwt/jwk/key_finder.rb +14 -29
  30. data/lib/jwt/jwk/kid_as_key_digest.rb +15 -0
  31. data/lib/jwt/jwk/rsa.rb +169 -25
  32. data/lib/jwt/jwk/set.rb +80 -0
  33. data/lib/jwt/jwk/thumbprint.rb +26 -0
  34. data/lib/jwt/jwk.rb +38 -15
  35. data/lib/jwt/security_utils.rb +2 -27
  36. data/lib/jwt/verify.rb +18 -3
  37. data/lib/jwt/version.rb +24 -4
  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 +29 -89
  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/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
41
+ return unless @key || @verify
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
53
+ raise(JWT::IncorrectAlgorithm, 'An algorithm must be specified') if allowed_algorithms.empty?
54
+ raise(JWT::IncorrectAlgorithm, 'Token is missing alg header') unless alg_in_header
55
+ raise(JWT::IncorrectAlgorithm, 'Expected a different algorithm') unless valid_alg_in_header?
56
+ end
57
+
58
+ def set_key
36
59
  @key = find_key(&@keyfinder) if @keyfinder
37
60
  @key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks]).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
38
65
 
39
- 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?
66
+ def verify_signature_for?(key)
67
+ allowed_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 valid_alg_in_header?
73
+ allowed_algorithms.any? { |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,223 @@
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
+ extend Forwardable
9
+ def_delegators :keypair, :public_key
10
+
11
+ KTY = 'EC'
12
+ KTYS = [KTY, OpenSSL::PKey::EC, JWT::JWK::EC].freeze
13
+ BINARY = 2
14
+ EC_PUBLIC_KEY_ELEMENTS = %i[kty crv x y].freeze
15
+ EC_PRIVATE_KEY_ELEMENTS = %i[d].freeze
16
+ EC_KEY_ELEMENTS = (EC_PRIVATE_KEY_ELEMENTS + EC_PUBLIC_KEY_ELEMENTS).freeze
17
+
18
+ def initialize(key, params = nil, options = {})
19
+ params ||= {}
20
+
21
+ # For backwards compatibility when kid was a String
22
+ params = { kid: params } if params.is_a?(String)
23
+
24
+ key_params = extract_key_params(key)
25
+
26
+ params = params.transform_keys(&:to_sym)
27
+ check_jwk(key_params, params)
28
+
29
+ super(options, key_params.merge(params))
30
+ end
31
+
32
+ def keypair
33
+ @keypair ||= create_ec_key(self[:crv], self[:x], self[:y], self[:d])
34
+ end
35
+
36
+ def private?
37
+ keypair.private_key?
38
+ end
39
+
40
+ def members
41
+ EC_PUBLIC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] }
42
+ end
43
+
44
+ def export(options = {})
45
+ exported = parameters.clone
46
+ exported.reject! { |k, _| EC_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true
47
+ exported
48
+ end
49
+
50
+ def key_digest
51
+ _crv, x_octets, y_octets = keypair_components(keypair)
52
+ sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(x_octets, BINARY)),
53
+ OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(y_octets, BINARY))])
54
+ OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
55
+ end
56
+
57
+ def []=(key, value)
58
+ if EC_KEY_ELEMENTS.include?(key.to_sym)
59
+ raise ArgumentError, 'cannot overwrite cryptographic key attributes'
60
+ end
61
+
62
+ super(key, value)
63
+ end
64
+
65
+ private
66
+
67
+ def extract_key_params(key)
68
+ case key
69
+ when JWT::JWK::EC
70
+ key.export(include_private: true)
71
+ when OpenSSL::PKey::EC # Accept OpenSSL key as input
72
+ @keypair = key # Preserve the object to avoid recreation
73
+ parse_ec_key(key)
74
+ when Hash
75
+ key.transform_keys(&:to_sym)
76
+ else
77
+ raise ArgumentError, 'key must be of type OpenSSL::PKey::EC or Hash with key parameters'
78
+ end
79
+ end
80
+
81
+ def check_jwk(keypair, params)
82
+ raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (EC_KEY_ELEMENTS & params.keys).empty?
83
+ raise JWT::JWKError, "Incorrect 'kty' value: #{keypair[:kty]}, expected #{KTY}" unless keypair[:kty] == KTY
84
+ raise JWT::JWKError, 'Key format is invalid for EC' unless keypair[:crv] && keypair[:x] && keypair[:y]
85
+ end
86
+
87
+ def keypair_components(ec_keypair)
88
+ encoded_point = ec_keypair.public_key.to_bn.to_s(BINARY)
89
+ case ec_keypair.group.curve_name
90
+ when 'prime256v1'
91
+ crv = 'P-256'
92
+ x_octets, y_octets = encoded_point.unpack('xa32a32')
93
+ when 'secp256k1'
94
+ crv = 'P-256K'
95
+ x_octets, y_octets = encoded_point.unpack('xa32a32')
96
+ when 'secp384r1'
97
+ crv = 'P-384'
98
+ x_octets, y_octets = encoded_point.unpack('xa48a48')
99
+ when 'secp521r1'
100
+ crv = 'P-521'
101
+ x_octets, y_octets = encoded_point.unpack('xa66a66')
102
+ else
103
+ raise JWT::JWKError, "Unsupported curve '#{ec_keypair.group.curve_name}'"
104
+ end
105
+ [crv, x_octets, y_octets]
106
+ end
107
+
108
+ def encode_octets(octets)
109
+ return unless octets
110
+
111
+ ::JWT::Base64.url_encode(octets)
112
+ end
113
+
114
+ def encode_open_ssl_bn(key_part)
115
+ ::JWT::Base64.url_encode(key_part.to_s(BINARY))
116
+ end
117
+
118
+ def parse_ec_key(key)
119
+ crv, x_octets, y_octets = keypair_components(key)
120
+ octets = key.private_key&.to_bn&.to_s(BINARY)
121
+ {
122
+ kty: KTY,
123
+ crv: crv,
124
+ x: encode_octets(x_octets),
125
+ y: encode_octets(y_octets),
126
+ d: encode_octets(octets)
127
+ }.compact
128
+ end
129
+
130
+ if ::JWT.openssl_3?
131
+ def create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d) # rubocop:disable Metrics/MethodLength
132
+ curve = EC.to_openssl_curve(jwk_crv)
133
+
134
+ x_octets = decode_octets(jwk_x)
135
+ y_octets = decode_octets(jwk_y)
136
+
137
+ point = OpenSSL::PKey::EC::Point.new(
138
+ OpenSSL::PKey::EC::Group.new(curve),
139
+ OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2)
140
+ )
141
+
142
+ sequence = if jwk_d
143
+ # https://datatracker.ietf.org/doc/html/rfc5915.html
144
+ # ECPrivateKey ::= SEQUENCE {
145
+ # version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1),
146
+ # privateKey OCTET STRING,
147
+ # parameters [0] ECParameters {{ NamedCurve }} OPTIONAL,
148
+ # publicKey [1] BIT STRING OPTIONAL
149
+ # }
150
+
151
+ OpenSSL::ASN1::Sequence([
152
+ OpenSSL::ASN1::Integer(1),
153
+ OpenSSL::ASN1::OctetString(OpenSSL::BN.new(decode_octets(jwk_d), 2).to_s(2)),
154
+ OpenSSL::ASN1::ObjectId(curve, 0, :EXPLICIT),
155
+ OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed), 1, :EXPLICIT)
156
+ ])
157
+ else
158
+ OpenSSL::ASN1::Sequence([
159
+ OpenSSL::ASN1::Sequence([OpenSSL::ASN1::ObjectId('id-ecPublicKey'), OpenSSL::ASN1::ObjectId(curve)]),
160
+ OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed))
161
+ ])
162
+ end
163
+
164
+ OpenSSL::PKey::EC.new(sequence.to_der)
165
+ end
166
+ else
167
+ def create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d)
168
+ curve = EC.to_openssl_curve(jwk_crv)
169
+
170
+ x_octets = decode_octets(jwk_x)
171
+ y_octets = decode_octets(jwk_y)
172
+
173
+ key = OpenSSL::PKey::EC.new(curve)
174
+
175
+ # The details of the `Point` instantiation are covered in:
176
+ # - https://docs.ruby-lang.org/en/2.4.0/OpenSSL/PKey/EC.html
177
+ # - https://www.openssl.org/docs/manmaster/man3/EC_POINT_new.html
178
+ # - https://tools.ietf.org/html/rfc5480#section-2.2
179
+ # - https://www.secg.org/SEC1-Ver-1.0.pdf
180
+ # Section 2.3.3 of the last of these references specifies that the
181
+ # encoding of an uncompressed point consists of the byte `0x04` followed
182
+ # by the x value then the y value.
183
+ point = OpenSSL::PKey::EC::Point.new(
184
+ OpenSSL::PKey::EC::Group.new(curve),
185
+ OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2)
186
+ )
187
+
188
+ key.public_key = point
189
+ key.private_key = OpenSSL::BN.new(decode_octets(jwk_d), 2) if jwk_d
190
+
191
+ key
192
+ end
193
+ end
194
+
195
+ def decode_octets(jwk_data)
196
+ ::JWT::Base64.url_decode(jwk_data)
197
+ end
198
+
199
+ def decode_open_ssl_bn(jwk_data)
200
+ OpenSSL::BN.new(::JWT::Base64.url_decode(jwk_data), BINARY)
201
+ end
202
+
203
+ class << self
204
+ def import(jwk_data)
205
+ new(jwk_data)
206
+ end
207
+
208
+ def to_openssl_curve(crv)
209
+ # The JWK specs and OpenSSL use different names for the same curves.
210
+ # See https://tools.ietf.org/html/rfc5480#section-2.1.1.1 for some
211
+ # pointers on different names for common curves.
212
+ case crv
213
+ when 'P-256' then 'prime256v1'
214
+ when 'P-384' then 'secp384r1'
215
+ when 'P-521' then 'secp521r1'
216
+ when 'P-256K' then 'secp256k1'
217
+ else raise JWT::JWKError, 'Invalid curve provided'
218
+ end
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,93 @@
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
+ self[:k]
28
+ end
29
+
30
+ def private?
31
+ true
32
+ end
33
+
34
+ def public_key
35
+ nil
36
+ end
37
+
38
+ # See https://tools.ietf.org/html/rfc7517#appendix-A.3
39
+ def export(options = {})
40
+ exported = parameters.clone
41
+ exported.reject! { |k, _| HMAC_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true
42
+ exported
43
+ end
44
+
45
+ def members
46
+ HMAC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] }
47
+ end
48
+
49
+ alias signing_key keypair # for backwards compatibility
50
+
51
+ def key_digest
52
+ sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::UTF8String.new(signing_key),
53
+ OpenSSL::ASN1::UTF8String.new(KTY)])
54
+ OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
55
+ end
56
+
57
+ def []=(key, value)
58
+ if HMAC_KEY_ELEMENTS.include?(key.to_sym)
59
+ raise ArgumentError, 'cannot overwrite cryptographic key attributes'
60
+ end
61
+
62
+ super(key, value)
63
+ end
64
+
65
+ private
66
+
67
+ def extract_key_params(key)
68
+ case key
69
+ when JWT::JWK::HMAC
70
+ key.export(include_private: true)
71
+ when String # Accept String key as input
72
+ { kty: KTY, k: key }
73
+ when Hash
74
+ key.transform_keys(&:to_sym)
75
+ else
76
+ raise ArgumentError, 'key must be of type String or Hash with key parameters'
77
+ end
78
+ end
79
+
80
+ def check_jwk(keypair, params)
81
+ raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (HMAC_KEY_ELEMENTS & params.keys).empty?
82
+ raise JWT::JWKError, "Incorrect 'kty' value: #{keypair[:kty]}, expected #{KTY}" unless keypair[:kty] == KTY
83
+ raise JWT::JWKError, 'Key format is invalid for HMAC' unless keypair[:k]
84
+ end
85
+
86
+ class << self
87
+ def import(jwk_data)
88
+ new(jwk_data)
89
+ end
90
+ end
91
+ end
92
+ end
93
+ 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