jwt 1.5.4 → 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 (76) hide show
  1. checksums.yaml +5 -13
  2. data/.codeclimate.yml +6 -18
  3. data/.github/workflows/coverage.yml +27 -0
  4. data/.github/workflows/test.yml +67 -0
  5. data/.gitignore +7 -0
  6. data/.reek.yml +22 -0
  7. data/.rspec +1 -1
  8. data/.rubocop.yml +66 -1
  9. data/.sourcelevel.yml +17 -0
  10. data/AUTHORS +119 -0
  11. data/Appraisals +13 -0
  12. data/CHANGELOG.md +786 -0
  13. data/CODE_OF_CONDUCT.md +84 -0
  14. data/CONTRIBUTING.md +99 -0
  15. data/Gemfile +4 -1
  16. data/README.md +332 -79
  17. data/Rakefile +15 -0
  18. data/lib/jwt/algos/ecdsa.rb +64 -0
  19. data/lib/jwt/algos/eddsa.rb +35 -0
  20. data/lib/jwt/algos/hmac.rb +36 -0
  21. data/lib/jwt/algos/none.rb +17 -0
  22. data/lib/jwt/algos/ps.rb +43 -0
  23. data/lib/jwt/algos/rsa.rb +22 -0
  24. data/lib/jwt/algos/unsupported.rb +19 -0
  25. data/lib/jwt/algos.rb +44 -0
  26. data/lib/jwt/base64.rb +19 -0
  27. data/lib/jwt/claims_validator.rb +37 -0
  28. data/lib/jwt/configuration/container.rb +21 -0
  29. data/lib/jwt/configuration/decode_configuration.rb +46 -0
  30. data/lib/jwt/configuration/jwk_configuration.rb +27 -0
  31. data/lib/jwt/configuration.rb +15 -0
  32. data/lib/jwt/decode.rb +119 -30
  33. data/lib/jwt/encode.rb +69 -0
  34. data/lib/jwt/error.rb +10 -0
  35. data/lib/jwt/json.rb +11 -9
  36. data/lib/jwt/jwk/ec.rb +199 -0
  37. data/lib/jwt/jwk/hmac.rb +67 -0
  38. data/lib/jwt/jwk/key_base.rb +35 -0
  39. data/lib/jwt/jwk/key_finder.rb +62 -0
  40. data/lib/jwt/jwk/kid_as_key_digest.rb +15 -0
  41. data/lib/jwt/jwk/rsa.rb +138 -0
  42. data/lib/jwt/jwk/thumbprint.rb +26 -0
  43. data/lib/jwt/jwk.rb +52 -0
  44. data/lib/jwt/security_utils.rb +59 -0
  45. data/lib/jwt/signature.rb +35 -0
  46. data/lib/jwt/verify.rb +59 -44
  47. data/lib/jwt/version.rb +8 -3
  48. data/lib/jwt/x5c_key_finder.rb +55 -0
  49. data/lib/jwt.rb +16 -162
  50. data/ruby-jwt.gemspec +14 -8
  51. metadata +71 -84
  52. data/.travis.yml +0 -13
  53. data/Manifest +0 -8
  54. data/spec/fixtures/certs/ec256-private.pem +0 -8
  55. data/spec/fixtures/certs/ec256-public.pem +0 -4
  56. data/spec/fixtures/certs/ec256-wrong-private.pem +0 -8
  57. data/spec/fixtures/certs/ec256-wrong-public.pem +0 -4
  58. data/spec/fixtures/certs/ec384-private.pem +0 -9
  59. data/spec/fixtures/certs/ec384-public.pem +0 -5
  60. data/spec/fixtures/certs/ec384-wrong-private.pem +0 -9
  61. data/spec/fixtures/certs/ec384-wrong-public.pem +0 -5
  62. data/spec/fixtures/certs/ec512-private.pem +0 -10
  63. data/spec/fixtures/certs/ec512-public.pem +0 -6
  64. data/spec/fixtures/certs/ec512-wrong-private.pem +0 -10
  65. data/spec/fixtures/certs/ec512-wrong-public.pem +0 -6
  66. data/spec/fixtures/certs/rsa-1024-private.pem +0 -15
  67. data/spec/fixtures/certs/rsa-1024-public.pem +0 -6
  68. data/spec/fixtures/certs/rsa-2048-private.pem +0 -27
  69. data/spec/fixtures/certs/rsa-2048-public.pem +0 -9
  70. data/spec/fixtures/certs/rsa-2048-wrong-private.pem +0 -27
  71. data/spec/fixtures/certs/rsa-2048-wrong-public.pem +0 -9
  72. data/spec/fixtures/certs/rsa-4096-private.pem +0 -51
  73. data/spec/fixtures/certs/rsa-4096-public.pem +0 -14
  74. data/spec/jwt/verify_spec.rb +0 -175
  75. data/spec/jwt_spec.rb +0 -232
  76. data/spec/spec_helper.rb +0 -31
