jwt 2.3.0 → 2.5.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/.codeclimate.yml +8 -0
  3. data/.github/workflows/coverage.yml +27 -0
  4. data/.github/workflows/test.yml +15 -22
  5. data/.gitignore +2 -0
  6. data/.reek.yml +22 -0
  7. data/.rubocop.yml +17 -47
  8. data/.sourcelevel.yml +3 -4
  9. data/AUTHORS +60 -53
  10. data/Appraisals +3 -0
  11. data/CHANGELOG.md +47 -0
  12. data/CODE_OF_CONDUCT.md +84 -0
  13. data/CONTRIBUTING.md +99 -0
  14. data/Gemfile +3 -1
  15. data/README.md +114 -34
  16. data/Rakefile +2 -0
  17. data/lib/jwt/algos/ecdsa.rb +37 -8
  18. data/lib/jwt/algos/eddsa.rb +5 -0
  19. data/lib/jwt/algos/hmac.rb +2 -0
  20. data/lib/jwt/algos/none.rb +2 -0
  21. data/lib/jwt/algos/ps.rb +3 -3
  22. data/lib/jwt/algos/rsa.rb +4 -1
  23. data/lib/jwt/algos/unsupported.rb +2 -0
  24. data/lib/jwt/claims_validator.rb +3 -1
  25. data/lib/jwt/configuration/container.rb +21 -0
  26. data/lib/jwt/configuration/decode_configuration.rb +46 -0
  27. data/lib/jwt/configuration/jwk_configuration.rb +27 -0
  28. data/lib/jwt/configuration.rb +15 -0
  29. data/lib/jwt/decode.rb +42 -8
  30. data/lib/jwt/encode.rb +6 -6
  31. data/lib/jwt/error.rb +1 -0
  32. data/lib/jwt/jwk/ec.rb +92 -43
  33. data/lib/jwt/jwk/hmac.rb +19 -10
  34. data/lib/jwt/jwk/key_base.rb +23 -6
  35. data/lib/jwt/jwk/key_finder.rb +1 -1
  36. data/lib/jwt/jwk/kid_as_key_digest.rb +15 -0
  37. data/lib/jwt/jwk/rsa.rb +54 -31
  38. data/lib/jwt/jwk/thumbprint.rb +26 -0
  39. data/lib/jwt/jwk.rb +1 -0
  40. data/lib/jwt/security_utils.rb +2 -0
  41. data/lib/jwt/signature.rb +3 -7
  42. data/lib/jwt/verify.rb +10 -2
  43. data/lib/jwt/version.rb +6 -2
  44. data/lib/jwt/x5c_key_finder.rb +55 -0
  45. data/lib/jwt.rb +5 -4
  46. data/ruby-jwt.gemspec +6 -3
  47. metadata +31 -7
  48. data/.rubocop_todo.yml +0 -185
  49. data/lib/jwt/default_options.rb +0 -16
data/lib/jwt/decode.rb CHANGED
@@ -4,12 +4,14 @@ require 'json'
4
4
 
5
5
  require 'jwt/signature'
6
6
  require 'jwt/verify'
7
+ require 'jwt/x5c_key_finder'
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
@@ -23,28 +25,50 @@ module JWT
23
25
  validate_segment_count!
24
26
  if @verify
25
27
  decode_crypto
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
36
53
  raise(JWT::IncorrectAlgorithm, 'An algorithm must be specified') if allowed_algorithms.empty?
37
- raise(JWT::IncorrectAlgorithm, 'Token is missing alg header') unless header['alg']
54
+ raise(JWT::IncorrectAlgorithm, 'Token is missing alg header') unless algorithm
38
55
  raise(JWT::IncorrectAlgorithm, 'Expected a different algorithm') unless options_includes_algo_in_header?
56
+ end
39
57
 
58
+ def set_key
40
59
  @key = find_key(&@keyfinder) if @keyfinder
41
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
42
65
 
43
- Signature.verify(header['alg'], @key, signing_input, @signature)
66
+ def verify_signature_for?(key)
67
+ Signature.verify(algorithm, key, signing_input, @signature)
44
68
  end
45
69
 
46
70
  def options_includes_algo_in_header?
47
- allowed_algorithms.any? { |alg| alg.casecmp(header['alg']).zero? }
71
+ allowed_algorithms.any? { |alg| alg.casecmp(algorithm).zero? }
48
72
  end
49
73
 
50
74
  def allowed_algorithms
@@ -65,8 +89,10 @@ module JWT
65
89
 
