jwt 2.3.0 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
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
  #