jwt 2.2.2 → 2.10.2

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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/AUTHORS +79 -44
  3. data/CHANGELOG.md +299 -5
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/CONTRIBUTING.md +99 -0
  6. data/README.md +416 -107
  7. data/lib/jwt/base64.rb +19 -2
  8. data/lib/jwt/claims/audience.rb +30 -0
  9. data/lib/jwt/claims/crit.rb +35 -0
  10. data/lib/jwt/claims/decode_verifier.rb +40 -0
  11. data/lib/jwt/claims/expiration.rb +32 -0
  12. data/lib/jwt/claims/issued_at.rb +22 -0
  13. data/lib/jwt/claims/issuer.rb +34 -0
  14. data/lib/jwt/claims/jwt_id.rb +35 -0
  15. data/lib/jwt/claims/not_before.rb +32 -0
  16. data/lib/jwt/claims/numeric.rb +77 -0
  17. data/lib/jwt/claims/required.rb +33 -0
  18. data/lib/jwt/claims/subject.rb +30 -0
  19. data/lib/jwt/claims/verification_methods.rb +20 -0
  20. data/lib/jwt/claims/verifier.rb +61 -0
  21. data/lib/jwt/claims.rb +74 -0
  22. data/lib/jwt/claims_validator.rb +7 -22
  23. data/lib/jwt/configuration/container.rb +52 -0
  24. data/lib/jwt/configuration/decode_configuration.rb +70 -0
  25. data/lib/jwt/configuration/jwk_configuration.rb +28 -0
  26. data/lib/jwt/configuration.rb +23 -0
  27. data/lib/jwt/decode.rb +70 -57
  28. data/lib/jwt/deprecations.rb +49 -0
  29. data/lib/jwt/encode.rb +16 -54
  30. data/lib/jwt/encoded_token.rb +139 -0
  31. data/lib/jwt/error.rb +37 -0
  32. data/lib/jwt/json.rb +1 -1
  33. data/lib/jwt/jwa/compat.rb +32 -0
  34. data/lib/jwt/jwa/ecdsa.rb +90 -0
  35. data/lib/jwt/jwa/eddsa.rb +35 -0
  36. data/lib/jwt/jwa/hmac.rb +82 -0
  37. data/lib/jwt/jwa/hmac_rbnacl.rb +50 -0
  38. data/lib/jwt/jwa/hmac_rbnacl_fixed.rb +47 -0
  39. data/lib/jwt/jwa/none.rb +24 -0
  40. data/lib/jwt/jwa/ps.rb +35 -0
  41. data/lib/jwt/jwa/rsa.rb +35 -0
  42. data/lib/jwt/jwa/signing_algorithm.rb +63 -0
  43. data/lib/jwt/jwa/unsupported.rb +20 -0
  44. data/lib/jwt/jwa/wrapper.rb +44 -0
  45. data/lib/jwt/jwa.rb +58 -0
  46. data/lib/jwt/jwk/ec.rb +250 -0
  47. data/lib/jwt/jwk/hmac.rb +102 -0
  48. data/lib/jwt/jwk/key_base.rb +58 -0
  49. data/lib/jwt/jwk/key_finder.rb +20 -30
  50. data/lib/jwt/jwk/kid_as_key_digest.rb +16 -0
  51. data/lib/jwt/jwk/okp_rbnacl.rb +109 -0
  52. data/lib/jwt/jwk/rsa.rb +174 -26
  53. data/lib/jwt/jwk/set.rb +82 -0
  54. data/lib/jwt/jwk/thumbprint.rb +26 -0
  55. data/lib/jwt/jwk.rb +40 -15
  56. data/lib/jwt/token.rb +112 -0
  57. data/lib/jwt/verify.rb +16 -74
  58. data/lib/jwt/version.rb +52 -10
  59. data/lib/jwt/x5c_key_finder.rb +52 -0
  60. data/lib/jwt.rb +28 -4
  61. data/ruby-jwt.gemspec +20 -11
  62. metadata +61 -63
  63. data/.codeclimate.yml +0 -20
  64. data/.ebert.yml +0 -18
  65. data/.gitignore +0 -11
  66. data/.rspec +0 -1
  67. data/.rubocop.yml +0 -98
  68. data/.travis.yml +0 -29
  69. data/Appraisals +0 -18
  70. data/Gemfile +0 -3
  71. data/Rakefile +0 -11
  72. data/lib/jwt/algos/ecdsa.rb +0 -35
  73. data/lib/jwt/algos/eddsa.rb +0 -23
  74. data/lib/jwt/algos/hmac.rb +0 -34
  75. data/lib/jwt/algos/ps.rb +0 -43
  76. data/lib/jwt/algos/rsa.rb +0 -19
  77. data/lib/jwt/algos/unsupported.rb +0 -16
  78. data/lib/jwt/default_options.rb +0 -15
  79. data/lib/jwt/security_utils.rb +0 -57
  80. data/lib/jwt/signature.rb +0 -54
