jwt 2.3.0 → 2.7.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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/AUTHORS +60 -53
  3. data/CHANGELOG.md +73 -0
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/CONTRIBUTING.md +99 -0
  6. data/README.md +188 -40
  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 +7 -4
  10. data/lib/jwt/algos/hmac.rb +56 -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 +5 -1
  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 +2 -0
  17. data/lib/jwt/algos.rb +38 -15
  18. data/lib/jwt/claims_validator.rb +3 -1
  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 +83 -26
  24. data/lib/jwt/encode.rb +30 -20
  25. data/lib/jwt/error.rb +1 -0
  26. data/lib/jwt/jwk/ec.rb +147 -61
  27. data/lib/jwt/jwk/hmac.rb +69 -24
  28. data/lib/jwt/jwk/key_base.rb +43 -6
  29. data/lib/jwt/jwk/key_finder.rb +19 -35
  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 +142 -54
  33. data/lib/jwt/jwk/set.rb +80 -0
  34. data/lib/jwt/jwk/thumbprint.rb +26 -0
  35. data/lib/jwt/jwk.rb +15 -11
  36. data/lib/jwt/security_utils.rb +2 -27
  37. data/lib/jwt/verify.rb +10 -2
  38. data/lib/jwt/version.rb +22 -2
  39. data/lib/jwt/x5c_key_finder.rb +55 -0
  40. data/lib/jwt.rb +5 -4
  41. data/ruby-jwt.gemspec +12 -5
  42. metadata +20 -16
  43. data/.github/workflows/test.yml +0 -74
  44. data/.gitignore +0 -11
  45. data/.rspec +0 -2
  46. data/.rubocop.yml +0 -97
  47. data/.rubocop_todo.yml +0 -185
  48. data/.sourcelevel.yml +0 -18
  49. data/Appraisals +0 -10
  50. data/Gemfile +0 -5
  51. data/Rakefile +0 -14
  52. data/lib/jwt/default_options.rb +0 -16
  53. data/lib/jwt/signature.rb +0 -39
data/lib/jwt/jwk/rsa.rb CHANGED
@@ -2,95 +2,174 @@
2
2
 
3
3
  module JWT
4
4
  module JWK
5
- class RSA < KeyBase
5
+ class RSA < KeyBase # rubocop:disable Metrics/ClassLength
6
6
  BINARY = 2
7
- KTY = 'RSA'.freeze
8
- KTYS = [KTY, OpenSSL::PKey::RSA].freeze
9
- RSA_KEY_ELEMENTS = %i[n e d p q dp dq qi].freeze
7
+ KTY = 'RSA'
8
+ KTYS = [KTY, OpenSSL::PKey::RSA, JWT::JWK::RSA].freeze
9
+ RSA_PUBLIC_KEY_ELEMENTS = %i[kty n e].freeze
10
+ RSA_PRIVATE_KEY_ELEMENTS = %i[d p q dp dq qi].freeze
11
+ RSA_KEY_ELEMENTS = (RSA_PRIVATE_KEY_ELEMENTS + RSA_PUBLIC_KEY_ELEMENTS).freeze
10
12
 
11
- def initialize(keypair, kid = nil)
12
- raise ArgumentError, 'keypair must be of type OpenSSL::PKey::RSA' unless keypair.is_a?(OpenSSL::PKey::RSA)
13
- super(keypair, kid || generate_kid(keypair.public_key))
13
+ RSA_OPT_PARAMS = %i[p q dp dq qi].freeze
14
+ RSA_ASN1_SEQUENCE = (%i[n e d] + RSA_OPT_PARAMS).freeze # https://www.rfc-editor.org/rfc/rfc3447#appendix-A.1.2
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
+ rsa_key
14
32
  end
15
33
 
16
34
  def private?
17
- keypair.private?
35
+ rsa_key.private?
18
36
  end
19
37
 
20
38
  def public_key
21
- keypair.public_key
39
+ rsa_key.public_key
22
40
  end
23
41
 