data/lib/jwt/encode.rb ADDED
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './algos'
4
+ require_relative './claims_validator'
5
+
6
+ # JWT::Encode module
7
+ module JWT
8
+ # Encoding logic for JWT
9
+ class Encode
10
+ ALG_NONE = 'none'
11
+ ALG_KEY = 'alg'
12
+
13
+ def initialize(options)
14
+ @payload = options[:payload]
15
+ @key = options[:key]
16
+ _, @algorithm = Algos.find(options[:algorithm])
17
+ @headers = options[:headers].transform_keys(&:to_s)
18
+ end
19
+
20
+ def segments
21
+ @segments ||= combine(encoded_header_and_payload, encoded_signature)
22
+ end
23
+
24
+ private
25
+
26
+ def encoded_header
27
+ @encoded_header ||= encode_header
28
+ end
29
+
30
+ def encoded_payload
31
+ @encoded_payload ||= encode_payload
32
+ end
33
+
34
+ def encoded_signature
35
+ @encoded_signature ||= encode_signature
36
+ end
37
+
38
+ def encoded_header_and_payload
39
+ @encoded_header_and_payload ||= combine(encoded_header, encoded_payload)
40
+ end
41
+
42
+ def encode_header
43
+ @headers[ALG_KEY] = @algorithm
44
+ encode(@headers)
45
+ end
46
+
47
+ def encode_payload
48
+ if @payload.is_a?(Hash)
49
+ ClaimsValidator.new(@payload).validate!
50
+ end
51
+
52
+ encode(@payload)
53
+ end
54
+
55
+ def encode_signature
56
+ return '' if @algorithm == ALG_NONE
57
+
58
+ ::JWT::Base64.url_encode(JWT::Signature.sign(@algorithm, encoded_header_and_payload, @key))
59
+ end
60
+
61
+ def encode(data)
62
+ ::JWT::Base64.url_encode(JWT::JSON.generate(data))
63
+ end
64
+
65
+ def combine(*parts)
66
+ parts.join('.')
67
+ end
68
+ end
69
+ end
data/lib/jwt/error.rb CHANGED
@@ -1,12 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JWT
4
+ class EncodeError < StandardError; end
2
5
  class DecodeError < StandardError; end
6
+ class RequiredDependencyError < StandardError; end
7
+
3
8
  class VerificationError < DecodeError; end
4
9
  class ExpiredSignature < DecodeError; end
5
10
  class IncorrectAlgorithm < DecodeError; end
6
11
  class ImmatureSignature < DecodeError; end
7
12
  class InvalidIssuerError < DecodeError; end
13
+ class UnsupportedEcdsaCurve < IncorrectAlgorithm; end
8
14
  class InvalidIatError < DecodeError; end
9
15
  class InvalidAudError < DecodeError; end
10
16
  class InvalidSubError < DecodeError; end
11
17
  class InvalidJtiError < DecodeError; end
18
+ class InvalidPayload < DecodeError; end
19
+ class MissingRequiredClaim < DecodeError; end
20
+
21
+ class JWKError < DecodeError; end
12
22
  end
data/lib/jwt/json.rb CHANGED
@@ -1,16 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'json'
2
4
 
3
5
  module JWT
4
- # JSON fallback implementation or ruby 1.8.x
5
- module Json
6
- def decode_json(encoded)
7
- JSON.parse(encoded)
8
- rescue JSON::ParserError
9
- raise JWT::DecodeError, 'Invalid segment encoding'
10
- end
6
+ # JSON wrapper
7
+ class JSON
8
+ class << self
9
+ def generate(data)
10
+ ::JSON.generate(data)
11
+ end
11
12
 
12
- def encode_json(raw)
13
- JSON.generate(raw)
13
+ def parse(data)
14
+ ::JSON.parse(data)
15
+ end
14
16
  end
15
17
  end
16
18
  end