data/lib/jwt/jwk/ec.rb ADDED
@@ -0,0 +1,250 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module JWT
6
+ module JWK
7
+ # JWK representation for Elliptic Curve (EC) keys
8
+ class EC < KeyBase # rubocop:disable Metrics/ClassLength
9
+ KTY = 'EC'
10
+ KTYS = [KTY, OpenSSL::PKey::EC, JWT::JWK::EC].freeze
11
+ BINARY = 2
12
+ EC_PUBLIC_KEY_ELEMENTS = %i[kty crv x y].freeze
13
+ EC_PRIVATE_KEY_ELEMENTS = %i[d].freeze
14
+ EC_KEY_ELEMENTS = (EC_PRIVATE_KEY_ELEMENTS + EC_PUBLIC_KEY_ELEMENTS).freeze
15
+ ZERO_BYTE = "\0".b.freeze
16
+
17
+ def initialize(key, params = nil, options = {})
18
+ params ||= {}
19
+
20
+ # For backwards compatibility when kid was a String
21
+ params = { kid: params } if params.is_a?(String)
22
+
23
+ key_params = extract_key_params(key)
24
+
25
+ params = params.transform_keys(&:to_sym)
26
+ check_jwk_params!(key_params, params)
27
+
28
+ super(options, key_params.merge(params))
29
+ end
30
+
31
+ def keypair
32
+ ec_key
33
+ end
34
+
35
+ def private?
36
+ ec_key.private_key?
37
+ end
38
+
39
+ def signing_key
40
+ ec_key
41
+ end
42
+
43
+ def verify_key
44
+ ec_key
45
+ end
46
+
47
+ def public_key
48
+ ec_key
49
+ end
50
+
51
+ def members
52
+ EC_PUBLIC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] }
53
+ end
54
+
55
+ def export(options = {})
56
+ exported = parameters.clone
57
+ exported.reject! { |k, _| EC_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true
58
+ exported
59
+ end
60
+
61
+ def key_digest
62
+ _crv, x_octets, y_octets = keypair_components(ec_key)
63
+ sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(x_octets, BINARY)),
64
+ OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(y_octets, BINARY))])
65
+ OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
66
+ end
67
+
68
+ def []=(key, value)
69
+ raise ArgumentError, 'cannot overwrite cryptographic key attributes' if EC_KEY_ELEMENTS.include?(key.to_sym)
70
+
71
+ super(key, value)
72
+ end
73
+
74
+ private
75
+
76
+ def ec_key
77
+ @ec_key ||= create_ec_key(self[:crv], self[:x], self[:y], self[:d])
78
+ end
79
+
80
+ def extract_key_params(key)
81
+ case key
82
+ when JWT::JWK::EC
83
+ key.export(include_private: true)
84
+ when OpenSSL::PKey::EC # Accept OpenSSL key as input
85
+ @ec_key = key # Preserve the object to avoid recreation
86
+ parse_ec_key(key)
87
+ when Hash
88
+ key.transform_keys(&:to_sym)
89
+ else
90
+ raise ArgumentError, 'key must be of type OpenSSL::PKey::EC or Hash with key parameters'
91
+ end
92
+ end
93
+
94
+ def check_jwk_params!(key_params, params)
95
+ raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (EC_KEY_ELEMENTS & params.keys).empty?
96
+ raise JWT::JWKError, "Incorrect 'kty' value: #{key_params[:kty]}, expected #{KTY}" unless key_params[:kty] == KTY
97
+ raise JWT::JWKError, 'Key format is invalid for EC' unless key_params[:crv] && key_params[:x] && key_params[:y]
98
+ end
99
+
100
+ def keypair_components(ec_keypair)
101
+ encoded_point = ec_keypair.public_key.to_bn.to_s(BINARY)
102
+ case ec_keypair.group.curve_name
103
+ when 'prime256v1'
104
+ crv = 'P-256'
105
+ x_octets, y_octets = encoded_point.unpack('xa32a32')
106
+ when 'secp256k1'
107
+ crv = 'P-256K'
108
+ x_octets, y_octets = encoded_point.unpack('xa32a32')
109
+ when 'secp384r1'
110
+ crv = 'P-384'
111
+ x_octets, y_octets = encoded_point.unpack('xa48a48')
112
+ when 'secp521r1'
113
+ crv = 'P-521'
114
+ x_octets, y_octets = encoded_point.unpack('xa66a66')
115
+ else
116
+ raise JWT::JWKError, "Unsupported curve '#{ec_keypair.group.curve_name}'"
117
+ end
118
+ [crv, x_octets, y_octets]
119
+ end
120
+
121
+ def encode_octets(octets)
122
+ return unless octets
123
+
124
+ ::JWT::Base64.url_encode(octets)
125
+ end
126
+
127
+ def encode_open_ssl_bn(key_part)
128
+ ::JWT::Base64.url_encode(key_part.to_s(BINARY))
129
+ end
130
+
131
+ def parse_ec_key(key)
132
+ crv, x_octets, y_octets = keypair_components(key)
133
+ octets = key.private_key&.to_bn&.to_s(BINARY)
134
+ {
135
+ kty: KTY,
136
+ crv: crv,
137
+ x: encode_octets(x_octets),
138
+ y: encode_octets(y_octets),
139
+ d: encode_octets(octets)
140
+ }.compact
141
+ end
142
+
143
+ if ::JWT.openssl_3?
144
+ def create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d) # rubocop:disable Metrics/MethodLength
145
+ curve = EC.to_openssl_curve(jwk_crv)
146
+ x_octets = decode_octets(jwk_x)
147
+ y_octets = decode_octets(jwk_y)
148
+
149
+ point = OpenSSL::PKey::EC::Point.new(
150
+ OpenSSL::PKey::EC::Group.new(curve),
151
+ OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2)
152
+ )
153
+
154
+ sequence = if jwk_d
155
+ # https://datatracker.ietf.org/doc/html/rfc5915.html
156
+ # ECPrivateKey ::= SEQUENCE {
157
+ # version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1),
158
+ # privateKey OCTET STRING,
159
+ # parameters [0] ECParameters {{ NamedCurve }} OPTIONAL,
160
+ # publicKey [1] BIT STRING OPTIONAL
161
+ # }
162
+
163
+ OpenSSL::ASN1::Sequence([
164
+ OpenSSL::ASN1::Integer(1),
165
+ OpenSSL::ASN1::OctetString(OpenSSL::BN.new(decode_octets(jwk_d), 2).to_s(2)),
166
+ OpenSSL::ASN1::ObjectId(curve, 0, :EXPLICIT),
167
+ OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed), 1, :EXPLICIT)
168
+ ])
169
+ else
170
+ OpenSSL::ASN1::Sequence([
171
+ OpenSSL::ASN1::Sequence([OpenSSL::ASN1::ObjectId('id-ecPublicKey'), OpenSSL::ASN1::ObjectId(curve)]),
172
+ OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed))
173
+ ])
174
+ end
175
+
176
+ OpenSSL::PKey::EC.new(sequence.to_der)
177
+ end
178
+ else
179
+ def create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d)
180
+ curve = EC.to_openssl_curve(jwk_crv)
181
+
182
+ x_octets = decode_octets(jwk_x)
183
+ y_octets = decode_octets(jwk_y)
184
+
185
+ key = OpenSSL::PKey::EC.new(curve)
186
+
187
+ # The details of the `Point` instantiation are covered in:
188
+ # - https://docs.ruby-lang.org/en/2.4.0/OpenSSL/PKey/EC.html
189
+ # - https://www.openssl.org/docs/manmaster/man3/EC_POINT_new.html
190
+ # - https://tools.ietf.org/html/rfc5480#section-2.2
191
+ # - https://www.secg.org/SEC1-Ver-1.0.pdf
192
+ # Section 2.3.3 of the last of these references specifies that the
193
+ # encoding of an uncompressed point consists of the byte `0x04` followed
194
+ # by the x value then the y value.
195
+ point = OpenSSL::PKey::EC::Point.new(
196
+ OpenSSL::PKey::EC::Group.new(curve),
197
+ OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2)
198
+ )
199
+
200
+ key.public_key = point
201
+ key.private_key = OpenSSL::BN.new(decode_octets(jwk_d), 2) if jwk_d
202
+
203
+ key
204
+ end
205
+ end
206
+
207
+ def decode_octets(base64_encoded_coordinate)
208
+ bytes = ::JWT::Base64.url_decode(base64_encoded_coordinate)
209
+ # Some base64 encoders on some platform omit a single 0-byte at
210
+ # the start of either Y or X coordinate of the elliptic curve point.
211
+ # This leads to an encoding error when data is passed to OpenSSL BN.
212
+ # It is know to have happend to exported JWKs on a Java application and
213
+ # on a Flutter/Dart application (both iOS and Android). All that is
214
+ # needed to fix the problem is adding a leading 0-byte. We know the
215
+ # required byte is 0 because with any other byte the point is no longer
216
+ # on the curve - and OpenSSL will actually communicate this via another
217
+ # exception. The indication of a stripped byte will be the fact that the
218
+ # coordinates - once decoded into bytes - should always be an even
219
+ # bytesize. For example, with a P-521 curve, both x and y must be 66 bytes.
220
+ # With a P-256 curve, both x and y must be 32 and so on. The simplest way
221
+ # to check for this truncation is thus to check whether the number of bytes
222
+ # is odd, and restore the leading 0-byte if it is.
223
+ if bytes.bytesize.odd?
224
+ ZERO_BYTE + bytes
225
+ else
226
+ bytes
227
+ end
228
+ end
229
+
230
+ class << self
231
+ def import(jwk_data)
232
+ new(jwk_data)
233
+ end
234
+
235
+ def to_openssl_curve(crv)
236
+ # The JWK specs and OpenSSL use different names for the same curves.
237
+ # See https://tools.ietf.org/html/rfc5480#section-2.1.1.1 for some
238
+ # pointers on different names for common curves.
239
+ case crv
240
+ when 'P-256' then 'prime256v1'
241
+ when 'P-384' then 'secp384r1'
242
+ when 'P-521' then 'secp521r1'
243
+ when 'P-256K' then 'secp256k1'
244
+ else raise JWT::JWKError, 'Invalid curve provided'
245
+ end
246
+ end
247
+ end
248
+ end
249
+ end
250
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWK
5
+ # JWK for HMAC keys
6
+ class HMAC < KeyBase
7
+ KTY = 'oct'
8
+ KTYS = [KTY, String, JWT::JWK::HMAC].freeze
9
+ HMAC_PUBLIC_KEY_ELEMENTS = %i[kty].freeze
10
+ HMAC_PRIVATE_KEY_ELEMENTS = %i[k].freeze
11
+ HMAC_KEY_ELEMENTS = (HMAC_PRIVATE_KEY_ELEMENTS + HMAC_PUBLIC_KEY_ELEMENTS).freeze
12
+
13
+ def initialize(key, params = nil, options = {})
14
+ params ||= {}
15
+
16
+ # For backwards compatibility when kid was a String
17
+ params = { kid: params } if params.is_a?(String)
18
+
19
+ key_params = extract_key_params(key)
20
+
21
+ params = params.transform_keys(&:to_sym)
22
+ check_jwk(key_params, params)
23
+
24
+ super(options, key_params.merge(params))
25
+ end
26
+
27
+ def keypair
28
+ secret
29
+ end
30
+
31
+ def private?
32
+ true
33
+ end
34
+
35
+ def public_key
36
+ nil
37
+ end
38
+
39
+ def verify_key
40
+ secret
41
+ end
42
+
43
+ def signing_key
44
+ secret
45
+ end
46
+
47
+ # See https://tools.ietf.org/html/rfc7517#appendix-A.3
48
+ def export(options = {})
49
+ exported = parameters.clone
50
+ exported.reject! { |k, _| HMAC_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true
51
+ exported
52
+ end
53
+
54
+ def members
55
+ HMAC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] }
56
+ end
57
+
58
+ def key_digest
59
+ sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::UTF8String.new(signing_key),
60
+ OpenSSL::ASN1::UTF8String.new(KTY)])
61
+ OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
62
+ end
63
+
64
+ def []=(key, value)
65
+ raise ArgumentError, 'cannot overwrite cryptographic key attributes' if HMAC_KEY_ELEMENTS.include?(key.to_sym)
66
+
67
+ super(key, value)
68
+ end
69
+
70
+ private
71
+
72
+ def secret
73
+ self[:k]
74
+ end
75
+
76
+ def extract_key_params(key)
77
+ case key
78
+ when JWT::JWK::HMAC
79
+ key.export(include_private: true)
80
+ when String # Accept String key as input
81
+ { kty: KTY, k: key }
82
+ when Hash
83
+ key.transform_keys(&:to_sym)
84
+ else
85
+ raise ArgumentError, 'key must be of type String or Hash with key parameters'
86
+ end
87
+ end
88
+
89
+ def check_jwk(keypair, params)
90
+ raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (HMAC_KEY_ELEMENTS & params.keys).empty?
91
+ raise JWT::JWKError, "Incorrect 'kty' value: #{keypair[:kty]}, expected #{KTY}" unless keypair[:kty] == KTY
92
+ raise JWT::JWKError, 'Key format is invalid for HMAC' unless keypair[:k]
93
+ end
94
+
95
+ class << self
96
+ def import(jwk_data)
97
+ new(jwk_data)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWK
5
+ # Base for JWK implementations
6
+ class KeyBase
7
+ def self.inherited(klass)
8
+ super
9
+ ::JWT::JWK.classes << klass
10
+ end
11
+
12
+ def initialize(options, params = {})
13
+ options ||= {}
14
+
15
+ @parameters = params.transform_keys(&:to_sym) # Uniform interface
16
+
17
+ # For backwards compatibility, kid_generator may be specified in the parameters
18
+ options[:kid_generator] ||= @parameters.delete(:kid_generator)
19
+
20
+ # Make sure the key has a kid
21
+ kid_generator = options[:kid_generator] || ::JWT.configuration.jwk.kid_generator
22
+ self[:kid] ||= kid_generator.new(self).generate
23
+ end
24
+
25
+ def kid
26
+ self[:kid]
27
+ end
28
+
29
+ def hash
30
+ self[:kid].hash
31
+ end
32
+
33
+ def [](key)
34
+ @parameters[key.to_sym]
35
+ end
36
+
37
+ def []=(key, value)
38
+ @parameters[key.to_sym] = value
39
+ end
40
+
41
+ def ==(other)
42
+ other.is_a?(::JWT::JWK::KeyBase) && self[:kid] == other[:kid]
43
+ end
44
+
45
+ alias eql? ==
46
+
47
+ def <=>(other)
48
+ return nil unless other.is_a?(::JWT::JWK::KeyBase)
49
+
50
+ self[:kid] <=> other[:kid]
51
+ end
52
+
53
+ private
54
+
55
+ attr_reader :parameters
56
+ end
57
+ end
58
+ end
@@ -2,55 +2,45 @@
2
2
 
