jwt 2.1.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 (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