66
90
  def find_key(&keyfinder)
67
91
  key = (keyfinder.arity == 2 ? yield(header, payload) : yield(header))
68
- raise JWT::DecodeError, 'No verification key available' unless key
69
- key
92
+ # key can be of type [string, nil, OpenSSL::PKey, Array]
93
+ return key if key && !Array(key).empty?
94
+
95
+ raise JWT::DecodeError, 'No verification key available'
70
96
  end
71
97
 
72
98
  def verify_claims
@@ -77,7 +103,7 @@ module JWT
77
103
  def validate_segment_count!
78
104
  return if segment_length == 3
79
105
  return if !@verify && segment_length == 2 # If no verifying required, the signature is not needed
80
- return if segment_length == 2 && header['alg'] == 'none'
106
+ return if segment_length == 2 && none_algorithm?
81
107
 
82
108
  raise(JWT::DecodeError, 'Not enough or too many segments')
83
109
  end
@@ -86,8 +112,16 @@ module JWT
86
112
  @segments.count
87
113
  end
88
114
 
115
+ def none_algorithm?
116
+ algorithm == 'none'
117
+ end
118
+
89
119
  def decode_crypto
90
- @signature = JWT::Base64.url_decode(@segments[2] || '')
120
+ @signature = ::JWT::Base64.url_decode(@segments[2] || '')
121
+ end
122
+
123
+ def algorithm
124
+ header['alg']
91
125
  end
92
126
 
93
127
  def header
@@ -103,7 +137,7 @@ module JWT
103
137
  end
104
138
 
105
139
  def parse_and_decode(segment)
106
- JWT::JSON.parse(JWT::Base64.url_decode(segment))
140
+ JWT::JSON.parse(::JWT::Base64.url_decode(segment))
107
141
  rescue ::JSON::ParserError
108
142
  raise JWT::DecodeError, 'Invalid segment encoding'
109
143
  end
data/lib/jwt/encode.rb CHANGED
@@ -7,14 +7,14 @@ require_relative './claims_validator'
7
7
  module JWT
8
8
  # Encoding logic for JWT
9
9
  class Encode
10
- ALG_NONE = 'none'.freeze
11
- ALG_KEY = 'alg'.freeze
10
+ ALG_NONE = 'none'
11
+ ALG_KEY = 'alg'
12
12
 
13
13
  def initialize(options)
14
14
  @payload = options[:payload]
15
15
  @key = options[:key]
16
16
  _, @algorithm = Algos.find(options[:algorithm])
17
- @headers = options[:headers].each_with_object({}) { |(key, value), headers| headers[key.to_s] = value }
17
+ @headers = options[:headers].transform_keys(&:to_s)
18
18
  end
19
19
 
20
20
  def segments
@@ -45,7 +45,7 @@ module JWT
45
45
  end
46
46
 
47
47
  def encode_payload
48
- if @payload && @payload.is_a?(Hash)
48
+ if @payload.is_a?(Hash)
49
49
  ClaimsValidator.new(@payload).validate!
50
50
  end
51
51
 
@@ -55,11 +55,11 @@ module JWT
55
55
  def encode_signature
56
56
  return '' if @algorithm == ALG_NONE
57
57
 
58
- JWT::Base64.url_encode(JWT::Signature.sign(@algorithm, encoded_header_and_payload, @key))
58
+ ::JWT::Base64.url_encode(JWT::Signature.sign(@algorithm, encoded_header_and_payload, @key))
59
59
  end
60
60
 
61
61
  def encode(data)
62
- JWT::Base64.url_encode(JWT::JSON.generate(data))
62
+ ::JWT::Base64.url_encode(JWT::JSON.generate(data))
63
63
  end
64
64
 
65
65
  def combine(*parts)
data/lib/jwt/error.rb CHANGED
@@ -10,6 +10,7 @@ module JWT
10
10
  class IncorrectAlgorithm < DecodeError; end
11
11
  class ImmatureSignature < DecodeError; end
12
12
  class InvalidIssuerError < DecodeError; end
13
+ class UnsupportedEcdsaCurve < IncorrectAlgorithm; end
13
14
  class InvalidIatError < DecodeError; end
14
15
  class InvalidAudError < DecodeError; end
15
16
  class InvalidSubError < DecodeError; end
data/lib/jwt/jwk/ec.rb CHANGED
@@ -4,39 +4,53 @@ 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
- KTY = 'EC'.freeze
11
+ KTY = 'EC'
12
12
  KTYS = [KTY, OpenSSL::PKey::EC].freeze