24
- def export(options = {})
25
- exported_hash = {
26
- kty: KTY,
27
- n: encode_open_ssl_bn(public_key.n),
28
- e: encode_open_ssl_bn(public_key.e),
29
- kid: kid
30
- }
42
+ def signing_key
43
+ rsa_key if private?
44
+ end
31
45
 
32
- return exported_hash unless private? && options[:include_private] == true
46
+ def verify_key
47
+ rsa_key.public_key
48
+ end
33
49
 
34
- append_private_parts(exported_hash)
50
+ def export(options = {})
51
+ exported = parameters.clone
52
+ exported.reject! { |k, _| RSA_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true
53
+ exported
35
54
  end
36
55
 
37
- private
56
+ def members
57
+ RSA_PUBLIC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] }
58
+ end
38
59
 
39
- def generate_kid(public_key)
60
+ def key_digest
40
61
  sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(public_key.n),
41
62
  OpenSSL::ASN1::Integer.new(public_key.e)])
42
63
  OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
43
64
  end
44
65
 
45
- def append_private_parts(the_hash)
46
- the_hash.merge(
47
- d: encode_open_ssl_bn(keypair.d),
48
- p: encode_open_ssl_bn(keypair.p),
49
- q: encode_open_ssl_bn(keypair.q),
50
- dp: encode_open_ssl_bn(keypair.dmp1),
51
- dq: encode_open_ssl_bn(keypair.dmq1),
52
- qi: encode_open_ssl_bn(keypair.iqmp)
53
- )
66
+ def []=(key, value)
67
+ if RSA_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 rsa_key
77
+ @rsa_key ||= self.class.create_rsa_key(jwk_attributes(*(RSA_KEY_ELEMENTS - [:kty])))
78
+ end
79
+
80
+ def extract_key_params(key)
81
+ case key
82
+ when JWT::JWK::RSA
83
+ key.export(include_private: true)
84
+ when OpenSSL::PKey::RSA # Accept OpenSSL key as input
85
+ @rsa_key = key # Preserve the object to avoid recreation
86
+ parse_rsa_key(key)
87
+ when Hash
88
+ key.transform_keys(&:to_sym)
89
+ else
90
+ raise ArgumentError, 'key must be of type OpenSSL::PKey::RSA 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 (RSA_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 RSA' unless key_params[:n] && key_params[:e]
98
+ end
99
+
100
+ def parse_rsa_key(key)
101
+ {
102
+ kty: KTY,
103
+ n: encode_open_ssl_bn(key.n),
104
+ e: encode_open_ssl_bn(key.e),
105
+ d: encode_open_ssl_bn(key.d),
106
+ p: encode_open_ssl_bn(key.p),
107
+ q: encode_open_ssl_bn(key.q),
108
+ dp: encode_open_ssl_bn(key.dmp1),
109
+ dq: encode_open_ssl_bn(key.dmq1),
110
+ qi: encode_open_ssl_bn(key.iqmp)
111
+ }.compact
112
+ end
113
+
114
+ def jwk_attributes(*attributes)
115
+ attributes.each_with_object({}) do |attribute, hash|
116
+ hash[attribute] = decode_open_ssl_bn(self[attribute])
117
+ end
54
118
  end
55
119
 
56
120
  def encode_open_ssl_bn(key_part)
121
+ return unless key_part
122
+
57
123
  ::JWT::Base64.url_encode(key_part.to_s(BINARY))
58
124
  end
59
125
 
126
+ def decode_open_ssl_bn(jwk_data)
127
+ self.class.decode_open_ssl_bn(jwk_data)
128
+ end
129
+
60
130
  class << self
61
131
  def import(jwk_data)
62
- pkey_params = jwk_attributes(jwk_data, *RSA_KEY_ELEMENTS) do |value|
63
- decode_open_ssl_bn(value)
64
- end
65
- kid = jwk_attributes(jwk_data, :kid)[:kid]
66
- self.new(rsa_pkey(pkey_params), kid)
132
+ new(jwk_data)
67
133
  end
68
134
 
69
- private
135
+ def decode_open_ssl_bn(jwk_data)
136
+ return nil unless jwk_data
70
137
 
