jwt 2.4.1 → 2.6.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/CHANGELOG.md +33 -2
- data/CONTRIBUTING.md +7 -7
- data/README.md +135 -31
- data/lib/jwt/algos/algo_wrapper.rb +30 -0
- data/lib/jwt/algos/ecdsa.rb +2 -4
- data/lib/jwt/algos/eddsa.rb +4 -4
- data/lib/jwt/algos/hmac.rb +54 -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 +3 -1
- data/lib/jwt/algos/ps.rb +3 -5
- data/lib/jwt/algos/rsa.rb +3 -4
- data/lib/jwt/algos.rb +38 -15
- data/lib/jwt/base64.rb +19 -0
- 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 +48 -27
- data/lib/jwt/encode.rb +30 -20
- data/lib/jwt/jwk/ec.rb +131 -62
- data/lib/jwt/jwk/hmac.rb +59 -24
- data/lib/jwt/jwk/key_base.rb +43 -7
- data/lib/jwt/jwk/key_finder.rb +14 -34
- data/lib/jwt/jwk/kid_as_key_digest.rb +15 -0
- data/lib/jwt/jwk/rsa.rb +128 -53
- data/lib/jwt/jwk/set.rb +80 -0
- data/lib/jwt/jwk/thumbprint.rb +26 -0
- data/lib/jwt/jwk.rb +13 -11
- data/lib/jwt/security_utils.rb +0 -27
- data/lib/jwt/version.rb +23 -2
- data/lib/jwt/x5c_key_finder.rb +1 -1
- data/lib/jwt.rb +5 -4
- data/ruby-jwt.gemspec +8 -4
- metadata +15 -30
- data/.codeclimate.yml +0 -8
- data/.github/workflows/coverage.yml +0 -27
- data/.github/workflows/test.yml +0 -66
- data/.gitignore +0 -13
- data/.reek.yml +0 -22
- data/.rspec +0 -2
- data/.rubocop.yml +0 -67
- data/.sourcelevel.yml +0 -17
- data/Appraisals +0 -13
- data/Gemfile +0 -7
- data/Rakefile +0 -16
- data/lib/jwt/default_options.rb +0 -18
- data/lib/jwt/signature.rb +0 -35
data/lib/jwt/jwk/ec.rb
CHANGED
@@ -4,53 +4,84 @@ require 'forwardable'
|
|
4
4
|
|
5
5
|
module JWT
|
6
6
|
module JWK
|
7
|
-
class EC < KeyBase
|
7
|
+
class EC < KeyBase # rubocop:disable Metrics/ClassLength
|
8
8
|
extend Forwardable
|
9
|
-
def_delegators
|
9
|
+
def_delegators :keypair, :public_key
|
10
10
|
|
11
11
|
KTY = 'EC'
|
12
|
-
KTYS = [KTY, OpenSSL::PKey::EC].freeze
|
12
|
+
KTYS = [KTY, OpenSSL::PKey::EC, JWT::JWK::EC].freeze
|
13
13
|
BINARY = 2
|
14
|
+
EC_PUBLIC_KEY_ELEMENTS = %i[kty crv x y].freeze
|
15
|
+
EC_PRIVATE_KEY_ELEMENTS = %i[d].freeze
|
16
|
+
EC_KEY_ELEMENTS = (EC_PRIVATE_KEY_ELEMENTS + EC_PUBLIC_KEY_ELEMENTS).freeze
|
14
17
|
|
15
|
-
def initialize(
|
16
|
-
|
18
|
+
def initialize(key, params = nil, options = {})
|
19
|
+
params ||= {}
|
17
20
|
|
18
|
-
kid
|
19
|
-
|
21
|
+
# For backwards compatibility when kid was a String
|
22
|
+
params = { kid: params } if params.is_a?(String)
|
23
|
+
|
24
|
+
key_params = extract_key_params(key)
|
25
|
+
|
26
|
+
params = params.transform_keys(&:to_sym)
|
27
|
+
check_jwk(key_params, params)
|
28
|
+
|
29
|
+
super(options, key_params.merge(params))
|
30
|
+
end
|
31
|
+
|
32
|
+
def keypair
|
33
|
+
@keypair ||= create_ec_key(self[:crv], self[:x], self[:y], self[:d])
|
20
34
|
end
|
21
35
|
|
22
36
|
def private?
|
23
|
-
|
37
|
+
keypair.private_key?
|
38
|
+
end
|
39
|
+
|
40
|
+
def members
|
41
|
+
EC_PUBLIC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] }
|
24
42
|
end
|
25
43
|
|
26
44
|
def export(options = {})
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
45
|
+
exported = parameters.clone
|
46
|
+
exported.reject! { |k, _| EC_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true
|
47
|
+
exported
|
48
|
+
end
|
49
|
+
|
50
|
+
def key_digest
|
51
|
+
_crv, x_octets, y_octets = keypair_components(keypair)
|
52
|
+
sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(x_octets, BINARY)),
|
53
|
+
OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(y_octets, BINARY))])
|
54
|
+
OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
|
55
|
+
end
|
56
|
+
|
57
|
+
def []=(key, value)
|
58
|
+
if EC_KEY_ELEMENTS.include?(key.to_sym)
|
59
|
+
raise ArgumentError, 'cannot overwrite cryptographic key attributes'
|
60
|
+
end
|
36
61
|
|
37
|
-
|
62
|
+
super(key, value)
|
38
63
|
end
|
39
64
|
|
40
65
|
private
|
41
66
|
|
42
|
-
def
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
67
|
+
def extract_key_params(key)
|
68
|
+
case key
|
69
|
+
when JWT::JWK::EC
|
70
|
+
key.export(include_private: true)
|
71
|
+
when OpenSSL::PKey::EC # Accept OpenSSL key as input
|
72
|
+
@keypair = key # Preserve the object to avoid recreation
|
73
|
+
parse_ec_key(key)
|
74
|
+
when Hash
|
75
|
+
key.transform_keys(&:to_sym)
|
76
|
+
else
|
77
|
+
raise ArgumentError, 'key must be of type OpenSSL::PKey::EC or Hash with key parameters'
|
78
|
+
end
|
47
79
|
end
|
48
80
|
|
49
|
-
def
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
|
81
|
+
def check_jwk(keypair, params)
|
82
|
+
raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (EC_KEY_ELEMENTS & params.keys).empty?
|
83
|
+
raise JWT::JWKError, "Incorrect 'kty' value: #{keypair[:kty]}, expected #{KTY}" unless keypair[:kty] == KTY
|
84
|
+
raise JWT::JWKError, 'Key format is invalid for EC' unless keypair[:crv] && keypair[:x] && keypair[:y]
|
54
85
|
end
|
55
86
|
|
56
87
|
def keypair_components(ec_keypair)
|
@@ -75,47 +106,66 @@ module JWT
|
|
75
106
|
end
|
76
107
|
|
77
108
|
def encode_octets(octets)
|
78
|
-
|
109
|
+
return unless octets
|
110
|
+
|
111
|
+
::JWT::Base64.url_encode(octets)
|
79
112
|
end
|
80
113
|
|
81
114
|
def encode_open_ssl_bn(key_part)
|
82
|
-
Base64.
|
115
|
+
::JWT::Base64.url_encode(key_part.to_s(BINARY))
|
83
116
|
end
|
84
117
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
118
|
+
def parse_ec_key(key)
|
119
|
+
crv, x_octets, y_octets = keypair_components(key)
|
120
|
+
octets = key.private_key&.to_bn&.to_s(BINARY)
|
121
|
+
{
|
122
|
+
kty: KTY,
|
123
|
+
crv: crv,
|
124
|
+
x: encode_octets(x_octets),
|
125
|
+
y: encode_octets(y_octets),
|
126
|
+
d: encode_octets(octets)
|
127
|
+
}.compact
|
128
|
+
end
|
92
129
|
|
93
|
-
|
94
|
-
|
130
|
+
if ::JWT.openssl_3?
|
131
|
+
def create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d) # rubocop:disable Metrics/MethodLength
|
132
|
+
curve = EC.to_openssl_curve(jwk_crv)
|
95
133
|
|
96
|
-
|
97
|
-
|
98
|
-
# See https://tools.ietf.org/html/rfc5480#section-2.1.1.1 for some
|
99
|
-
# pointers on different names for common curves.
|
100
|
-
case crv
|
101
|
-
when 'P-256' then 'prime256v1'
|
102
|
-
when 'P-384' then 'secp384r1'
|
103
|
-
when 'P-521' then 'secp521r1'
|
104
|
-
when 'P-256K' then 'secp256k1'
|
105
|
-
else raise JWT::JWKError, 'Invalid curve provided'
|
106
|
-
end
|
107
|
-
end
|
134
|
+
x_octets = decode_octets(jwk_x)
|
135
|
+
y_octets = decode_octets(jwk_y)
|
108
136
|
|
109
|
-
|
137
|
+
point = OpenSSL::PKey::EC::Point.new(
|
138
|
+
OpenSSL::PKey::EC::Group.new(curve),
|
139
|
+
OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2)
|
140
|
+
)
|
110
141
|
|
111
|
-
|
112
|
-
|
113
|
-
|
142
|
+
sequence = if jwk_d
|
143
|
+
# https://datatracker.ietf.org/doc/html/rfc5915.html
|
144
|
+
# ECPrivateKey ::= SEQUENCE {
|
145
|
+
# version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1),
|
146
|
+
# privateKey OCTET STRING,
|
147
|
+
# parameters [0] ECParameters {{ NamedCurve }} OPTIONAL,
|
148
|
+
# publicKey [1] BIT STRING OPTIONAL
|
149
|
+
# }
|
150
|
+
|
151
|
+
OpenSSL::ASN1::Sequence([
|
152
|
+
OpenSSL::ASN1::Integer(1),
|
153
|
+
OpenSSL::ASN1::OctetString(OpenSSL::BN.new(decode_octets(jwk_d), 2).to_s(2)),
|
154
|
+
OpenSSL::ASN1::ObjectId(curve, 0, :EXPLICIT),
|
155
|
+
OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed), 1, :EXPLICIT)
|
156
|
+
])
|
157
|
+
else
|
158
|
+
OpenSSL::ASN1::Sequence([
|
159
|
+
OpenSSL::ASN1::Sequence([OpenSSL::ASN1::ObjectId('id-ecPublicKey'), OpenSSL::ASN1::ObjectId(curve)]),
|
160
|
+
OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed))
|
161
|
+
])
|
114
162
|
end
|
115
|
-
end
|
116
163
|
|
117
|
-
|
118
|
-
|
164
|
+
OpenSSL::PKey::EC.new(sequence.to_der)
|
165
|
+
end
|
166
|
+
else
|
167
|
+
def create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d)
|
168
|
+
curve = EC.to_openssl_curve(jwk_crv)
|
119
169
|
|
120
170
|
x_octets = decode_octets(jwk_x)
|
121
171
|
y_octets = decode_octets(jwk_y)
|
@@ -140,13 +190,32 @@ module JWT
|
|
140
190
|
|
141
191
|
key
|
142
192
|
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def decode_octets(jwk_data)
|
196
|
+
::JWT::Base64.url_decode(jwk_data)
|
197
|
+
end
|
198
|
+
|
199
|
+
def decode_open_ssl_bn(jwk_data)
|
200
|
+
OpenSSL::BN.new(::JWT::Base64.url_decode(jwk_data), BINARY)
|
201
|
+
end
|
143
202
|
|
144
|
-
|
145
|
-
|
203
|
+
class << self
|
204
|
+
def import(jwk_data)
|
205
|
+
new(jwk_data)
|
146
206
|
end
|
147
207
|
|
148
|
-
def
|
149
|
-
OpenSSL
|
208
|
+
def to_openssl_curve(crv)
|
209
|
+
# The JWK specs and OpenSSL use different names for the same curves.
|
210
|
+
# See https://tools.ietf.org/html/rfc5480#section-2.1.1.1 for some
|
211
|
+
# pointers on different names for common curves.
|
212
|
+
case crv
|
213
|
+
when 'P-256' then 'prime256v1'
|
214
|
+
when 'P-384' then 'secp384r1'
|
215
|
+
when 'P-521' then 'secp521r1'
|
216
|
+
when 'P-256K' then 'secp256k1'
|
217
|
+
else raise JWT::JWKError, 'Invalid curve provided'
|
218
|
+
end
|
150
219
|
end
|
151
220
|
end
|
152
221
|
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
|
+
self[:k]
|
14
28
|
end
|
15
29
|
|
16
30
|
def private?
|
@@ -23,34 +37,55 @@ module JWT
|
|
23
37
|
|
24
38
|
# See https://tools.ietf.org/html/rfc7517#appendix-A.3
|
25
39
|
def export(options = {})
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
return exported_hash unless private? && options[:include_private] == true
|
40
|
+
exported = parameters.clone
|
41
|
+
exported.reject! { |k, _| HMAC_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true
|
42
|
+
exported
|
43
|
+
end
|
32
44
|
|
33
|
-
|
34
|
-
|
35
|
-
)
|
45
|
+
def members
|
46
|
+
HMAC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] }
|
36
47
|
end
|
37
48
|
|
38
|
-
|
49
|
+
alias signing_key keypair # for backwards compatibility
|
39
50
|
|
40
|
-
def
|
41
|
-
sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::UTF8String.new(
|
51
|
+
def key_digest
|
52
|
+
sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::UTF8String.new(signing_key),
|
42
53
|
OpenSSL::ASN1::UTF8String.new(KTY)])
|
43
54
|
OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
|
44
55
|
end
|
45
56
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
57
|
+
def []=(key, value)
|
58
|
+
if HMAC_KEY_ELEMENTS.include?(key.to_sym)
|
59
|
+
raise ArgumentError, 'cannot overwrite cryptographic key attributes'
|
60
|
+
end
|
50
61
|
|
51
|
-
|
62
|
+
super(key, value)
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def extract_key_params(key)
|
68
|
+
case key
|
69
|
+
when JWT::JWK::HMAC
|
70
|
+
key.export(include_private: true)
|
71
|
+
when String # Accept String key as input
|
72
|
+
{ kty: KTY, k: key }
|
73
|
+
when Hash
|
74
|
+
key.transform_keys(&:to_sym)
|
75
|
+
else
|
76
|
+
raise ArgumentError, 'key must be of type String or Hash with key parameters'
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def check_jwk(keypair, params)
|
81
|
+
raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (HMAC_KEY_ELEMENTS & params.keys).empty?
|
82
|
+
raise JWT::JWKError, "Incorrect 'kty' value: #{keypair[:kty]}, expected #{KTY}" unless keypair[:kty] == KTY
|
83
|
+
raise JWT::JWKError, 'Key format is invalid for HMAC' unless keypair[:k]
|
84
|
+
end
|
52
85
|
|
53
|
-
|
86
|
+
class << self
|
87
|
+
def import(jwk_data)
|
88
|
+
new(jwk_data)
|
54
89
|
end
|
55
90
|
end
|
56
91
|
end
|
data/lib/jwt/jwk/key_base.rb
CHANGED
@@ -3,17 +3,53 @@
|
|
3
3
|
module JWT
|
4
4
|
module JWK
|
5
5
|
class KeyBase
|
6
|
-
attr_reader :keypair, :kid
|
7
|
-
|
8
|
-
def initialize(keypair, kid = nil)
|
9
|
-
@keypair = keypair
|
10
|
-
@kid = kid
|
11
|
-
end
|
12
|
-
|
13
6
|
def self.inherited(klass)
|
14
7
|
super
|
15
8
|
::JWT::JWK.classes << klass
|
16
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)
|
18
|
+
|
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
|
22
|
+
end
|
23
|
+
|
24
|
+
def kid
|
25
|
+
self[:kid]
|
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
|
17
53
|
end
|
18
54
|
end
|
19
55
|
end
|
data/lib/jwt/jwk/key_finder.rb
CHANGED
@@ -5,8 +5,12 @@ module JWT
|
|
5
5
|
class KeyFinder
|
6
6
|
def initialize(options)
|
7
7
|
jwks_or_loader = options[:jwks]
|
8
|
-
|
9
|
-
@
|
8
|
+
|
9
|
+
@jwks_loader = if jwks_or_loader.respond_to?(:call)
|
10
|
+
jwks_or_loader
|
11
|
+
else
|
12
|
+
->(_options) { jwks_or_loader }
|
13
|
+
end
|
10
14
|
end
|
11
15
|
|
12
16
|
def key_for(kid)
|
@@ -14,48 +18,24 @@ module JWT
|
|
14
18
|
|
15
19
|
jwk = resolve_key(kid)
|
16
20
|
|
17
|
-
raise ::JWT::DecodeError, 'No keys found in jwks'
|
21
|
+
raise ::JWT::DecodeError, 'No keys found in jwks' unless @jwks.any?
|
18
22
|
raise ::JWT::DecodeError, "Could not find public key for kid #{kid}" unless jwk
|
19
23
|
|
20
|
-
|
24
|
+
jwk.keypair
|
21
25
|
end
|
22
26
|
|
23
27
|
private
|
24
28
|
|
25
29
|
def resolve_key(kid)
|
26
|
-
|
30
|
+
# First try without invalidation to facilitate application caching
|
31
|
+
@jwks ||= JWT::JWK::Set.new(@jwks_loader.call(kid: kid))
|
32
|
+
jwk = @jwks.find { |key| key[:kid] == kid }
|
27
33
|
|
28
34
|
return jwk if jwk
|
29
35
|
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
36
|
+
# Second try, invalidate for backwards compatibility
|
37
|
+
@jwks = JWT::JWK::Set.new(@jwks_loader.call(invalidate: true, kid_not_found: true, kid: kid))
|
38
|
+
@jwks.find { |key| key[:kid] == kid }
|
59
39
|
end
|
60
40
|
end
|
61
41
|
end
|