13
13
  BINARY = 2
14
14
 
15
- def initialize(keypair, kid = nil)
15
+ attr_reader :keypair
16
+
17
+ def initialize(keypair, options = {})
16
18
  raise ArgumentError, 'keypair must be of type OpenSSL::PKey::EC' unless keypair.is_a?(OpenSSL::PKey::EC)
17
19
 
18
- kid ||= generate_kid(keypair)
19
- super(keypair, kid)
20
+ @keypair = keypair
21
+
22
+ super(options)
20
23
  end
21
24
 
22
25
  def private?
23
26
  @keypair.private_key?
24
27
  end
25
28
 
26
- def export(options = {})
29
+ def members
27
30
  crv, x_octets, y_octets = keypair_components(keypair)
28
- exported_hash = {
31
+ {
29
32
  kty: KTY,
30
33
  crv: crv,
31
34
  x: encode_octets(x_octets),
32
- y: encode_octets(y_octets),
33
- kid: kid
35
+ y: encode_octets(y_octets)
34
36
  }
37
+ end
38
+
39
+ def export(options = {})
40
+ exported_hash = members.merge(kid: kid)
41
+
35
42
  return exported_hash unless private? && options[:include_private] == true
36
43
 
37
44
  append_private_parts(exported_hash)
38
45
  end
39
46
 
47
+ def key_digest
48
+ _crv, x_octets, y_octets = keypair_components(keypair)
49
+ sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(x_octets, BINARY)),
50
+ OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(y_octets, BINARY))])
51
+ OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
52
+ end
53
+
40
54
  private
41
55
 
42
56
  def append_private_parts(the_hash)
@@ -46,19 +60,15 @@ module JWT
46
60
  )
47
61
  end
48
62
 
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)
54
- end
55
-
56
63
  def keypair_components(ec_keypair)
57
64
  encoded_point = ec_keypair.public_key.to_bn.to_s(BINARY)
58
65
  case ec_keypair.group.curve_name
59
66
  when 'prime256v1'
60
67
  crv = 'P-256'
61
68
  x_octets, y_octets = encoded_point.unpack('xa32a32')
69
+ when 'secp256k1'
70
+ crv = 'P-256K'
71
+ x_octets, y_octets = encoded_point.unpack('xa32a32')
62
72
  when 'secp384r1'
63
73
  crv = 'P-384'
64
74
  x_octets, y_octets = encoded_point.unpack('xa48a48')
@@ -87,7 +97,7 @@ module JWT
87
97
  jwk_crv, jwk_x, jwk_y, jwk_d, jwk_kid = jwk_attrs(jwk_data, %i[crv x y d kid])
88
98
  raise JWT::JWKError, 'Key format is invalid for EC' unless jwk_crv && jwk_x && jwk_y
89
99
 
90
- new(ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d), jwk_kid)
100
+ new(ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d), kid: jwk_kid)
91
101
  end
92
102
 
93
103
  def to_openssl_curve(crv)
@@ -98,6 +108,7 @@ module JWT
98
108
  when 'P-256' then 'prime256v1'
99
109
  when 'P-384' then 'secp384r1'
100
110
  when 'P-521' then 'secp521r1'
111
+ when 'P-256K' then 'secp256k1'
101
112
  else raise JWT::JWKError, 'Invalid curve provided'
102
113
  end
103
114
  end
@@ -110,31 +121,69 @@ module JWT
110
121
  end
111
122
  end
112
123
 