71
- def jwk_attributes(jwk_data, *attributes)
72
- attributes.each_with_object({}) do |attribute, hash|
73
- value = jwk_data[attribute] || jwk_data[attribute.to_s]
74
- value = yield(value) if block_given?
75
- hash[attribute] = value
76
- end
138
+ OpenSSL::BN.new(::JWT::Base64.url_decode(jwk_data), BINARY)
77
139
  end
78
140
 
79
- def rsa_pkey(rsa_parameters)
80
- raise JWT::JWKError, 'Key format is invalid for RSA' unless rsa_parameters[:n] && rsa_parameters[:e]
141
+ def create_rsa_key_using_der(rsa_parameters)
142
+ validate_rsa_parameters!(rsa_parameters)
81
143
 
82
- populate_key(OpenSSL::PKey::RSA.new, rsa_parameters)
144
+ sequence = RSA_ASN1_SEQUENCE.each_with_object([]) do |key, arr|
145
+ next if rsa_parameters[key].nil?
146
+
147
+ arr << OpenSSL::ASN1::Integer.new(rsa_parameters[key])
148
+ end
149
+
150
+ if sequence.size > 2 # Append "two-prime" version for private key
151
+ sequence.unshift(OpenSSL::ASN1::Integer.new(0))
152
+
153
+ raise JWT::JWKError, 'Creating a RSA key with a private key requires the CRT parameters to be defined' if sequence.size < RSA_ASN1_SEQUENCE.size
154
+ end
155
+
156
+ OpenSSL::PKey::RSA.new(OpenSSL::ASN1::Sequence(sequence).to_der)
83
157
  end
84
158
 
85
- if OpenSSL::PKey::RSA.new.respond_to?(:set_key)
86
- def populate_key(rsa_key, rsa_parameters)
159
+ def create_rsa_key_using_sets(rsa_parameters)
160
+ validate_rsa_parameters!(rsa_parameters)
161
+
162
+ OpenSSL::PKey::RSA.new.tap do |rsa_key|
87
163
  rsa_key.set_key(rsa_parameters[:n], rsa_parameters[:e], rsa_parameters[:d])
88
164
  rsa_key.set_factors(rsa_parameters[:p], rsa_parameters[:q]) if rsa_parameters[:p] && rsa_parameters[:q]
89
165
  rsa_key.set_crt_params(rsa_parameters[:dp], rsa_parameters[:dq], rsa_parameters[:qi]) if rsa_parameters[:dp] && rsa_parameters[:dq] && rsa_parameters[:qi]
90
- rsa_key
91
166
  end
92
- else
93
- def populate_key(rsa_key, rsa_parameters)
167
+ end
168
+
169
+ def create_rsa_key_using_accessors(rsa_parameters) # rubocop:disable Metrics/AbcSize
170
+ validate_rsa_parameters!(rsa_parameters)
171
+
172
+ OpenSSL::PKey::RSA.new.tap do |rsa_key|
94
173
  rsa_key.n = rsa_parameters[:n]
95
174
  rsa_key.e = rsa_parameters[:e]
96
175
  rsa_key.d = rsa_parameters[:d] if rsa_parameters[:d]
@@ -99,15 +178,24 @@ module JWT
99
178
  rsa_key.dmp1 = rsa_parameters[:dp] if rsa_parameters[:dp]
100
179
  rsa_key.dmq1 = rsa_parameters[:dq] if rsa_parameters[:dq]
101
180
  rsa_key.iqmp = rsa_parameters[:qi] if rsa_parameters[:qi]
102
-
103
- rsa_key
104
181
  end
105
182
  end
106
183
 
107
- def decode_open_ssl_bn(jwk_data)
108
- return nil unless jwk_data
184
+ def validate_rsa_parameters!(rsa_parameters)
185
+ return unless rsa_parameters.key?(:d)
109
186
 