3
3
  module JWT
4
4
  module JWK
5
+ # @api private
5
6
  class KeyFinder
6
7
  def initialize(options)
8
+ @allow_nil_kid = options[:allow_nil_kid]
7
9
  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
+
11
+ @jwks_loader = if jwks_or_loader.respond_to?(:call)
12
+ jwks_or_loader
13
+ else
14
+ ->(_options) { jwks_or_loader }
15
+ end
10
16
  end
11
17
 
12
18
  def key_for(kid)
13
- raise ::JWT::DecodeError, 'No key id (kid) found from token headers' unless kid
19
+ raise ::JWT::DecodeError, 'No key id (kid) found from token headers' unless kid || @allow_nil_kid
20
+ raise ::JWT::DecodeError, 'Invalid type for kid header parameter' unless kid.nil? || kid.is_a?(String)
14
21
 
15
22
  jwk = resolve_key(kid)
16
23
 
24
+ raise ::JWT::DecodeError, 'No keys found in jwks' unless @jwks.any?
17
25
  raise ::JWT::DecodeError, "Could not find public key for kid #{kid}" unless jwk
18
26
 
19
- ::JWT::JWK.import(jwk).keypair
27
+ jwk.verify_key
20
28
  end
21
29
 
22
30
  private
23
31
 
