jwt 2.4.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -2
  3. data/CONTRIBUTING.md +7 -7
  4. data/README.md +135 -31
  5. data/lib/jwt/algos/algo_wrapper.rb +30 -0
  6. data/lib/jwt/algos/ecdsa.rb +2 -4
  7. data/lib/jwt/algos/eddsa.rb +4 -4
  8. data/lib/jwt/algos/hmac.rb +54 -17
  9. data/lib/jwt/algos/hmac_rbnacl.rb +53 -0
  10. data/lib/jwt/algos/hmac_rbnacl_fixed.rb +52 -0
  11. data/lib/jwt/algos/none.rb +3 -1
  12. data/lib/jwt/algos/ps.rb +3 -5
  13. data/lib/jwt/algos/rsa.rb +3 -4
  14. data/lib/jwt/algos.rb +38 -15
  15. data/lib/jwt/base64.rb +19 -0
  16. data/lib/jwt/configuration/container.rb +21 -0
  17. data/lib/jwt/configuration/decode_configuration.rb +46 -0
  18. data/lib/jwt/configuration/jwk_configuration.rb +27 -0
  19. data/lib/jwt/configuration.rb +15 -0
  20. data/lib/jwt/decode.rb +48 -27
  21. data/lib/jwt/encode.rb +30 -20
  22. data/lib/jwt/jwk/ec.rb +131 -62
  23. data/lib/jwt/jwk/hmac.rb +59 -24
  24. data/lib/jwt/jwk/key_base.rb +43 -7
  25. data/lib/jwt/jwk/key_finder.rb +14 -34
  26. data/lib/jwt/jwk/kid_as_key_digest.rb +15 -0
  27. data/lib/jwt/jwk/rsa.rb +128 -53
  28. data/lib/jwt/jwk/set.rb +80 -0
  29. data/lib/jwt/jwk/thumbprint.rb +26 -0
  30. data/lib/jwt/jwk.rb +13 -11
  31. data/lib/jwt/security_utils.rb +0 -27
  32. data/lib/jwt/version.rb +23 -2
  33. data/lib/jwt/x5c_key_finder.rb +1 -1
  34. data/lib/jwt.rb +5 -4
  35. data/ruby-jwt.gemspec +8 -4
  36. metadata +15 -30
  37. data/.codeclimate.yml +0 -8
  38. data/.github/workflows/coverage.yml +0 -27
  39. data/.github/workflows/test.yml +0 -66
  40. data/.gitignore +0 -13
  41. data/.reek.yml +0 -22
  42. data/.rspec +0 -2
  43. data/.rubocop.yml +0 -67
  44. data/.sourcelevel.yml +0 -17
  45. data/Appraisals +0 -13
  46. data/Gemfile +0 -7
  47. data/Rakefile +0 -16
  48. data/lib/jwt/default_options.rb +0 -18
  49. data/lib/jwt/signature.rb +0 -35
data/lib/jwt/jwk/ec.rb CHANGED
@@ -4,53 +4,84 @@ require 'forwardable'
4
4
 
5
5
  module JWT
6
6
  module JWK
7
- class EC < KeyBase
7
+ class EC < KeyBase # rubocop:disable Metrics/ClassLength
8
8
  extend Forwardable
9
- def_delegators :@keypair, :public_key
9
+ def_delegators :keypair, :public_key
10
10
 
11
11
  KTY = 'EC'
12
- KTYS = [KTY, OpenSSL::PKey::EC].freeze
12
+ KTYS = [KTY, OpenSSL::PKey::EC, JWT::JWK::EC].freeze
13
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
14
17
 
15
- def initialize(keypair, kid = nil)
16
- raise ArgumentError, 'keypair must be of type OpenSSL::PKey::EC' unless keypair.is_a?(OpenSSL::PKey::EC)
18
+ def initialize(key, params = nil, options = {})
19
+ params ||= {}
17
20
 
18
- kid ||= generate_kid(keypair)
19
- super(keypair, kid)
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])
20
34
  end
21
35
 
22
36
  def private?
23
- @keypair.private_key?
37
+ keypair.private_key?
38
+ end
39
+
40
+ def members
41
+ EC_PUBLIC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] }
24
42
  end
25
43
 
26
44
  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
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
36
61
 
37
- append_private_parts(exported_hash)
62
+ super(key, value)
38
63
  end
39
64
 
40
65
  private
41
66
 
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
- )
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
47
79
  end
48
80
 
49
- def generate_kid(ec_keypair)
50
- _crv, x_octets, y_octets = keypair_components(ec_keypair)
51
- sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(x_octets, BINARY)),
52
- OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(y_octets, BINARY))])
53
- OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
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]
54
85
  end
55
86
 
56
87
  def keypair_components(ec_keypair)
@@ -75,47 +106,66 @@ module JWT
75
106
  end
76
107
 
77
108
  def encode_octets(octets)
78
- Base64.urlsafe_encode64(octets, padding: false)
109
+ return unless octets
110
+
111
+ ::JWT::Base64.url_encode(octets)
79
112
  end
80
113
 
81
114
  def encode_open_ssl_bn(key_part)
82
- Base64.urlsafe_encode64(key_part.to_s(BINARY), padding: false)
115
+ ::JWT::Base64.url_encode(key_part.to_s(BINARY))
83
116
  end
84
117
 
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
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
92
129
 
93
- new(ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d), jwk_kid)
94
- end
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)
95
133
 
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
134
+ x_octets = decode_octets(jwk_x)
135
+ y_octets = decode_octets(jwk_y)
108
136
 
109
- private
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
+ )
110
141
 
111
- def jwk_attrs(jwk_data, attrs)
112
- attrs.map do |attr|
113
- jwk_data[attr] || jwk_data[attr.to_s]
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
+ ])
114
162
  end