110
- OpenSSL::BN.new(::JWT::Base64.url_decode(jwk_data), BINARY)
187
+ parameters = RSA_OPT_PARAMS - rsa_parameters.keys
188
+ return if parameters.empty? || parameters.size == RSA_OPT_PARAMS.size
189
+
190
+ raise JWT::JWKError, 'When one of p, q, dp, dq or qi is given all the other optimization parameters also needs to be defined' # https://www.rfc-editor.org/rfc/rfc7518.html#section-6.3.2
191
+ end
192
+
193
+ if ::JWT.openssl_3?
194
+ alias create_rsa_key create_rsa_key_using_der
195
+ elsif OpenSSL::PKey::RSA.new.respond_to?(:set_key)
196
+ alias create_rsa_key create_rsa_key_using_sets
197
+ else
198
+ alias create_rsa_key create_rsa_key_using_accessors
111
199
  end
112
200
  end
113
201
  end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module JWT
6
+ module JWK
7
+ class Set
8
+ include Enumerable
9
+ extend Forwardable
10
+
11
+ attr_reader :keys
12
+
13
+ def initialize(jwks = nil, options = {}) # rubocop:disable Metrics/CyclomaticComplexity
14
+ jwks ||= {}
15
+
16
+ @keys = case jwks
17
+ when JWT::JWK::Set # Simple duplication
18
+ jwks.keys
19
+ when JWT::JWK::KeyBase # Singleton
20
+ [jwks]
21
+ when Hash
22
+ jwks = jwks.transform_keys(&:to_sym)
23
+ [*jwks[:keys]].map { |k| JWT::JWK.new(k, nil, options) }
24
+ when Array
25
+ jwks.map { |k| JWT::JWK.new(k, nil, options) }
26
+ else
27
+ raise ArgumentError, 'Can only create new JWKS from Hash, Array and JWK'
28
+ end
29
+ end
30
+
31
+ def export(options = {})
32
+ { keys: @keys.map { |k| k.export(options) } }
33
+ end
34
+
35
+ def_delegators :@keys, :each, :size, :delete, :dig
36
+
37
+ def select!(&block)
38
+ return @keys.select! unless block
39
+
40
+ self if @keys.select!(&block)
41
+ end
42
+
43
+ def reject!(&block)
44
+ return @keys.reject! unless block
45
+
46
+ self if @keys.reject!(&block)
47
+ end
48
+
49
+ def uniq!(&block)
50
+ self if @keys.uniq!(&block)
51
+ end
52
+
53
+ def merge(enum)
54
+ @keys += JWT::JWK::Set.new(enum.to_a).keys
55
+ self
56
+ end
57
+
58
+ def union(enum)
59
+ dup.merge(enum)
60
+ end
61
+
62
+ def add(key)
63
+ @keys << JWT::JWK.new(key)
64
+ self
65
+ end
66
+
67
+ def ==(other)
68
+ other.is_a?(JWT::JWK::Set) && keys.sort == other.keys.sort
69
+ end
70
+
71
+ alias eql? ==
72
+ alias filter! select!
73
+ alias length size
74
+ # For symbolic manipulation
75
+ alias | union
76
+ alias + union
77
+ alias << add
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWK
5
+ # https://tools.ietf.org/html/rfc7638
6
+ class Thumbprint
7
+ attr_reader :jwk
8
+
9
+ def initialize(jwk)
10
+ @jwk = jwk
11
+ end
12
+
13
+ def generate
14
+ ::Base64.urlsafe_encode64(
15
+ Digest::SHA256.digest(
16
+ JWT::JSON.generate(
17
+ jwk.members.sort.to_h
18
+ )
19
+ ), padding: false
20
+ )
21
+ end
22
+
23
+ alias to_s generate
24
+ end
25
+ end
26
+ end
data/lib/jwt/jwk.rb CHANGED
@@ -1,23 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'jwk/key_finder'
4
+ require_relative 'jwk/set'
4
5
 
5
6
  module JWT
6
7
  module JWK
7
8
  class << self