24
32
  def resolve_key(kid)
25
- jwk = find_key(kid)
26
-
27
- return jwk if jwk
28
-
29
- if reloadable?
30
- load_keys(invalidate: true)
31
- return find_key(kid)
32
- end
33
-
34
- nil
35
- end
33
+ key_matcher = ->(key) { (kid.nil? && @allow_nil_kid) || key[:kid] == kid }
36
34
 
37
- def jwks
38
- return @jwks if @jwks
35
+ # First try without invalidation to facilitate application caching
36
+ @jwks ||= JWT::JWK::Set.new(@jwks_loader.call(kid: kid))
37
+ jwk = @jwks.find { |key| key_matcher.call(key) }
39
38
 
40
- load_keys
41
- @jwks
42
- end
43
-
44
- def load_keys(opts = {})
45
- @jwks = @jwk_loader.call(opts)
46
- end
47
-
48
- def find_key(kid)
49
- Array(jwks[:keys]).find { |key| key[:kid] == kid }
50
- end
39
+ return jwk if jwk
51
40
 
52
- def reloadable?
53
- @jwk_loader
41
+ # Second try, invalidate for backwards compatibility
42
+ @jwks = JWT::JWK::Set.new(@jwks_loader.call(invalidate: true, kid_not_found: true, kid: kid))
43
+ @jwks.find { |key| key_matcher.call(key) }
54
44
  end