113
- def ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d)
114
- curve = to_openssl_curve(jwk_crv)
115
-
116
- x_octets = decode_octets(jwk_x)
117
- y_octets = decode_octets(jwk_y)
118
-
119
- key = OpenSSL::PKey::EC.new(curve)
120
-
121
- # The details of the `Point` instantiation are covered in:
122
- # - https://docs.ruby-lang.org/en/2.4.0/OpenSSL/PKey/EC.html
123
- # - https://www.openssl.org/docs/manmaster/man3/EC_POINT_new.html
124
- # - https://tools.ietf.org/html/rfc5480#section-2.2
125
- # - https://www.secg.org/SEC1-Ver-1.0.pdf
126
- # Section 2.3.3 of the last of these references specifies that the
127
- # encoding of an uncompressed point consists of the byte `0x04` followed
128
- # by the x value then the y value.
129
- point = OpenSSL::PKey::EC::Point.new(
130
- OpenSSL::PKey::EC::Group.new(curve),
131
- OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2)
132
- )
133
-
134
- key.public_key = point
135
- key.private_key = OpenSSL::BN.new(decode_octets(jwk_d), 2) if jwk_d
136
-
137
- key
124
+ if ::JWT.openssl_3?
125
+ def ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d) # rubocop:disable Metrics/MethodLength
126
+ curve = to_openssl_curve(jwk_crv)
127
+
128
+ x_octets = decode_octets(jwk_x)
129
+ y_octets = decode_octets(jwk_y)
130
+
131
+ point = OpenSSL::PKey::EC::Point.new(
132
+ OpenSSL::PKey::EC::Group.new(curve),
133
+ OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2)
134
+ )
135
+
136
+ sequence = if jwk_d
137
+ # https://datatracker.ietf.org/doc/html/rfc5915.html
138
+ # ECPrivateKey ::= SEQUENCE {
139
+ # version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1),
140
+ # privateKey OCTET STRING,
141
+ # parameters [0] ECParameters {{ NamedCurve }} OPTIONAL,
142
+ # publicKey [1] BIT STRING OPTIONAL
143
+ # }
144
+
145
+ OpenSSL::ASN1::Sequence([
146
+ OpenSSL::ASN1::Integer(1),
147
+ OpenSSL::ASN1::OctetString(OpenSSL::BN.new(decode_octets(jwk_d), 2).to_s(2)),
148
+ OpenSSL::ASN1::ObjectId(curve, 0, :EXPLICIT),
149
+ OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed), 1, :EXPLICIT)
150
+ ])
151
+ else
152
+ OpenSSL::ASN1::Sequence([
153
+ OpenSSL::ASN1::Sequence([OpenSSL::ASN1::ObjectId('id-ecPublicKey'), OpenSSL::ASN1::ObjectId(curve)]),
154
+ OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed))
155
+ ])
156
+ end
157
+
158
+ OpenSSL::PKey::EC.new(sequence.to_der)
159
+ end
160
+ else
161
+ def ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d)
162
+ curve = to_openssl_curve(jwk_crv)
163
+
164
+ x_octets = decode_octets(jwk_x)
165
+ y_octets = decode_octets(jwk_y)
166
+
167
+ key = OpenSSL::PKey::EC.new(curve)
168
+
169
+ # The details of the `Point` instantiation are covered in:
170
+ # - https://docs.ruby-lang.org/en/2.4.0/OpenSSL/PKey/EC.html
171
+ # - https://www.openssl.org/docs/manmaster/man3/EC_POINT_new.html
172
+ # - https://tools.ietf.org/html/rfc5480#section-2.2
173
+ # - https://www.secg.org/SEC1-Ver-1.0.pdf
174
+ # Section 2.3.3 of the last of these references specifies that the
175
+ # encoding of an uncompressed point consists of the byte `0x04` followed
176
+ # by the x value then the y value.
177
+ point = OpenSSL::PKey::EC::Point.new(
178
+ OpenSSL::PKey::EC::Group.new(curve),
179
+ OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2)
180
+ )
181
+
182
+ key.public_key = point
183
+ key.private_key = OpenSSL::BN.new(decode_octets(jwk_d), 2) if jwk_d
184
+
185
+ key
186
+ end
138
187
  end
139
188
 
140
189
  def decode_octets(jwk_data)
data/lib/jwt/jwk/hmac.rb CHANGED
@@ -3,14 +3,16 @@
3
3
  module JWT
4
4
  module JWK
5
5
  class HMAC < KeyBase
6
- KTY = 'oct'.freeze
6
+ KTY = 'oct'
7
7
  KTYS = [KTY, String].freeze
8
8
 
9
- def initialize(keypair, kid = nil)
10
- raise ArgumentError, 'keypair must be of type String' unless keypair.is_a?(String)
9
+ attr_reader :signing_key
11
10
 
12
- super
13
- @kid = kid || generate_kid
11
+ def initialize(signing_key, options = {})
12
+ raise ArgumentError, 'signing_key must be of type String' unless signing_key.is_a?(String)
13
+
14
+ @signing_key = signing_key
15
+ super(options)
14
16
  end
15
17
 
16
18
  def private?
@@ -31,14 +33,21 @@ module JWT
31
33
  return exported_hash unless private? && options[:include_private] == true
32
34
 
33
35
  exported_hash.merge(
34
- k: keypair
36
+ k: signing_key
35
37
  )
36
38
  end
37
39
 
38
- private
40
+ def members
41
+ {
42
+ kty: KTY,
43
+ k: signing_key
44
+ }
45
+ end
46
+
47
+ alias keypair signing_key # for backwards compatibility
39
48
 