8
- def import(jwk_data)
9
- jwk_kty = jwk_data[:kty] || jwk_data['kty']
10
- raise JWT::JWKError, 'Key type (kty) not provided' unless jwk_kty
11
-
12
- mappings.fetch(jwk_kty.to_s) do |kty|
13
- raise JWT::JWKError, "Key type #{kty} not supported"
14
- end.import(jwk_data)
15
- end
9
+ def create_from(key, params = nil, options = {})
10
+ if key.is_a?(Hash)
11
+ jwk_kty = key[:kty] || key['kty']
12
+ raise JWT::JWKError, 'Key type (kty) not provided' unless jwk_kty
13
+
14
+ return mappings.fetch(jwk_kty.to_s) do |kty|
15
+ raise JWT::JWKError, "Key type #{kty} not supported"
16
+ end.new(key, params, options)
17
+ end
16
18
 
17
- def create_from(keypair, kid = nil)
18
- mappings.fetch(keypair.class) do |klass|
19
+ mappings.fetch(key.class) do |klass|
19
20
  raise JWT::JWKError, "Cannot create JWK from a #{klass.name}"
20
- end.new(keypair, kid)
21
+ end.new(key, params, options)
21
22
  end
22
23
 
23
24
  def classes
@@ -26,6 +27,7 @@ module JWT
26
27
  end
27
28
 
28
29
  alias new create_from
30
+ alias import create_from
29
31
 
30
32
  private
31
33
 
@@ -36,6 +38,7 @@ module JWT
36
38
  def generate_mappings
37
39
  classes.each_with_object({}) do |klass, hash|
38
40
  next unless klass.const_defined?('KTYS')
41
+
39
42
  Array(klass::KTYS).each do |kty|
40
43
  hash[kty] = klass
41
44
  end
@@ -49,3 +52,4 @@ require_relative 'jwk/key_base'
49
52
  require_relative 'jwk/ec'
50
53
  require_relative 'jwk/rsa'
51
54
  require_relative 'jwk/hmac'
55
+ require_relative 'jwk/okp_rbnacl' if ::JWT.rbnacl?
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JWT
2
4
  # Collection of security methods
3
5
  #
@@ -5,17 +7,6 @@ module JWT
5
7
  module SecurityUtils
6
8
  module_function
7
9
 
8
- def secure_compare(left, right)
9
- left_bytesize = left.bytesize
10
-
11
- return false unless left_bytesize == right.bytesize
12
-
13
- unpacked_left = left.unpack "C#{left_bytesize}"
14
- result = 0
15
- right.each_byte { |byte| result |= byte ^ unpacked_left.shift }
16
- result.zero?
17
- end
18
-
19
10
  def verify_rsa(algorithm, public_key, signing_input, signature)
20
11
  public_key.verify(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), signature, signing_input)
21
12
  end
@@ -37,21 +28,5 @@ module JWT
37
28
  sig_char = signature[byte_size..-1] || ''
38
29
  OpenSSL::ASN1::Sequence.new([sig_bytes, sig_char].map { |int| OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(int, 2)) }).to_der
39
30
  end
40
-
41
- def rbnacl_fixup(algorithm, key)
42
- algorithm = algorithm.sub('HS', 'SHA').to_sym
43
-
44
- return [] unless defined?(RbNaCl) && RbNaCl::HMAC.constants(false).include?(algorithm)
45
-
46
- authenticator = RbNaCl::HMAC.const_get(algorithm)
47
-
48
- # Fall back to OpenSSL for keys larger than 32 bytes.
49
- return [] if key.bytesize > authenticator.key_bytes
50
-
51
- [
52
- authenticator,
53
- key.bytes.fill(0, key.bytesize...authenticator.key_bytes).pack('C*')
54
- ]
55
- end
56
31
  end
57
32
  end
data/lib/jwt/verify.rb CHANGED
@@ -19,6 +19,7 @@ module JWT
19
19
  def verify_claims(payload, options)
20
20
  options.each do |key, val|
21
21
  next unless key.to_s =~ /verify/
22
+
22
23
  Verify.send(key, payload, options) if val
23
24
  end
24
25
  end
@@ -53,9 +54,14 @@ module JWT
53
54
 
54
55
  iss = @payload['iss']