data/lib/jwt/jwk/ec.rb ADDED
@@ -0,0 +1,199 @@
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].freeze
13
+ BINARY = 2
14
+
15
+ attr_reader :keypair
16
+
17
+ def initialize(keypair, options = {})
18
+ raise ArgumentError, 'keypair must be of type OpenSSL::PKey::EC' unless keypair.is_a?(OpenSSL::PKey::EC)
19
+
20
+ @keypair = keypair
21
+
22
+ super(options)
23
+ end
24
+
25
+ def private?
26
+ @keypair.private_key?
27
+ end
28
+
29
+ def members
30
+ crv, x_octets, y_octets = keypair_components(keypair)
31
+ {
32
+ kty: KTY,
33
+ crv: crv,
34
+ x: encode_octets(x_octets),
35
+ y: encode_octets(y_octets)
36
+ }
37
+ end
38
+
39
+ def export(options = {})
40
+ exported_hash = members.merge(kid: kid)
41
+
42
+ return exported_hash unless private? && options[:include_private] == true
43
+
44
+ append_private_parts(exported_hash)
45
+ end
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
+
54
+ private
55
+
56
+ def append_private_parts(the_hash)
57
+ octets = keypair.private_key.to_bn.to_s(BINARY)
58
+ the_hash.merge(
59
+ d: encode_octets(octets)
60
+ )
61
+ end
62
+
63
+ def keypair_components(ec_keypair)
64
+ encoded_point = ec_keypair.public_key.to_bn.to_s(BINARY)
65
+ case ec_keypair.group.curve_name
66
+ when 'prime256v1'
67
+ crv = 'P-256'
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')
72
+ when 'secp384r1'
73
+ crv = 'P-384'
74
+ x_octets, y_octets = encoded_point.unpack('xa48a48')
75
+ when 'secp521r1'
76
+ crv = 'P-521'
77
+ x_octets, y_octets = encoded_point.unpack('xa66a66')
78
+ else
79
+ raise JWT::JWKError, "Unsupported curve '#{ec_keypair.group.curve_name}'"
80
+ end
81
+ [crv, x_octets, y_octets]
82
+ end
83
+
84
+ def encode_octets(octets)
85
+ ::JWT::Base64.url_encode(octets)
86
+ end
87
+
88
+ def encode_open_ssl_bn(key_part)
89
+ ::JWT::Base64.url_encode(key_part.to_s(BINARY))
90
+ end
91
+
92
+ class << self
93
+ def import(jwk_data)
94
+ # See https://tools.ietf.org/html/rfc7518#section-6.2.1 for an
95
+ # explanation of the relevant parameters.
96
+
97
+ jwk_crv, jwk_x, jwk_y, jwk_d, jwk_kid = jwk_attrs(jwk_data, %i[crv x y d kid])
98
+ raise JWT::JWKError, 'Key format is invalid for EC' unless jwk_crv && jwk_x && jwk_y
99
+
100
+ new(ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d), kid: jwk_kid)
101
+ end
102
+
103
+ def to_openssl_curve(crv)
104
+ # The JWK specs and OpenSSL use different names for the same curves.
105
+ # See https://tools.ietf.org/html/rfc5480#section-2.1.1.1 for some
106
+ # pointers on different names for common curves.
107
+ case crv
108
+ when 'P-256' then 'prime256v1'
109
+ when 'P-384' then 'secp384r1'
110
+ when 'P-521' then 'secp521r1'
111
+ when 'P-256K' then 'secp256k1'
112
+ else raise JWT::JWKError, 'Invalid curve provided'
113
+ end
114
+ end
115
+
116
+ private
117
+
118
+ def jwk_attrs(jwk_data, attrs)
119
+ attrs.map do |attr|
120
+ jwk_data[attr] || jwk_data[attr.to_s]
121
+ end
122
+ end
123
+
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
187
+ end
188
+
189
+ def decode_octets(jwk_data)
190
+ ::JWT::Base64.url_decode(jwk_data)
191
+ end
192
+
193
+ def decode_open_ssl_bn(jwk_data)
194
+ OpenSSL::BN.new(::JWT::Base64.url_decode(jwk_data), BINARY)
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWK
5
+ class HMAC < KeyBase
6
+ KTY = 'oct'
7
+ KTYS = [KTY, String].freeze
8
+
9
+ attr_reader :signing_key
10
+
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)
16
+ end
17
+
18
+ def private?
19
+ true
20
+ end
21
+
22
+ def public_key
23
+ nil
24
+ end
25
+
26
+ # See https://tools.ietf.org/html/rfc7517#appendix-A.3
27
+ def export(options = {})
28
+ exported_hash = {
29
+ kty: KTY,
30
+ kid: kid
31
+ }
32
+
33
+ return exported_hash unless private? && options[:include_private] == true
34
+
35
+ exported_hash.merge(
36
+ k: signing_key
37
+ )
38
+ end
39
+
40
+ def members
41
+ {
42
+ kty: KTY,
43
+ k: signing_key
44
+ }
45
+ end
46
+
47
+ alias keypair signing_key # for backwards compatibility
48
+
49
+ def key_digest
50
+ sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::UTF8String.new(signing_key),
51
+ OpenSSL::ASN1::UTF8String.new(KTY)])
52
+ OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
53
+ end
54
+
55
+ class << self
56
+ def import(jwk_data)
57
+ jwk_k = jwk_data[:k] || jwk_data['k']
58
+ jwk_kid = jwk_data[:kid] || jwk_data['kid']
59
+
60
+ raise JWT::JWKError, 'Key format is invalid for HMAC' unless jwk_k
61
+
62
+ new(jwk_k, kid: jwk_kid)
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,35 @@
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)
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
20
+ end
21
+
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
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWK
5
+ class KeyFinder
6
+ def initialize(options)
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)
10
+ end
11
+
12
+ def key_for(kid)
13
+ raise ::JWT::DecodeError, 'No key id (kid) found from token headers' unless kid
14
+
15
+ jwk = resolve_key(kid)
16
+
17
+ raise ::JWT::DecodeError, 'No keys found in jwks' if jwks_keys.empty?
18
+ raise ::JWT::DecodeError, "Could not find public key for kid #{kid}" unless jwk
19
+
20
+ ::JWT::JWK.import(jwk).keypair
21
+ end
22
+
23
+ private
24
+
25
+ def resolve_key(kid)
26
+ jwk = find_key(kid)
27
+
28
+ return jwk if jwk
29
+
30
+ if reloadable?
31
+ load_keys(invalidate: true, kid_not_found: true, kid: kid) # invalidate for backwards compatibility
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
59
+ end
60
+ end
61
+ end
62
+ 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
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWK
5
+ class RSA < KeyBase
6
+ BINARY = 2
7
+ KTY = 'RSA'
8
+ KTYS = [KTY, OpenSSL::PKey::RSA].freeze
9
+ RSA_KEY_ELEMENTS = %i[n e d p q dp dq qi].freeze
10
+
11
+ attr_reader :keypair
12
+
13
+ def initialize(keypair, options = {})
14
+ raise ArgumentError, 'keypair must be of type OpenSSL::PKey::RSA' unless keypair.is_a?(OpenSSL::PKey::RSA)
15
+
16
+ @keypair = keypair
17
+
18
+ super(options)
19
+ end
20
+
21
+ def private?
22
+ keypair.private?
23
+ end
24
+
25
+ def public_key
26
+ keypair.public_key
27
+ end
28
+
29
+ def export(options = {})
30
+ exported_hash = members.merge(kid: kid)
31
+
32
+ return exported_hash unless private? && options[:include_private] == true
33
+
34
+ append_private_parts(exported_hash)
35
+ end
36
+
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
44
+
45
+ def key_digest
46
+ sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(public_key.n),
47
+ OpenSSL::ASN1::Integer.new(public_key.e)])
48
+ OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
49
+ end
50
+
51
+ private
52
+
53
+ def append_private_parts(the_hash)
54
+ the_hash.merge(
55
+ d: encode_open_ssl_bn(keypair.d),
56
+ p: encode_open_ssl_bn(keypair.p),
57
+ q: encode_open_ssl_bn(keypair.q),
58
+ dp: encode_open_ssl_bn(keypair.dmp1),
59
+ dq: encode_open_ssl_bn(keypair.dmq1),
60
+ qi: encode_open_ssl_bn(keypair.iqmp)
61
+ )
62
+ end
63
+
64
+ def encode_open_ssl_bn(key_part)
65
+ ::JWT::Base64.url_encode(key_part.to_s(BINARY))
66
+ end
67
+
68
+ class << self
69
+ def import(jwk_data)
70
+ pkey_params = jwk_attributes(jwk_data, *RSA_KEY_ELEMENTS) do |value|
71
+ decode_open_ssl_bn(value)
72
+ end
73
+ new(rsa_pkey(pkey_params), kid: jwk_attributes(jwk_data, :kid)[:kid])
74
+ end
75
+
76
+ private
77
+
78
+ def jwk_attributes(jwk_data, *attributes)
79
+ attributes.each_with_object({}) do |attribute, hash|
80
+ value = jwk_data[attribute] || jwk_data[attribute.to_s]
81
+ value = yield(value) if block_given?
82
+ hash[attribute] = value
83
+ end
84
+ end
85
+
86
+ def rsa_pkey(rsa_parameters)
87
+ raise JWT::JWKError, 'Key format is invalid for RSA' unless rsa_parameters[:n] && rsa_parameters[:e]
88
+
89
+ create_rsa_key(rsa_parameters)
90
+ end
91
+
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
114
+ end
115
+ else
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
127
+ end
128
+ end
129
+
130
+ def decode_open_ssl_bn(jwk_data)
131
+ return nil unless jwk_data
132
+
133
+ OpenSSL::BN.new(::JWT::Base64.url_decode(jwk_data), BINARY)
134
+ end
135
+ end
136
+ end
137
+ end
138
+ 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