40
- def generate_kid
41
- sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::UTF8String.new(keypair),
49
+ def key_digest
50
+ sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::UTF8String.new(signing_key),
42
51
  OpenSSL::ASN1::UTF8String.new(KTY)])
43
52
  OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
44
53
  end
@@ -50,7 +59,7 @@ module JWT
50
59
 
51
60
  raise JWT::JWKError, 'Key format is invalid for HMAC' unless jwk_k
52
61
 
53
- self.new(jwk_k, jwk_kid)
62
+ new(jwk_k, kid: jwk_kid)
54
63
  end
55
64
  end
56
65
  end
@@ -3,15 +3,32 @@
3
3
  module JWT
4
4
  module JWK
5
5
  class KeyBase
6
- attr_reader :keypair, :kid
6
+ def self.inherited(klass)
7
+ super
8
+ ::JWT::JWK.classes << klass
9
+ end
7
10
 
8
- def initialize(keypair, kid = nil)
9
- @keypair = keypair
10
- @kid = kid
11
+ def initialize(options)
12
+ options ||= {}
13
+
14
+ if options.is_a?(String) # For backwards compatibility when kid was a String
15
+ options = { kid: options }
16
+ end
17
+
18
+ @kid = options[:kid]
19
+ @kid_generator = options[:kid_generator] || ::JWT.configuration.jwk.kid_generator
11
20
  end
12
21
 
13
- def self.inherited(klass)
14
- ::JWT::JWK.classes << klass
22
+ def kid
23
+ @kid ||= generate_kid
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :kid_generator
29
+
30
+ def generate_kid
31
+ kid_generator.new(self).generate
15
32
  end
16
33
  end
17
34
  end
@@ -28,7 +28,7 @@ module JWT
28
28
  return jwk if jwk
29
29
 
30
30
  if reloadable?
31
- load_keys(invalidate: true)
31
+ load_keys(invalidate: true, kid_not_found: true, kid: kid) # invalidate for backwards compatibility
32
32
  return find_key(kid)
33
33
  end
34
34
 
@@ -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
data/lib/jwt/jwk/rsa.rb CHANGED
@@ -4,13 +4,18 @@ module JWT
4
4
  module JWK
5
5
  class RSA < KeyBase
6
6
  BINARY = 2
7
- KTY = 'RSA'.freeze
7
+ KTY = 'RSA'
8
8
  KTYS = [KTY, OpenSSL::PKey::RSA].freeze
9
9
  RSA_KEY_ELEMENTS = %i[n e d p q dp dq qi].freeze
10
10
 
11
- def initialize(keypair, kid = nil)
11
+ attr_reader :keypair
12
+
13
+ def initialize(keypair, options = {})
12
14
  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))
15
+
16
+ @keypair = keypair
17
+
18
+ super(options)
14
19
  end
15
20
 
16
21
  def private?
@@ -22,26 +27,29 @@ module JWT
22
27
  end
23
28
 
24
29
  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
- }
30
+ exported_hash = members.merge(kid: kid)
31
31
 
32
32
  return exported_hash unless private? && options[:include_private] == true
33
33
 
34
34
  append_private_parts(exported_hash)
35
35
  end
36
36
 
37
- private
37
+ def members
38
+ {
39
+ kty: KTY,
40
+ n: encode_open_ssl_bn(public_key.n),
41
+ e: encode_open_ssl_bn(public_key.e)
42
+ }
43
+ end
38
44
 
39
- def generate_kid(public_key)
45
+ def key_digest
40
46
  sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(public_key.n),
41
47
  OpenSSL::ASN1::Integer.new(public_key.e)])
42
48
  OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
43
49
  end
44
50
 
51
+ private
52
+
45
53
  def append_private_parts(the_hash)