55
56
 
56
- return if Array(options_iss).map(&:to_s).include?(iss.to_s)
57
+ options_iss = Array(options_iss).map { |item| item.is_a?(Symbol) ? item.to_s : item }
57
58
 
58
- raise(JWT::InvalidIssuerError, "Invalid issuer. Expected #{options_iss}, received #{iss || '<none>'}")
59
+ case iss
60
+ when *options_iss
61
+ nil
62
+ else
63
+ raise(JWT::InvalidIssuerError, "Invalid issuer. Expected #{options_iss}, received #{iss || '<none>'}")
64
+ end
59
65
  end
60
66
 
61
67
  def verify_jti
@@ -77,12 +83,14 @@ module JWT
77
83
 
78
84
  def verify_sub
79
85
  return unless (options_sub = @options[:sub])
86
+
80
87
  sub = @payload['sub']
81
88
  raise(JWT::InvalidSubError, "Invalid subject. Expected #{options_sub}, received #{sub || '<none>'}") unless sub.to_s == options_sub.to_s
82
89
  end
83
90
 
84
91
  def verify_required_claims
85
92
  return unless (options_required_claims = @options[:required_claims])
93
+
86
94
  options_required_claims.each do |required_claim|
87
95
  raise(JWT::MissingRequiredClaim, "Missing required claim #{required_claim}") unless @payload.include?(required_claim)
88
96
  end
data/lib/jwt/version.rb CHANGED
@@ -1,4 +1,3 @@
1
- # encoding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
3
  # Moments version builder module
@@ -12,7 +11,7 @@ module JWT
12
11
  # major version
13
12
  MAJOR = 2
14
13
  # minor version
15
- MINOR = 3
14
+ MINOR = 7
16
15
  # tiny version
17
16
  TINY = 0
18
17
  # alpha, beta, etc. tag
@@ -21,4 +20,25 @@ module JWT
21
20
  # Build version string
22
21
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
23
22
  end
23
+
24
+ def self.openssl_3?
25
+ return false if OpenSSL::OPENSSL_VERSION.include?('LibreSSL')
26
+ return true if OpenSSL::OPENSSL_VERSION_NUMBER >= 3 * 0x10000000
27
+ end
28
+
29
+ def self.rbnacl?
30
+ defined?(::RbNaCl)
31
+ end
32
+
33
+ def self.rbnacl_6_or_greater?
34
+ rbnacl? && ::Gem::Version.new(::RbNaCl::VERSION) >= ::Gem::Version.new('6.0.0')
35
+ end
36
+
37
+ def self.openssl_3_hmac_empty_key_regression?
38
+ openssl_3? && openssl_version <= ::Gem::Version.new('3.0.0')
39
+ end
40
+
41
+ def self.openssl_version
42
+ @openssl_version ||= ::Gem::Version.new(OpenSSL::VERSION)
43
+ end
24
44
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'jwt/error'
5
+
6
+ module JWT
7
+ # If the x5c header certificate chain can be validated by trusted root
8
+ # certificates, and none of the certificates are revoked, returns the public
9
+ # key from the first certificate.
10
+ # See https://tools.ietf.org/html/rfc7515#section-4.1.6
11
+ class X5cKeyFinder
12
+ def initialize(root_certificates, crls = nil)
13
+ raise(ArgumentError, 'Root certificates must be specified') unless root_certificates
14
+
15
+ @store = build_store(root_certificates, crls)
16
+ end
17
+
18
+ def from(x5c_header_or_certificates)
19
+ signing_certificate, *certificate_chain = parse_certificates(x5c_header_or_certificates)
20
+ store_context = OpenSSL::X509::StoreContext.new(@store, signing_certificate, certificate_chain)
21
+
22
+ if store_context.verify
23
+ signing_certificate.public_key
24
+ else
25
+ error = "Certificate verification failed: #{store_context.error_string}."
26
+ if (current_cert = store_context.current_cert)
27
+ error = "#{error} Certificate subject: #{current_cert.subject}."
28
+ end
29
+
30
+ raise(JWT::VerificationError, error)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def build_store(root_certificates, crls)
37
+ store = OpenSSL::X509::Store.new
38
+ store.purpose = OpenSSL::X509::PURPOSE_ANY
39
+ store.flags = OpenSSL::X509::V_FLAG_CRL_CHECK | OpenSSL::X509::V_FLAG_CRL_CHECK_ALL
40
+ root_certificates.each { |certificate| store.add_cert(certificate) }
41
+ crls&.each { |crl| store.add_crl(crl) }
42
+ store
43
+ end
44
+
45
+ def parse_certificates(x5c_header_or_certificates)
46
+ if x5c_header_or_certificates.all? { |obj| obj.is_a?(OpenSSL::X509::Certificate) }
47
+ x5c_header_or_certificates
48
+ else
49
+ x5c_header_or_certificates.map do |encoded|
50
+ OpenSSL::X509::Certificate.new(::JWT::Base64.url_decode(encoded))
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
data/lib/jwt.rb CHANGED
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'jwt/version'
3
4
  require 'jwt/base64'