55
45
  end
56
46
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWK
5
+ # @api private
6
+ class KidAsKeyDigest
7
+ def initialize(jwk)
8
+ @jwk = jwk
9
+ end
10
+
11
+ def generate
12
+ @jwk.key_digest
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWK
5
+ # JSON Web Key (JWK) representation for Ed25519 keys
6
+ class OKPRbNaCl < KeyBase
7
+ KTY = 'OKP'
8
+ KTYS = [KTY, JWT::JWK::OKPRbNaCl, RbNaCl::Signatures::Ed25519::SigningKey, RbNaCl::Signatures::Ed25519::VerifyKey].freeze
9
+ OKP_PUBLIC_KEY_ELEMENTS = %i[kty n x].freeze
10
+ OKP_PRIVATE_KEY_ELEMENTS = %i[d].freeze
11
+
12
+ def initialize(key, params = nil, options = {})
13
+ params ||= {}
14
+ Deprecations.warning('Using the OKP JWK for Ed25519 keys is deprecated and will be removed in a future version of ruby-jwt. Please use the ruby-eddsa gem instead.')
15
+ # For backwards compatibility when kid was a String
16
+ params = { kid: params } if params.is_a?(String)
17
+
18
+ key_params = extract_key_params(key)
19
+
20
+ params = params.transform_keys(&:to_sym)
21
+ check_jwk_params!(key_params, params)
22
+ super(options, key_params.merge(params))
23
+ end
24
+
25
+ def verify_key
26
+ return @verify_key if defined?(@verify_key)
27
+
28
+ @verify_key = verify_key_from_parameters
29
+ end
30
+
31
+ def signing_key
32
+ return @signing_key if defined?(@signing_key)
33
+
34
+ @signing_key = signing_key_from_parameters
35
+ end
36
+
37
+ def key_digest
38
+ Thumbprint.new(self).to_s
39
+ end
40
+
41
+ def private?
42
+ !signing_key.nil?
43
+ end
44
+
45
+ def members
46
+ OKP_PUBLIC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] }
47
+ end
48
+
49
+ def export(options = {})
50
+ exported = parameters.clone
51
+ exported.reject! { |k, _| OKP_PRIVATE_KEY_ELEMENTS.include?(k) } unless private? && options[:include_private] == true
52
+ exported
53
+ end
54
+
55
+ private
56
+
57
+ def extract_key_params(key)
58
+ case key
59
+ when JWT::JWK::KeyBase
60
+ key.export(include_private: true)
61
+ when RbNaCl::Signatures::Ed25519::SigningKey
62
+ @signing_key = key
63
+ @verify_key = key.verify_key
64
+ parse_okp_key_params(@verify_key, @signing_key)
65
+ when RbNaCl::Signatures::Ed25519::VerifyKey
66
+ @signing_key = nil
67
+ @verify_key = key
68
+ parse_okp_key_params(@verify_key)
69
+ when Hash
70
+ key.transform_keys(&:to_sym)
71
+ else
72
+ raise ArgumentError, 'key must be of type RbNaCl::Signatures::Ed25519::SigningKey, RbNaCl::Signatures::Ed25519::VerifyKey or Hash with key parameters'
73
+ end
74
+ end
75
+
76
+ def check_jwk_params!(key_params, _given_params)
77
+ raise JWT::JWKError, "Incorrect 'kty' value: #{key_params[:kty]}, expected #{KTY}" unless key_params[:kty] == KTY
78
+ end
79
+
80
+ def parse_okp_key_params(verify_key, signing_key = nil)
81
+ params = {
82
+ kty: KTY,
83
+ crv: 'Ed25519',
84
+ x: ::JWT::Base64.url_encode(verify_key.to_bytes)
85
+ }
86
+
87
+ params[:d] = ::JWT::Base64.url_encode(signing_key.to_bytes) if signing_key
88
+
89
+ params
90
+ end
91
+
92
+ def verify_key_from_parameters
93
+ RbNaCl::Signatures::Ed25519::VerifyKey.new(::JWT::Base64.url_decode(self[:x]))
94
+ end
95
+
96
+ def signing_key_from_parameters
97
+ return nil unless self[:d]
98
+
99
+ RbNaCl::Signatures::Ed25519::SigningKey.new(::JWT::Base64.url_decode(self[:d]))
100
+ end
101
+
102
+ class << self
103
+ def import(jwk_data)
104
+ new(jwk_data)
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end