jwt 2.3.0 → 2.7.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.
- checksums.yaml +4 -4
- data/AUTHORS +60 -53
- data/CHANGELOG.md +73 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/CONTRIBUTING.md +99 -0
- data/README.md +188 -40
- data/lib/jwt/algos/algo_wrapper.rb +30 -0
- data/lib/jwt/algos/ecdsa.rb +39 -12
- data/lib/jwt/algos/eddsa.rb +7 -4
- data/lib/jwt/algos/hmac.rb +56 -17
- data/lib/jwt/algos/hmac_rbnacl.rb +53 -0
- data/lib/jwt/algos/hmac_rbnacl_fixed.rb +52 -0
- data/lib/jwt/algos/none.rb +5 -1
- data/lib/jwt/algos/ps.rb +6 -8
- data/lib/jwt/algos/rsa.rb +7 -5
- data/lib/jwt/algos/unsupported.rb +2 -0
- data/lib/jwt/algos.rb +38 -15
- data/lib/jwt/claims_validator.rb +3 -1
- data/lib/jwt/configuration/container.rb +21 -0
- data/lib/jwt/configuration/decode_configuration.rb +46 -0
- data/lib/jwt/configuration/jwk_configuration.rb +27 -0
- data/lib/jwt/configuration.rb +15 -0
- data/lib/jwt/decode.rb +83 -26
- data/lib/jwt/encode.rb +30 -20
- data/lib/jwt/error.rb +1 -0
- data/lib/jwt/jwk/ec.rb +147 -61
- data/lib/jwt/jwk/hmac.rb +69 -24
- data/lib/jwt/jwk/key_base.rb +43 -6
- data/lib/jwt/jwk/key_finder.rb +19 -35
- data/lib/jwt/jwk/kid_as_key_digest.rb +15 -0
- data/lib/jwt/jwk/okp_rbnacl.rb +110 -0
- data/lib/jwt/jwk/rsa.rb +142 -54
- data/lib/jwt/jwk/set.rb +80 -0
- data/lib/jwt/jwk/thumbprint.rb +26 -0
- data/lib/jwt/jwk.rb +15 -11
- data/lib/jwt/security_utils.rb +2 -27
- data/lib/jwt/verify.rb +10 -2
- data/lib/jwt/version.rb +22 -2
- data/lib/jwt/x5c_key_finder.rb +55 -0
- data/lib/jwt.rb +5 -4
- data/ruby-jwt.gemspec +12 -5
- metadata +20 -16
- data/.github/workflows/test.yml +0 -74
- data/.gitignore +0 -11
- data/.rspec +0 -2
- data/.rubocop.yml +0 -97
- data/.rubocop_todo.yml +0 -185
- data/.sourcelevel.yml +0 -18
- data/Appraisals +0 -10
- data/Gemfile +0 -5
- data/Rakefile +0 -14
- data/lib/jwt/default_options.rb +0 -16
- data/lib/jwt/signature.rb +0 -39
data/lib/jwt/jwk/ec.rb
CHANGED
|
@@ -4,61 +4,108 @@ require 'forwardable'
|
|
|
4
4
|
|
|
5
5
|
module JWT
|
|
6
6
|
module JWK
|
|
7
|
-
class EC < KeyBase
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
KTY = 'EC'.freeze
|
|
12
|
-
KTYS = [KTY, OpenSSL::PKey::EC].freeze
|
|
7
|
+
class EC < KeyBase # rubocop:disable Metrics/ClassLength
|
|
8
|
+
KTY = 'EC'
|
|
9
|
+
KTYS = [KTY, OpenSSL::PKey::EC, JWT::JWK::EC].freeze
|
|
13
10
|
BINARY = 2
|
|
11
|
+
EC_PUBLIC_KEY_ELEMENTS = %i[kty crv x y].freeze
|
|
12
|
+
EC_PRIVATE_KEY_ELEMENTS = %i[d].freeze
|
|
13
|
+
EC_KEY_ELEMENTS = (EC_PRIVATE_KEY_ELEMENTS + EC_PUBLIC_KEY_ELEMENTS).freeze
|
|
14
|
+
|
|
15
|
+
def initialize(key, params = nil, options = {})
|
|
16
|
+
params ||= {}
|
|
17
|
+
|
|
18
|
+
# For backwards compatibility when kid was a String
|
|
19
|
+
params = { kid: params } if params.is_a?(String)
|
|
20
|
+
|
|
21
|
+
key_params = extract_key_params(key)
|
|
22
|
+
|
|
23
|
+
params = params.transform_keys(&:to_sym)
|
|
24
|
+
check_jwk_params!(key_params, params)
|
|
14
25
|
|
|
15
|
-
|
|
16
|
-
|
|
26
|
+
super(options, key_params.merge(params))
|
|
27
|
+
end
|
|
17
28
|
|
|
18
|
-
|
|
19
|
-
|
|
29
|
+
def keypair
|
|
30
|
+
ec_key
|
|
20
31
|
end
|
|
21
32
|
|
|
22
33
|
def private?
|
|
23
|
-
|
|
34
|
+
ec_key.private_key?
|
|
24
35
|
end
|
|
25
36
|
|
|
26
|
-
def
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
kty: KTY,
|
|
30
|
-
crv: crv,
|
|
31
|
-
x: encode_octets(x_octets),
|
|
32
|
-
y: encode_octets(y_octets),
|
|
33
|
-
kid: kid
|
|
34
|
-
}
|
|
35
|
-
return exported_hash unless private? && options[:include_private] == true
|
|
37
|
+
def signing_key
|
|
38
|
+
ec_key
|
|
39
|
+
end
|
|
36
40
|
|
|
37
|
-
|
|
41
|
+
def verify_key
|
|
42
|
+
ec_key
|
|
38
43
|
end
|
|
39
44
|
|
|
40
|
-
|
|
45
|
+
def public_key
|
|
46
|
+
ec_key
|
|
47
|
+
end
|
|
41
48
|
|
|
42
|
-
def
|
|
43
|
-
|
|
44
|
-
the_hash.merge(
|
|
45
|
-
d: encode_octets(octets)
|
|
46
|
-
)
|
|
49
|
+
def members
|
|
50
|
+
EC_PUBLIC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] }
|
|
47
51
|
end
|
|
48
52
|
|
|
49
|
-
def
|
|
50
|
-
|
|
53
|
+
def export(options = {})
|
|
54
|
+
exported = parameters.clone
|
|
55
|
+
exported.reject! { |k, _| EC_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true
|
|
56
|
+
exported
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def key_digest
|
|
60
|
+
_crv, x_octets, y_octets = keypair_components(ec_key)
|
|
51
61
|
sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(x_octets, BINARY)),
|
|
52
62
|
OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(y_octets, BINARY))])
|
|
53
63
|
OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
|
|
54
64
|
end
|
|
55
65
|
|
|
66
|
+
def []=(key, value)
|
|
67
|
+
if EC_KEY_ELEMENTS.include?(key.to_sym)
|
|
68
|
+
raise ArgumentError, 'cannot overwrite cryptographic key attributes'
|
|
69
|
+
end
|
|
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
|
+
|
|
56
100
|
def keypair_components(ec_keypair)
|
|
57
101
|
encoded_point = ec_keypair.public_key.to_bn.to_s(BINARY)
|
|
58
102
|
case ec_keypair.group.curve_name
|
|
59
103
|
when 'prime256v1'
|
|
60
104
|
crv = 'P-256'
|
|
61
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')
|
|
62
109
|
when 'secp384r1'
|
|
63
110
|
crv = 'P-384'
|
|
64
111
|
x_octets, y_octets = encoded_point.unpack('xa48a48')
|
|
@@ -72,6 +119,8 @@ module JWT
|
|
|
72
119
|
end
|
|
73
120
|
|
|
74
121
|
def encode_octets(octets)
|
|
122
|
+
return unless octets
|
|
123
|
+
|
|
75
124
|
::JWT::Base64.url_encode(octets)
|
|
76
125
|
end
|
|
77
126
|
|
|
@@ -79,39 +128,57 @@ module JWT
|
|
|
79
128
|
::JWT::Base64.url_encode(key_part.to_s(BINARY))
|
|
80
129
|
end
|
|
81
130
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
89
142
|
|
|
90
|
-
|
|
91
|
-
|
|
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)
|
|
92
146
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
# See https://tools.ietf.org/html/rfc5480#section-2.1.1.1 for some
|
|
96
|
-
# pointers on different names for common curves.
|
|
97
|
-
case crv
|
|
98
|
-
when 'P-256' then 'prime256v1'
|
|
99
|
-
when 'P-384' then 'secp384r1'
|
|
100
|
-
when 'P-521' then 'secp521r1'
|
|
101
|
-
else raise JWT::JWKError, 'Invalid curve provided'
|
|
102
|
-
end
|
|
103
|
-
end
|
|
147
|
+
x_octets = decode_octets(jwk_x)
|
|
148
|
+
y_octets = decode_octets(jwk_y)
|
|
104
149
|
|
|
105
|
-
|
|
150
|
+
point = OpenSSL::PKey::EC::Point.new(
|
|
151
|
+
OpenSSL::PKey::EC::Group.new(curve),
|
|
152
|
+
OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2)
|
|
153
|
+
)
|
|
106
154
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
155
|
+
sequence = if jwk_d
|
|
156
|
+
# https://datatracker.ietf.org/doc/html/rfc5915.html
|
|
157
|
+
# ECPrivateKey ::= SEQUENCE {
|
|
158
|
+
# version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1),
|
|
159
|
+
# privateKey OCTET STRING,
|
|
160
|
+
# parameters [0] ECParameters {{ NamedCurve }} OPTIONAL,
|
|
161
|
+
# publicKey [1] BIT STRING OPTIONAL
|
|
162
|
+
# }
|
|
163
|
+
|
|
164
|
+
OpenSSL::ASN1::Sequence([
|
|
165
|
+
OpenSSL::ASN1::Integer(1),
|
|
166
|
+
OpenSSL::ASN1::OctetString(OpenSSL::BN.new(decode_octets(jwk_d), 2).to_s(2)),
|
|
167
|
+
OpenSSL::ASN1::ObjectId(curve, 0, :EXPLICIT),
|
|
168
|
+
OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed), 1, :EXPLICIT)
|
|
169
|
+
])
|
|
170
|
+
else
|
|
171
|
+
OpenSSL::ASN1::Sequence([
|
|
172
|
+
OpenSSL::ASN1::Sequence([OpenSSL::ASN1::ObjectId('id-ecPublicKey'), OpenSSL::ASN1::ObjectId(curve)]),
|
|
173
|
+
OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed))
|
|
174
|
+
])
|
|
110
175
|
end
|
|
111
|
-
end
|
|
112
176
|
|
|
113
|
-
|
|
114
|
-
|
|
177
|
+
OpenSSL::PKey::EC.new(sequence.to_der)
|
|
178
|
+
end
|
|
179
|
+
else
|
|
180
|
+
def create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d)
|
|
181
|
+
curve = EC.to_openssl_curve(jwk_crv)
|
|
115
182
|
|
|
116
183
|
x_octets = decode_octets(jwk_x)
|
|
117
184
|
y_octets = decode_octets(jwk_y)
|
|
@@ -136,13 +203,32 @@ module JWT
|
|
|
136
203
|
|
|
137
204
|
key
|
|
138
205
|
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def decode_octets(jwk_data)
|
|
209
|
+
::JWT::Base64.url_decode(jwk_data)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def decode_open_ssl_bn(jwk_data)
|
|
213
|
+
OpenSSL::BN.new(::JWT::Base64.url_decode(jwk_data), BINARY)
|
|
214
|
+
end
|
|
139
215
|
|
|
140
|
-
|
|
141
|
-
|
|
216
|
+
class << self
|
|
217
|
+
def import(jwk_data)
|
|
218
|
+
new(jwk_data)
|
|
142
219
|
end
|
|
143
220
|
|
|
144
|
-
def
|
|
145
|
-
OpenSSL
|
|
221
|
+
def to_openssl_curve(crv)
|
|
222
|
+
# The JWK specs and OpenSSL use different names for the same curves.
|
|
223
|
+
# See https://tools.ietf.org/html/rfc5480#section-2.1.1.1 for some
|
|
224
|
+
# pointers on different names for common curves.
|
|
225
|
+
case crv
|
|
226
|
+
when 'P-256' then 'prime256v1'
|
|
227
|
+
when 'P-384' then 'secp384r1'
|
|
228
|
+
when 'P-521' then 'secp521r1'
|
|
229
|
+
when 'P-256K' then 'secp256k1'
|
|
230
|
+
else raise JWT::JWKError, 'Invalid curve provided'
|
|
231
|
+
end
|
|
146
232
|
end
|
|
147
233
|
end
|
|
148
234
|
end
|
data/lib/jwt/jwk/hmac.rb
CHANGED
|
@@ -3,14 +3,28 @@
|
|
|
3
3
|
module JWT
|
|
4
4
|
module JWK
|
|
5
5
|
class HMAC < KeyBase
|
|
6
|
-
KTY
|
|
7
|
-
KTYS = [KTY, String].freeze
|
|
6
|
+
KTY = 'oct'
|
|
7
|
+
KTYS = [KTY, String, JWT::JWK::HMAC].freeze
|
|
8
|
+
HMAC_PUBLIC_KEY_ELEMENTS = %i[kty].freeze
|
|
9
|
+
HMAC_PRIVATE_KEY_ELEMENTS = %i[k].freeze
|
|
10
|
+
HMAC_KEY_ELEMENTS = (HMAC_PRIVATE_KEY_ELEMENTS + HMAC_PUBLIC_KEY_ELEMENTS).freeze
|
|
8
11
|
|
|
9
|
-
def initialize(
|
|
10
|
-
|
|
12
|
+
def initialize(key, params = nil, options = {})
|
|
13
|
+
params ||= {}
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
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(key_params, params)
|
|
22
|
+
|
|
23
|
+
super(options, key_params.merge(params))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def keypair
|
|
27
|
+
secret
|
|
14
28
|
end
|
|
15
29
|
|
|
16
30
|
def private?
|
|
@@ -21,36 +35,67 @@ module JWT
|
|
|
21
35
|
nil
|
|
22
36
|
end
|
|
23
37
|
|
|
38
|
+
def verify_key
|
|
39
|
+
secret
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def signing_key
|
|
43
|
+
secret
|
|
44
|
+
end
|
|
45
|
+
|
|
24
46
|
# See https://tools.ietf.org/html/rfc7517#appendix-A.3
|
|
25
47
|
def export(options = {})
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
48
|
+
exported = parameters.clone
|
|
49
|
+
exported.reject! { |k, _| HMAC_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true
|
|
50
|
+
exported
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def members
|
|
54
|
+
HMAC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def key_digest
|
|
58
|
+
sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::UTF8String.new(signing_key),
|
|
59
|
+
OpenSSL::ASN1::UTF8String.new(KTY)])
|
|
60
|
+
OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
|
|
61
|
+
end
|
|
30
62
|
|
|
31
|
-
|
|
63
|
+
def []=(key, value)
|
|
64
|
+
if HMAC_KEY_ELEMENTS.include?(key.to_sym)
|
|
65
|
+
raise ArgumentError, 'cannot overwrite cryptographic key attributes'
|
|
66
|
+
end
|
|
32
67
|
|
|
33
|
-
|
|
34
|
-
k: keypair
|
|
35
|
-
)
|
|
68
|
+
super(key, value)
|
|
36
69
|
end
|
|
37
70
|
|
|
38
71
|
private
|
|
39
72
|
|
|
40
|
-
def
|
|
41
|
-
|
|
42
|
-
OpenSSL::ASN1::UTF8String.new(KTY)])
|
|
43
|
-
OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
|
|
73
|
+
def secret
|
|
74
|
+
self[:k]
|
|
44
75
|
end
|
|
45
76
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
77
|
+
def extract_key_params(key)
|
|
78
|
+
case key
|
|
79
|
+
when JWT::JWK::HMAC
|
|
80
|
+
key.export(include_private: true)
|
|
81
|
+
when String # Accept String key as input
|
|
82
|
+
{ kty: KTY, k: key }
|
|
83
|
+
when Hash
|
|
84
|
+
key.transform_keys(&:to_sym)
|
|
85
|
+
else
|
|
86
|
+
raise ArgumentError, 'key must be of type String or Hash with key parameters'
|
|
87
|
+
end
|
|
88
|
+
end
|
|
50
89
|
|
|
51
|
-
|
|
90
|
+
def check_jwk(keypair, params)
|
|
91
|
+
raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (HMAC_KEY_ELEMENTS & params.keys).empty?
|
|
92
|
+
raise JWT::JWKError, "Incorrect 'kty' value: #{keypair[:kty]}, expected #{KTY}" unless keypair[:kty] == KTY
|
|
93
|
+
raise JWT::JWKError, 'Key format is invalid for HMAC' unless keypair[:k]
|
|
94
|
+
end
|
|
52
95
|
|
|
53
|
-
|
|
96
|
+
class << self
|
|
97
|
+
def import(jwk_data)
|
|
98
|
+
new(jwk_data)
|
|
54
99
|
end
|
|
55
100
|
end
|
|
56
101
|
end
|
data/lib/jwt/jwk/key_base.rb
CHANGED
|
@@ -3,16 +3,53 @@
|
|
|
3
3
|
module JWT
|
|
4
4
|
module JWK
|
|
5
5
|
class KeyBase
|
|
6
|
-
|
|
6
|
+
def self.inherited(klass)
|
|
7
|
+
super
|
|
8
|
+
::JWT::JWK.classes << klass
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(options, params = {})
|
|
12
|
+
options ||= {}
|
|
13
|
+
|
|
14
|
+
@parameters = params.transform_keys(&:to_sym) # Uniform interface
|
|
15
|
+
|
|
16
|
+
# For backwards compatibility, kid_generator may be specified in the parameters
|
|
17
|
+
options[:kid_generator] ||= @parameters.delete(:kid_generator)
|
|
7
18
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
19
|
+
# Make sure the key has a kid
|
|
20
|
+
kid_generator = options[:kid_generator] || ::JWT.configuration.jwk.kid_generator
|
|
21
|
+
self[:kid] ||= kid_generator.new(self).generate
|
|
11
22
|
end
|
|
12
23
|
|
|
13
|
-
def
|
|
14
|
-
|
|
24
|
+
def kid
|
|
25
|
+
self[:kid]
|
|
15
26
|
end
|
|
27
|
+
|
|
28
|
+
def hash
|
|
29
|
+
self[:kid].hash
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def [](key)
|
|
33
|
+
@parameters[key.to_sym]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def []=(key, value)
|
|
37
|
+
@parameters[key.to_sym] = value
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def ==(other)
|
|
41
|
+
self[:kid] == other[:kid]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
alias eql? ==
|
|
45
|
+
|
|
46
|
+
def <=>(other)
|
|
47
|
+
self[:kid] <=> other[:kid]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
attr_reader :parameters
|
|
16
53
|
end
|
|
17
54
|
end
|
|
18
55
|
end
|
data/lib/jwt/jwk/key_finder.rb
CHANGED
|
@@ -4,58 +4,42 @@ module JWT
|
|
|
4
4
|
module JWK
|
|
5
5
|
class KeyFinder
|
|
6
6
|
def initialize(options)
|
|
7
|
+
@allow_nil_kid = options[:allow_nil_kid]
|
|
7
8
|
jwks_or_loader = options[:jwks]
|
|
8
|
-
|
|
9
|
-
@
|
|
9
|
+
|
|
10
|
+
@jwks_loader = if jwks_or_loader.respond_to?(:call)
|
|
11
|
+
jwks_or_loader
|
|
12
|
+
else
|
|
13
|
+
->(_options) { jwks_or_loader }
|
|
14
|
+
end
|
|
10
15
|
end
|
|
11
16
|
|
|
12
17
|
def key_for(kid)
|
|
13
|
-
raise ::JWT::DecodeError, 'No key id (kid) found from token headers' unless kid
|
|
18
|
+
raise ::JWT::DecodeError, 'No key id (kid) found from token headers' unless kid || @allow_nil_kid
|
|
19
|
+
raise ::JWT::DecodeError, 'Invalid type for kid header parameter' unless kid.nil? || kid.is_a?(String)
|
|
14
20
|
|
|
15
21
|
jwk = resolve_key(kid)
|
|
16
22
|
|
|
17
|
-
raise ::JWT::DecodeError, 'No keys found in jwks'
|
|
23
|
+
raise ::JWT::DecodeError, 'No keys found in jwks' unless @jwks.any?
|
|
18
24
|
raise ::JWT::DecodeError, "Could not find public key for kid #{kid}" unless jwk
|
|
19
25
|
|
|
20
|
-
|
|
26
|
+
jwk.verify_key
|
|
21
27
|
end
|
|
22
28
|
|
|
23
29
|
private
|
|
24
30
|
|
|
25
31
|
def resolve_key(kid)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return jwk if jwk
|
|
29
|
-
|
|
30
|
-
if reloadable?
|
|
31
|
-
load_keys(invalidate: true)
|
|
32
|
-
return find_key(kid)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
nil
|
|
36
|
-
end
|
|
32
|
+
key_matcher = ->(key) { (kid.nil? && @allow_nil_kid) || key[:kid] == kid }
|
|
37
33
|
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
# First try without invalidation to facilitate application caching
|
|
35
|
+
@jwks ||= JWT::JWK::Set.new(@jwks_loader.call(kid: kid))
|
|
36
|
+
jwk = @jwks.find { |key| key_matcher.call(key) }
|
|
40
37
|
|
|
41
|
-
|
|
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
|
|
38
|
+
return jwk if jwk
|
|
56
39
|
|
|
57
|
-
|
|
58
|
-
@
|
|
40
|
+
# Second try, invalidate for backwards compatibility
|
|
41
|
+
@jwks = JWT::JWK::Set.new(@jwks_loader.call(invalidate: true, kid_not_found: true, kid: kid))
|
|
42
|
+
@jwks.find { |key| key_matcher.call(key) }
|
|
59
43
|
end
|
|
60
44
|
end
|
|
61
45
|
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JWT
|
|
4
|
+
module JWK
|
|
5
|
+
class OKPRbNaCl < KeyBase
|
|
6
|
+
KTY = 'OKP'
|
|
7
|
+
KTYS = [KTY, JWT::JWK::OKPRbNaCl, RbNaCl::Signatures::Ed25519::SigningKey, RbNaCl::Signatures::Ed25519::VerifyKey].freeze
|
|
8
|
+
OKP_PUBLIC_KEY_ELEMENTS = %i[kty n x].freeze
|
|
9
|
+
OKP_PRIVATE_KEY_ELEMENTS = %i[d].freeze
|
|
10
|
+
|
|
11
|
+
def initialize(key, params = nil, options = {})
|
|
12
|
+
params ||= {}
|
|
13
|
+
|
|
14
|
+
# For backwards compatibility when kid was a String
|
|
15
|
+
params = { kid: params } if params.is_a?(String)
|
|
16
|
+
|
|
17
|
+
key_params = extract_key_params(key)
|
|
18
|
+
|
|
19
|
+
params = params.transform_keys(&:to_sym)
|
|
20
|
+
check_jwk_params!(key_params, params)
|
|
21
|
+
super(options, key_params.merge(params))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def verify_key
|
|
25
|
+
return @verify_key if defined?(@verify_key)
|
|
26
|
+
|
|
27
|
+
@verify_key = verify_key_from_parameters
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def signing_key
|
|
31
|
+
return @signing_key if defined?(@signing_key)
|
|
32
|
+
|
|
33
|
+
@signing_key = signing_key_from_parameters
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def key_digest
|
|
37
|
+
Thumbprint.new(self).to_s
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def private?
|
|
41
|
+
!signing_key.nil?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def members
|
|
45
|
+
OKP_PUBLIC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def export(options = {})
|
|
49
|
+
exported = parameters.clone
|
|
50
|
+
exported.reject! { |k, _| OKP_PRIVATE_KEY_ELEMENTS.include?(k) } unless private? && options[:include_private] == true
|
|
51
|
+
exported
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def extract_key_params(key)
|
|
57
|
+
case key
|
|
58
|
+
when JWT::JWK::KeyBase
|
|
59
|
+
key.export(include_private: true)
|
|
60
|
+
when RbNaCl::Signatures::Ed25519::SigningKey
|
|
61
|
+
@signing_key = key
|
|
62
|
+
@verify_key = key.verify_key
|
|
63
|
+
parse_okp_key_params(@verify_key, @signing_key)
|
|
64
|
+
when RbNaCl::Signatures::Ed25519::VerifyKey
|
|
65
|
+
@signing_key = nil
|
|
66
|
+
@verify_key = key
|
|
67
|
+
parse_okp_key_params(@verify_key)
|
|
68
|
+
when Hash
|
|
69
|
+
key.transform_keys(&:to_sym)
|
|
70
|
+
else
|
|
71
|
+
raise ArgumentError, 'key must be of type RbNaCl::Signatures::Ed25519::SigningKey, RbNaCl::Signatures::Ed25519::VerifyKey or Hash with key parameters'
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def check_jwk_params!(key_params, _given_params)
|
|
76
|
+
raise JWT::JWKError, "Incorrect 'kty' value: #{key_params[:kty]}, expected #{KTY}" unless key_params[:kty] == KTY
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def parse_okp_key_params(verify_key, signing_key = nil)
|
|
80
|
+
params = {
|
|
81
|
+
kty: KTY,
|
|
82
|
+
crv: 'Ed25519',
|
|
83
|
+
x: ::JWT::Base64.url_encode(verify_key.to_bytes)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if signing_key
|
|
87
|
+
params[:d] = ::JWT::Base64.url_encode(signing_key.to_bytes)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
params
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def verify_key_from_parameters
|
|
94
|
+
RbNaCl::Signatures::Ed25519::VerifyKey.new(::JWT::Base64.url_decode(self[:x]))
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def signing_key_from_parameters
|
|
98
|
+
return nil unless self[:d]
|
|
99
|
+
|
|
100
|
+
RbNaCl::Signatures::Ed25519::SigningKey.new(::JWT::Base64.url_decode(self[:d]))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
class << self
|
|
104
|
+
def import(jwk_data)
|
|
105
|
+
new(jwk_data)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|