46
54
  the_hash.merge(
47
55
  d: encode_open_ssl_bn(keypair.d),
@@ -62,8 +70,7 @@ module JWT
62
70
  pkey_params = jwk_attributes(jwk_data, *RSA_KEY_ELEMENTS) do |value|
63
71
  decode_open_ssl_bn(value)
64
72
  end
65
- kid = jwk_attributes(jwk_data, :kid)[:kid]
66
- self.new(rsa_pkey(pkey_params), kid)
73
+ new(rsa_pkey(pkey_params), kid: jwk_attributes(jwk_data, :kid)[:kid])
67
74
  end
68
75
 
69
76
  private
@@ -79,28 +86,44 @@ module JWT
79
86
  def rsa_pkey(rsa_parameters)
80
87
  raise JWT::JWKError, 'Key format is invalid for RSA' unless rsa_parameters[:n] && rsa_parameters[:e]
81
88
 
82
- populate_key(OpenSSL::PKey::RSA.new, rsa_parameters)
89
+ create_rsa_key(rsa_parameters)
83
90
  end
84
91
 
85
- if OpenSSL::PKey::RSA.new.respond_to?(:set_key)
86
- def populate_key(rsa_key, rsa_parameters)
87
- rsa_key.set_key(rsa_parameters[:n], rsa_parameters[:e], rsa_parameters[:d])
88
- rsa_key.set_factors(rsa_parameters[:p], rsa_parameters[:q]) if rsa_parameters[:p] && rsa_parameters[:q]
89
- 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
92
+ if ::JWT.openssl_3?
93
+ ASN1_SEQUENCE = %i[n e d p q dp dq qi].freeze
94
+ def create_rsa_key(rsa_parameters)
95
+ sequence = ASN1_SEQUENCE.each_with_object([]) do |key, arr|
96
+ next if rsa_parameters[key].nil?
97
+
98
+ arr << OpenSSL::ASN1::Integer.new(rsa_parameters[key])
99
+ end
100
+
101
+ if sequence.size > 2 # For a private key
102
+ sequence.unshift(OpenSSL::ASN1::Integer.new(0))
103
+ end
104
+
105
+ OpenSSL::PKey::RSA.new(OpenSSL::ASN1::Sequence(sequence).to_der)
106
+ end
107
+ elsif OpenSSL::PKey::RSA.new.respond_to?(:set_key)
108
+ def create_rsa_key(rsa_parameters)
109
+ OpenSSL::PKey::RSA.new.tap do |rsa_key|
110
+ rsa_key.set_key(rsa_parameters[:n], rsa_parameters[:e], rsa_parameters[:d])
111
+ rsa_key.set_factors(rsa_parameters[:p], rsa_parameters[:q]) if rsa_parameters[:p] && rsa_parameters[:q]
112
+ rsa_key.set_crt_params(rsa_parameters[:dp], rsa_parameters[:dq], rsa_parameters[:qi]) if rsa_parameters[:dp] && rsa_parameters[:dq] && rsa_parameters[:qi]
113
+ end
91
114
  end
92
115
  else
93
- def populate_key(rsa_key, rsa_parameters)
94
- rsa_key.n = rsa_parameters[:n]
95
- rsa_key.e = rsa_parameters[:e]
96
- rsa_key.d = rsa_parameters[:d] if rsa_parameters[:d]
97
- rsa_key.p = rsa_parameters[:p] if rsa_parameters[:p]
98
- rsa_key.q = rsa_parameters[:q] if rsa_parameters[:q]
99
- rsa_key.dmp1 = rsa_parameters[:dp] if rsa_parameters[:dp]
100
- rsa_key.dmq1 = rsa_parameters[:dq] if rsa_parameters[:dq]
101
- rsa_key.iqmp = rsa_parameters[:qi] if rsa_parameters[:qi]
102
-
103
- rsa_key
116
+ def create_rsa_key(rsa_parameters) # rubocop:disable Metrics/AbcSize
117
+ OpenSSL::PKey::RSA.new.tap do |rsa_key|
118
+ rsa_key.n = rsa_parameters[:n]
119
+ rsa_key.e = rsa_parameters[:e]
120
+ rsa_key.d = rsa_parameters[:d] if rsa_parameters[:d]
121
+ rsa_key.p = rsa_parameters[:p] if rsa_parameters[:p]
122
+ rsa_key.q = rsa_parameters[:q] if rsa_parameters[:q]
123
+ rsa_key.dmp1 = rsa_parameters[:dp] if rsa_parameters[:dp]
124
+ rsa_key.dmq1 = rsa_parameters[:dq] if rsa_parameters[:dq]
125
+ rsa_key.iqmp = rsa_parameters[:qi] if rsa_parameters[:qi]
126
+ end
104
127
  end
105
128
  end
106
129
 
@@ -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
@@ -36,6 +36,7 @@ module JWT
36
36
  def generate_mappings
37
37
  classes.each_with_object({}) do |klass, hash|
38
38
  next unless klass.const_defined?('KTYS')
39
+
39
40
  Array(klass::KTYS).each do |kty|
40
41
  hash[kty] = klass
41
42
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JWT
2
4
  # Collection of security methods
3
5
  #