4
5
  require 'jwt/json'
5
6
  require 'jwt/decode'
6
- require 'jwt/default_options'
7
+ require 'jwt/configuration'
7
8
  require 'jwt/encode'
8
9
  require 'jwt/error'
9
10
  require 'jwt/jwk'
@@ -13,7 +14,7 @@ require 'jwt/jwk'
13
14
  # Should be up to date with the latest spec:
14
15
  # https://tools.ietf.org/html/rfc7519
15
16
  module JWT
16
- include JWT::DefaultOptions
17
+ extend ::JWT::Configuration
17
18
 
18
19
  module_function
19
20
 
@@ -24,7 +25,7 @@ module JWT
24
25
  headers: header_fields).segments
25
26
  end
26
27
 
27
- def decode(jwt, key = nil, verify = true, options = {}, &keyfinder)
28
- Decode.new(jwt, key, verify, DEFAULT_OPTIONS.merge(options), &keyfinder).decode_segments
28
+ def decode(jwt, key = nil, verify = true, options = {}, &keyfinder) # rubocop:disable Style/OptionalBooleanParameter
29
+ Decode.new(jwt, key, verify, configuration.decode.to_h.merge(options), &keyfinder).decode_segments
29
30
  end
30
31
  end
data/ruby-jwt.gemspec CHANGED
@@ -1,4 +1,6 @@
1
- lib = File.expand_path('../lib/', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
2
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
5
  require 'jwt/version'
4
6
 
@@ -13,15 +15,20 @@ Gem::Specification.new do |spec|
13
15
  spec.description = 'A pure ruby implementation of the RFC 7519 OAuth JSON Web Token (JWT) standard.'
14
16
  spec.homepage = 'https://github.com/jwt/ruby-jwt'
15
17
  spec.license = 'MIT'
16
- spec.required_ruby_version = '>= 2.1'
18
+ spec.required_ruby_version = '>= 2.5'
17
19
  spec.metadata = {
18
20
  'bug_tracker_uri' => 'https://github.com/jwt/ruby-jwt/issues',
19
- 'changelog_uri' => "https://github.com/jwt/ruby-jwt/blob/v#{JWT.gem_version}/CHANGELOG.md"
21
+ 'changelog_uri' => "https://github.com/jwt/ruby-jwt/blob/v#{JWT.gem_version}/CHANGELOG.md",
22
+ 'rubygems_mfa_required' => 'true'
20
23
  }
21
24
 
22
- spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec|gemfiles|coverage|bin)/}) }
25
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
26
+ f.match(%r{^(spec|gemfiles|coverage|bin)/}) || # Irrelevant folders
27
+ f.match(/^\.+/) || # Files and folders starting with .
28
+ f.match(/^(Appraisals|Gemfile|Rakefile)$/) # Irrelevant files
29
+ end
30
+
23
31
  spec.executables = []
24
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
25
32
  spec.require_paths = %w[lib]
26
33
 
27
34
  spec.add_development_dependency 'appraisal'