115
- end
116
163
 
117
- def ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d)
118
- curve = to_openssl_curve(jwk_crv)
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)
119
169
 
120
170
  x_octets = decode_octets(jwk_x)
121
171
  y_octets = decode_octets(jwk_y)
@@ -140,13 +190,32 @@ module JWT
140
190
 
141
191
  key
142
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
143
202
 
144
- def decode_octets(jwk_data)
145
- Base64.urlsafe_decode64(jwk_data)
203
+ class << self
204
+ def import(jwk_data)
205
+ new(jwk_data)
146
206
  end
147
207
 
148
- def decode_open_ssl_bn(jwk_data)
149
- OpenSSL::BN.new(Base64.urlsafe_decode64(jwk_data), BINARY)
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
150
219
  end
151
220
  end
152
221
  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
+ self[:k]
14
28
  end
15
29
 
16
30
  def private?
@@ -23,34 +37,55 @@ module JWT
23
37
 
24
38
  # See https://tools.ietf.org/html/rfc7517#appendix-A.3
25
39
  def export(options = {})
26
- exported_hash = {
27
- kty: KTY,
28
- kid: kid
29
- }
30
-
31
- return exported_hash unless private? && options[:include_private] == true
40
+ exported = parameters.clone
41
+ exported.reject! { |k, _| HMAC_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true
42
+ exported
43
+ end
32
44
 
33
- exported_hash.merge(
34
- k: keypair
35
- )
45
+ def members
46
+ HMAC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] }
36
47
  end
37
48
 
38
- private
49
+ alias signing_key keypair # for backwards compatibility
39
50
 
40
- def generate_kid
41
- sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::UTF8String.new(keypair),
51
+ def key_digest
52
+ sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::UTF8String.new(signing_key),
42
53
  OpenSSL::ASN1::UTF8String.new(KTY)])
43
54
  OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
44
55
  end
45
56
 
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']
57
+ def []=(key, value)
58
+ if HMAC_KEY_ELEMENTS.include?(key.to_sym)
59
+ raise ArgumentError, 'cannot overwrite cryptographic key attributes'
60
+ end
50
61
 
51
- raise JWT::JWKError, 'Key format is invalid for HMAC' unless jwk_k
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
52
85
 
53
- new(jwk_k, jwk_kid)
86
+ class << self
87
+ def import(jwk_data)
88
+ new(jwk_data)
54
89
  end
55
90
  end
56
91
  end
@@ -3,17 +3,53 @@
3
3
  module JWT
4
4
  module JWK
5
5
  class KeyBase
6
- attr_reader :keypair, :kid
7
-
8
- def initialize(keypair, kid = nil)
9
- @keypair = keypair
10
- @kid = kid
11
- end
12
-
13
6
  def self.inherited(klass)
14
7
  super
15
8
  ::JWT::JWK.classes << klass
16
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
17
53
  end
18
54
  end
19
55
  end
@@ -5,8 +5,12 @@ module JWT
5
5
  class KeyFinder
6
6
  def initialize(options)
7
7
  jwks_or_loader = options[:jwks]
8
- @jwks = jwks_or_loader if jwks_or_loader.is_a?(Hash)
9
- @jwk_loader = jwks_or_loader if jwks_or_loader.respond_to?(:call)
8
+
9
+ @jwks_loader = if jwks_or_loader.respond_to?(:call)
10
+ jwks_or_loader
11
+ else
12
+ ->(_options) { jwks_or_loader }
13
+ end
10
14
  end
11
15
 
12
16
  def key_for(kid)
@@ -14,48 +18,24 @@ module JWT
14
18
 
15
19
  jwk = resolve_key(kid)
16
20
 
17
- raise ::JWT::DecodeError, 'No keys found in jwks' if jwks_keys.empty?
21
+ raise ::JWT::DecodeError, 'No keys found in jwks' unless @jwks.any?
18
22
  raise ::JWT::DecodeError, "Could not find public key for kid #{kid}" unless jwk
19
23
 
20
- ::JWT::JWK.import(jwk).keypair
24
+ jwk.keypair
21
25
  end
22
26
 
23
27
  private
24
28
 
25
29
  def resolve_key(kid)
26
- jwk = find_key(kid)
30
+ # First try without invalidation to facilitate application caching
31
+ @jwks ||= JWT::JWK::Set.new(@jwks_loader.call(kid: kid))
32
+ jwk = @jwks.find { |key| key[:kid] == kid }
27
33
 
28
34
  return jwk if jwk
29
35
 
30
- if reloadable?
31
- load_keys(invalidate: true)
32
- return find_key(kid)
33
- end
34
-
35
- nil
36
- end
37
-
38
- def jwks
39
- return @jwks if @jwks
40
-
41
- load_keys
42
- @jwks
43
- end
44
-
45
- def load_keys(opts = {})
46
- @jwks = @jwk_loader.call(opts)
47
- end
48
-
49
- def jwks_keys
50
- Array(jwks[:keys] || jwks['keys'])
51
- end
52
-
53
- def find_key(kid)
54
- jwks_keys.find { |key| (key[:kid] || key['kid']) == kid }
55
- end
56
-
57
- def reloadable?
58
- @jwk_loader
36
+ # Second try, invalidate for backwards compatibility
37
+ @jwks = JWT::JWK::Set.new(@jwks_loader.call(invalidate: true, kid_not_found: true, kid: kid))
38
+ @jwks.find { |key| key[:kid] == kid }
59
39
  end
60
40
  end
61
41
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWK
5
+ class KidAsKeyDigest
6
+ def initialize(jwk)
7
+ @jwk = jwk
8
+ end
9
+
10
+ def generate
11
+ @jwk.key_digest
12
+ end
13
+ end
14
+ end
15
+ end