jwt 2.1.0 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +5 -5
  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 +3 -1
  6. data/.reek.yml +21 -39
  7. data/.rspec +1 -0
  8. data/.rubocop.yml +21 -52
  9. data/{.ebert.yml → .sourcelevel.yml} +3 -4
  10. data/AUTHORS +119 -0
  11. data/Appraisals +13 -0
  12. data/CHANGELOG.md +329 -19
  13. data/CODE_OF_CONDUCT.md +84 -0
  14. data/CONTRIBUTING.md +99 -0
  15. data/Gemfile +4 -0
  16. data/README.md +261 -100
  17. data/Rakefile +6 -1
  18. data/lib/jwt/algos/ecdsa.rb +37 -8
  19. data/lib/jwt/algos/eddsa.rb +16 -4
  20. data/lib/jwt/algos/hmac.rb +3 -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 +4 -1
  24. data/lib/jwt/algos/unsupported.rb +7 -4
  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 +120 -24
  33. data/lib/jwt/encode.rb +43 -25
  34. data/lib/jwt/error.rb +6 -0
  35. data/lib/jwt/json.rb +18 -0
  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 +8 -0
  45. data/lib/jwt/signature.rb +7 -22
  46. data/lib/jwt/verify.rb +19 -8
  47. data/lib/jwt/version.rb +6 -2
  48. data/lib/jwt/x5c_key_finder.rb +55 -0
  49. data/lib/jwt.rb +12 -44
  50. data/ruby-jwt.gemspec +13 -9
  51. metadata +44 -97
  52. data/.travis.yml +0 -14
  53. data/Manifest +0 -8
  54. data/lib/jwt/default_options.rb +0 -15
  55. data/spec/fixtures/certs/ec256-private.pem +0 -8
  56. data/spec/fixtures/certs/ec256-public.pem +0 -4
  57. data/spec/fixtures/certs/ec256-wrong-private.pem +0 -8
  58. data/spec/fixtures/certs/ec256-wrong-public.pem +0 -4
  59. data/spec/fixtures/certs/ec384-private.pem +0 -9
  60. data/spec/fixtures/certs/ec384-public.pem +0 -5
  61. data/spec/fixtures/certs/ec384-wrong-private.pem +0 -9
  62. data/spec/fixtures/certs/ec384-wrong-public.pem +0 -5
  63. data/spec/fixtures/certs/ec512-private.pem +0 -10
  64. data/spec/fixtures/certs/ec512-public.pem +0 -6
  65. data/spec/fixtures/certs/ec512-wrong-private.pem +0 -10
  66. data/spec/fixtures/certs/ec512-wrong-public.pem +0 -6
  67. data/spec/fixtures/certs/rsa-1024-private.pem +0 -15
  68. data/spec/fixtures/certs/rsa-1024-public.pem +0 -6
  69. data/spec/fixtures/certs/rsa-2048-private.pem +0 -27
  70. data/spec/fixtures/certs/rsa-2048-public.pem +0 -9
  71. data/spec/fixtures/certs/rsa-2048-wrong-private.pem +0 -27
  72. data/spec/fixtures/certs/rsa-2048-wrong-public.pem +0 -9
  73. data/spec/fixtures/certs/rsa-4096-private.pem +0 -51
  74. data/spec/fixtures/certs/rsa-4096-public.pem +0 -14
  75. data/spec/integration/readme_examples_spec.rb +0 -202
  76. data/spec/jwt/verify_spec.rb +0 -232
  77. data/spec/jwt_spec.rb +0 -315
  78. data/spec/spec_helper.rb +0 -28
data/lib/jwt/encode.rb CHANGED
@@ -1,51 +1,69 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
3
+ require_relative './algos'
4
+ require_relative './claims_validator'
4
5
 
5
6
  # JWT::Encode module
6
7
  module JWT
7
8
  # Encoding logic for JWT
8
9
  class Encode
9
- attr_reader :payload, :key, :algorithm, :header_fields, :segments
10
+ ALG_NONE = 'none'
11
+ ALG_KEY = 'alg'
10
12
 
11
- def self.base64url_encode(str)
12
- Base64.encode64(str).tr('+/', '-_').gsub(/[\n=]/, '')
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)
13
18
  end
14
19
 
15
- def initialize(payload, key, algorithm, header_fields)
16
- @payload = payload
17
- @key = key
18
- @algorithm = algorithm
19
- @header_fields = header_fields
20
- @segments = encode_segments
20
+ def segments
21
+ @segments ||= combine(encoded_header_and_payload, encoded_signature)
21
22
  end
22
23
 
23
24
  private
24
25
 
25
26
  def encoded_header
26
- header = { 'alg' => @algorithm }.merge(@header_fields)
27
- Encode.base64url_encode(JSON.generate(header))
27
+ @encoded_header ||= encode_header
28
28
  end
29
29
 
30
30
  def encoded_payload
31
- raise InvalidPayload, 'exp claim must be an integer' if @payload && @payload.is_a?(Hash) && @payload.key?('exp') && !@payload['exp'].is_a?(Integer)
32
- Encode.base64url_encode(JSON.generate(@payload))
31
+ @encoded_payload ||= encode_payload
33
32
  end
34
33
 
35
- def encoded_signature(signing_input)
36
- if @algorithm == 'none'
37
- ''
38
- else
39
- signature = JWT::Signature.sign(@algorithm, signing_input, @key)
40
- Encode.base64url_encode(signature)
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!
41
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))
42
63
  end
43
64
 
44
- def encode_segments
45
- header = encoded_header
46
- payload = encoded_payload
47
- signature = encoded_signature([header, payload].join('.'))
48
- [header, payload, signature].join('.')
65
+ def combine(*parts)
66
+ parts.join('.')
49
67
  end
50
68
  end
51
69
  end
data/lib/jwt/error.rb CHANGED
@@ -3,14 +3,20 @@
3
3
  module JWT
4
4
  class EncodeError < StandardError; end
5
5
  class DecodeError < StandardError; end
6
+ class RequiredDependencyError < StandardError; end
7
+
6
8
  class VerificationError < DecodeError; end
7
9
  class ExpiredSignature < DecodeError; end
8
10
  class IncorrectAlgorithm < DecodeError; end
9
11
  class ImmatureSignature < DecodeError; end
10
12
  class InvalidIssuerError < DecodeError; end
13
+ class UnsupportedEcdsaCurve < IncorrectAlgorithm; end
11
14
  class InvalidIatError < DecodeError; end
12
15
  class InvalidAudError < DecodeError; end
13
16
  class InvalidSubError < DecodeError; end
14
17
  class InvalidJtiError < DecodeError; end
15
18
  class InvalidPayload < DecodeError; end
19
+ class MissingRequiredClaim < DecodeError; end
20
+
21
+ class JWKError < DecodeError; end
16
22
  end
data/lib/jwt/json.rb ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module JWT
6
+ # JSON wrapper
7
+ class JSON
8
+ class << self
9
+ def generate(data)
10
+ ::JSON.generate(data)
11
+ end
12
+
13
+ def parse(data)
14
+ ::JSON.parse(data)
15
+ end
16
+ end
17
+ end
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