jwt 2.3.0 → 2.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.codeclimate.yml +8 -0
- data/.github/workflows/coverage.yml +27 -0
- data/.github/workflows/test.yml +15 -22
- data/.gitignore +2 -0
- data/.reek.yml +22 -0
- data/.rubocop.yml +17 -47
- data/.sourcelevel.yml +3 -4
- data/AUTHORS +60 -53
- data/Appraisals +3 -0
- data/CHANGELOG.md +47 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/CONTRIBUTING.md +99 -0
- data/Gemfile +3 -1
- data/README.md +114 -34
- data/Rakefile +2 -0
- data/lib/jwt/algos/ecdsa.rb +37 -8
- data/lib/jwt/algos/eddsa.rb +5 -0
- data/lib/jwt/algos/hmac.rb +2 -0
- data/lib/jwt/algos/none.rb +2 -0
- data/lib/jwt/algos/ps.rb +3 -3
- data/lib/jwt/algos/rsa.rb +4 -1
- data/lib/jwt/algos/unsupported.rb +2 -0
- 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 +42 -8
- data/lib/jwt/encode.rb +6 -6
- data/lib/jwt/error.rb +1 -0
- data/lib/jwt/jwk/ec.rb +92 -43
- data/lib/jwt/jwk/hmac.rb +19 -10
- data/lib/jwt/jwk/key_base.rb +23 -6
- data/lib/jwt/jwk/key_finder.rb +1 -1
- data/lib/jwt/jwk/kid_as_key_digest.rb +15 -0
- data/lib/jwt/jwk/rsa.rb +54 -31
- data/lib/jwt/jwk/thumbprint.rb +26 -0
- data/lib/jwt/jwk.rb +1 -0
- data/lib/jwt/security_utils.rb +2 -0
- data/lib/jwt/signature.rb +3 -7
- data/lib/jwt/verify.rb +10 -2
- data/lib/jwt/version.rb +6 -2
- data/lib/jwt/x5c_key_finder.rb +55 -0
- data/lib/jwt.rb +5 -4
- data/ruby-jwt.gemspec +6 -3
- metadata +31 -7
- data/.rubocop_todo.yml +0 -185
- data/lib/jwt/default_options.rb +0 -16
data/lib/jwt/decode.rb
CHANGED
@@ -4,12 +4,14 @@ require 'json'
|
|
4
4
|
|
5
5
|
require 'jwt/signature'
|
6
6
|
require 'jwt/verify'
|
7
|
+
require 'jwt/x5c_key_finder'
|
7
8
|
# JWT::Decode module
|
8
9
|
module JWT
|
9
10
|
# Decoding logic for JWT
|
10
11
|
class Decode
|
11
12
|
def initialize(jwt, key, verify, options, &keyfinder)
|
12
13
|
raise(JWT::DecodeError, 'Nil JSON web token') unless jwt
|
14
|
+
|
13
15
|
@jwt = jwt
|
14
16
|
@key = key
|
15
17
|
@options = options
|
@@ -23,28 +25,50 @@ module JWT
|
|
23
25
|
validate_segment_count!
|
24
26
|
if @verify
|
25
27
|
decode_crypto
|
28
|
+
verify_algo
|
29
|
+
set_key
|
26
30
|
verify_signature
|
27
31
|
verify_claims
|
28
32
|
end
|
29
33
|
raise(JWT::DecodeError, 'Not enough or too many segments') unless header && payload
|
34
|
+
|
30
35
|
[payload, header]
|
31
36
|
end
|
32
37
|
|
33
38
|
private
|
34
39
|
|
35
40
|
def verify_signature
|
41
|
+
return unless @key || @verify
|
42
|
+
|
43
|
+
return if none_algorithm?
|
44
|
+
|
45
|
+
raise JWT::DecodeError, 'No verification key available' unless @key
|
46
|
+
|
47
|
+
return if Array(@key).any? { |key| verify_signature_for?(key) }
|
48
|
+
|
49
|
+
raise(JWT::VerificationError, 'Signature verification failed')
|
50
|
+
end
|
51
|
+
|
52
|
+
def verify_algo
|
36
53
|
raise(JWT::IncorrectAlgorithm, 'An algorithm must be specified') if allowed_algorithms.empty?
|
37
|
-
raise(JWT::IncorrectAlgorithm, 'Token is missing alg header') unless
|
54
|
+
raise(JWT::IncorrectAlgorithm, 'Token is missing alg header') unless algorithm
|
38
55
|
raise(JWT::IncorrectAlgorithm, 'Expected a different algorithm') unless options_includes_algo_in_header?
|
56
|
+
end
|
39
57
|
|
58
|
+
def set_key
|
40
59
|
@key = find_key(&@keyfinder) if @keyfinder
|
41
60
|
@key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks]).key_for(header['kid']) if @options[:jwks]
|
61
|
+
if (x5c_options = @options[:x5c])
|
62
|
+
@key = X5cKeyFinder.new(x5c_options[:root_certificates], x5c_options[:crls]).from(header['x5c'])
|
63
|
+
end
|
64
|
+
end
|
42
65
|
|
43
|
-
|
66
|
+
def verify_signature_for?(key)
|
67
|
+
Signature.verify(algorithm, key, signing_input, @signature)
|
44
68
|
end
|
45
69
|
|
46
70
|
def options_includes_algo_in_header?
|
47
|
-
allowed_algorithms.any? { |alg| alg.casecmp(
|
71
|
+
allowed_algorithms.any? { |alg| alg.casecmp(algorithm).zero? }
|
48
72
|
end
|
49
73
|
|
50
74
|
def allowed_algorithms
|
@@ -65,8 +89,10 @@ module JWT
|
|
65
89
|
|
66
90
|
def find_key(&keyfinder)
|
67
91
|
key = (keyfinder.arity == 2 ? yield(header, payload) : yield(header))
|
68
|
-
|
69
|
-
key
|
92
|
+
# key can be of type [string, nil, OpenSSL::PKey, Array]
|
93
|
+
return key if key && !Array(key).empty?
|
94
|
+
|
95
|
+
raise JWT::DecodeError, 'No verification key available'
|
70
96
|
end
|
71
97
|
|
72
98
|
def verify_claims
|
@@ -77,7 +103,7 @@ module JWT
|
|
77
103
|
def validate_segment_count!
|
78
104
|
return if segment_length == 3
|
79
105
|
return if !@verify && segment_length == 2 # If no verifying required, the signature is not needed
|
80
|
-
return if segment_length == 2 &&
|
106
|
+
return if segment_length == 2 && none_algorithm?
|
81
107
|
|
82
108
|
raise(JWT::DecodeError, 'Not enough or too many segments')
|
83
109
|
end
|
@@ -86,8 +112,16 @@ module JWT
|
|
86
112
|
@segments.count
|
87
113
|
end
|
88
114
|
|
115
|
+
def none_algorithm?
|
116
|
+
algorithm == 'none'
|
117
|
+
end
|
118
|
+
|
89
119
|
def decode_crypto
|
90
|
-
@signature = JWT::Base64.url_decode(@segments[2] || '')
|
120
|
+
@signature = ::JWT::Base64.url_decode(@segments[2] || '')
|
121
|
+
end
|
122
|
+
|
123
|
+
def algorithm
|
124
|
+
header['alg']
|
91
125
|
end
|
92
126
|
|
93
127
|
def header
|
@@ -103,7 +137,7 @@ module JWT
|
|
103
137
|
end
|
104
138
|
|
105
139
|
def parse_and_decode(segment)
|
106
|
-
JWT::JSON.parse(JWT::Base64.url_decode(segment))
|
140
|
+
JWT::JSON.parse(::JWT::Base64.url_decode(segment))
|
107
141
|
rescue ::JSON::ParserError
|
108
142
|
raise JWT::DecodeError, 'Invalid segment encoding'
|
109
143
|
end
|
data/lib/jwt/encode.rb
CHANGED
@@ -7,14 +7,14 @@ require_relative './claims_validator'
|
|
7
7
|
module JWT
|
8
8
|
# Encoding logic for JWT
|
9
9
|
class Encode
|
10
|
-
ALG_NONE = 'none'
|
11
|
-
ALG_KEY = 'alg'
|
10
|
+
ALG_NONE = 'none'
|
11
|
+
ALG_KEY = 'alg'
|
12
12
|
|
13
13
|
def initialize(options)
|
14
14
|
@payload = options[:payload]
|
15
15
|
@key = options[:key]
|
16
16
|
_, @algorithm = Algos.find(options[:algorithm])
|
17
|
-
@headers = options[:headers].
|
17
|
+
@headers = options[:headers].transform_keys(&:to_s)
|
18
18
|
end
|
19
19
|
|
20
20
|
def segments
|
@@ -45,7 +45,7 @@ module JWT
|
|
45
45
|
end
|
46
46
|
|
47
47
|
def encode_payload
|
48
|
-
if @payload
|
48
|
+
if @payload.is_a?(Hash)
|
49
49
|
ClaimsValidator.new(@payload).validate!
|
50
50
|
end
|
51
51
|
|
@@ -55,11 +55,11 @@ module JWT
|
|
55
55
|
def encode_signature
|
56
56
|
return '' if @algorithm == ALG_NONE
|
57
57
|
|
58
|
-
JWT::Base64.url_encode(JWT::Signature.sign(@algorithm, encoded_header_and_payload, @key))
|
58
|
+
::JWT::Base64.url_encode(JWT::Signature.sign(@algorithm, encoded_header_and_payload, @key))
|
59
59
|
end
|
60
60
|
|
61
61
|
def encode(data)
|
62
|
-
JWT::Base64.url_encode(JWT::JSON.generate(data))
|
62
|
+
::JWT::Base64.url_encode(JWT::JSON.generate(data))
|
63
63
|
end
|
64
64
|
|
65
65
|
def combine(*parts)
|
data/lib/jwt/error.rb
CHANGED
@@ -10,6 +10,7 @@ module JWT
|
|
10
10
|
class IncorrectAlgorithm < DecodeError; end
|
11
11
|
class ImmatureSignature < DecodeError; end
|
12
12
|
class InvalidIssuerError < DecodeError; end
|
13
|
+
class UnsupportedEcdsaCurve < IncorrectAlgorithm; end
|
13
14
|
class InvalidIatError < DecodeError; end
|
14
15
|
class InvalidAudError < DecodeError; end
|
15
16
|
class InvalidSubError < DecodeError; end
|
data/lib/jwt/jwk/ec.rb
CHANGED
@@ -4,39 +4,53 @@ 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
|
-
KTY = 'EC'
|
11
|
+
KTY = 'EC'
|
12
12
|
KTYS = [KTY, OpenSSL::PKey::EC].freeze
|
13
13
|
BINARY = 2
|
14
14
|
|
15
|
-
|
15
|
+
attr_reader :keypair
|
16
|
+
|
17
|
+
def initialize(keypair, options = {})
|
16
18
|
raise ArgumentError, 'keypair must be of type OpenSSL::PKey::EC' unless keypair.is_a?(OpenSSL::PKey::EC)
|
17
19
|
|
18
|
-
|
19
|
-
|
20
|
+
@keypair = keypair
|
21
|
+
|
22
|
+
super(options)
|
20
23
|
end
|
21
24
|
|
22
25
|
def private?
|
23
26
|
@keypair.private_key?
|
24
27
|
end
|
25
28
|
|
26
|
-
def
|
29
|
+
def members
|
27
30
|
crv, x_octets, y_octets = keypair_components(keypair)
|
28
|
-
|
31
|
+
{
|
29
32
|
kty: KTY,
|
30
33
|
crv: crv,
|
31
34
|
x: encode_octets(x_octets),
|
32
|
-
y: encode_octets(y_octets)
|
33
|
-
kid: kid
|
35
|
+
y: encode_octets(y_octets)
|
34
36
|
}
|
37
|
+
end
|
38
|
+
|
39
|
+
def export(options = {})
|
40
|
+
exported_hash = members.merge(kid: kid)
|
41
|
+
|
35
42
|
return exported_hash unless private? && options[:include_private] == true
|
36
43
|
|
37
44
|
append_private_parts(exported_hash)
|
38
45
|
end
|
39
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
|
+
|
40
54
|
private
|
41
55
|
|
42
56
|
def append_private_parts(the_hash)
|
@@ -46,19 +60,15 @@ module JWT
|
|
46
60
|
)
|
47
61
|
end
|
48
62
|
|
49
|
-
def generate_kid(ec_keypair)
|
50
|
-
_crv, x_octets, y_octets = keypair_components(ec_keypair)
|
51
|
-
sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(x_octets, BINARY)),
|
52
|
-
OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(y_octets, BINARY))])
|
53
|
-
OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
|
54
|
-
end
|
55
|
-
|
56
63
|
def keypair_components(ec_keypair)
|
57
64
|
encoded_point = ec_keypair.public_key.to_bn.to_s(BINARY)
|
58
65
|
case ec_keypair.group.curve_name
|
59
66
|
when 'prime256v1'
|
60
67
|
crv = 'P-256'
|
61
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')
|
62
72
|
when 'secp384r1'
|
63
73
|
crv = 'P-384'
|
64
74
|
x_octets, y_octets = encoded_point.unpack('xa48a48')
|
@@ -87,7 +97,7 @@ module JWT
|
|
87
97
|
jwk_crv, jwk_x, jwk_y, jwk_d, jwk_kid = jwk_attrs(jwk_data, %i[crv x y d kid])
|
88
98
|
raise JWT::JWKError, 'Key format is invalid for EC' unless jwk_crv && jwk_x && jwk_y
|
89
99
|
|
90
|
-
new(ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d), jwk_kid)
|
100
|
+
new(ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d), kid: jwk_kid)
|
91
101
|
end
|
92
102
|
|
93
103
|
def to_openssl_curve(crv)
|
@@ -98,6 +108,7 @@ module JWT
|
|
98
108
|
when 'P-256' then 'prime256v1'
|
99
109
|
when 'P-384' then 'secp384r1'
|
100
110
|
when 'P-521' then 'secp521r1'
|
111
|
+
when 'P-256K' then 'secp256k1'
|
101
112
|
else raise JWT::JWKError, 'Invalid curve provided'
|
102
113
|
end
|
103
114
|
end
|
@@ -110,31 +121,69 @@ module JWT
|
|
110
121
|
end
|
111
122
|
end
|
112
123
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
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
|
138
187
|
end
|
139
188
|
|
140
189
|
def decode_octets(jwk_data)
|
data/lib/jwt/jwk/hmac.rb
CHANGED
@@ -3,14 +3,16 @@
|
|
3
3
|
module JWT
|
4
4
|
module JWK
|
5
5
|
class HMAC < KeyBase
|
6
|
-
KTY
|
6
|
+
KTY = 'oct'
|
7
7
|
KTYS = [KTY, String].freeze
|
8
8
|
|
9
|
-
|
10
|
-
raise ArgumentError, 'keypair must be of type String' unless keypair.is_a?(String)
|
9
|
+
attr_reader :signing_key
|
11
10
|
|
12
|
-
|
13
|
-
|
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)
|
14
16
|
end
|
15
17
|
|
16
18
|
def private?
|
@@ -31,14 +33,21 @@ module JWT
|
|
31
33
|
return exported_hash unless private? && options[:include_private] == true
|
32
34
|
|
33
35
|
exported_hash.merge(
|
34
|
-
k:
|
36
|
+
k: signing_key
|
35
37
|
)
|
36
38
|
end
|
37
39
|
|
38
|
-
|
40
|
+
def members
|
41
|
+
{
|
42
|
+
kty: KTY,
|
43
|
+
k: signing_key
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
alias keypair signing_key # for backwards compatibility
|
39
48
|
|
40
|
-
def
|
41
|
-
sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::UTF8String.new(
|
49
|
+
def key_digest
|
50
|
+
sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::UTF8String.new(signing_key),
|
42
51
|
OpenSSL::ASN1::UTF8String.new(KTY)])
|
43
52
|
OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
|
44
53
|
end
|
@@ -50,7 +59,7 @@ module JWT
|
|
50
59
|
|
51
60
|
raise JWT::JWKError, 'Key format is invalid for HMAC' unless jwk_k
|
52
61
|
|
53
|
-
|
62
|
+
new(jwk_k, kid: jwk_kid)
|
54
63
|
end
|
55
64
|
end
|
56
65
|
end
|
data/lib/jwt/jwk/key_base.rb
CHANGED
@@ -3,15 +3,32 @@
|
|
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
|
7
10
|
|
8
|
-
def initialize(
|
9
|
-
|
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
|
11
20
|
end
|
12
21
|
|
13
|
-
def
|
14
|
-
|
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
|
15
32
|
end
|
16
33
|
end
|
17
34
|
end
|
data/lib/jwt/jwk/key_finder.rb
CHANGED
data/lib/jwt/jwk/rsa.rb
CHANGED
@@ -4,13 +4,18 @@ module JWT
|
|
4
4
|
module JWK
|
5
5
|
class RSA < KeyBase
|
6
6
|
BINARY = 2
|
7
|
-
KTY = 'RSA'
|
7
|
+
KTY = 'RSA'
|
8
8
|
KTYS = [KTY, OpenSSL::PKey::RSA].freeze
|
9
9
|
RSA_KEY_ELEMENTS = %i[n e d p q dp dq qi].freeze
|
10
10
|
|
11
|
-
|
11
|
+
attr_reader :keypair
|
12
|
+
|
13
|
+
def initialize(keypair, options = {})
|
12
14
|
raise ArgumentError, 'keypair must be of type OpenSSL::PKey::RSA' unless keypair.is_a?(OpenSSL::PKey::RSA)
|
13
|
-
|
15
|
+
|
16
|
+
@keypair = keypair
|
17
|
+
|
18
|
+
super(options)
|
14
19
|
end
|
15
20
|
|
16
21
|
def private?
|
@@ -22,26 +27,29 @@ module JWT
|
|
22
27
|
end
|
23
28
|
|
24
29
|
def export(options = {})
|
25
|
-
exported_hash =
|
26
|
-
kty: KTY,
|
27
|
-
n: encode_open_ssl_bn(public_key.n),
|
28
|
-
e: encode_open_ssl_bn(public_key.e),
|
29
|
-
kid: kid
|
30
|
-
}
|
30
|
+
exported_hash = members.merge(kid: kid)
|
31
31
|
|
32
32
|
return exported_hash unless private? && options[:include_private] == true
|
33
33
|
|
34
34
|
append_private_parts(exported_hash)
|
35
35
|
end
|
36
36
|
|
37
|
-
|
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
|
38
44
|
|
39
|
-
def
|
45
|
+
def key_digest
|
40
46
|
sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(public_key.n),
|
41
47
|
OpenSSL::ASN1::Integer.new(public_key.e)])
|
42
48
|
OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
|
43
49
|
end
|
44
50
|
|
51
|
+
private
|
52
|
+
|
45
53
|
def append_private_parts(the_hash)
|
46
54
|
the_hash.merge(
|
47
55
|
d: encode_open_ssl_bn(keypair.d),
|
@@ -62,8 +70,7 @@ module JWT
|
|
62
70
|
pkey_params = jwk_attributes(jwk_data, *RSA_KEY_ELEMENTS) do |value|
|
63
71
|
decode_open_ssl_bn(value)
|
64
72
|
end
|
65
|
-
kid
|
66
|
-
self.new(rsa_pkey(pkey_params), kid)
|
73
|
+
new(rsa_pkey(pkey_params), kid: jwk_attributes(jwk_data, :kid)[:kid])
|
67
74
|
end
|
68
75
|
|
69
76
|
private
|
@@ -79,28 +86,44 @@ module JWT
|
|
79
86
|
def rsa_pkey(rsa_parameters)
|
80
87
|
raise JWT::JWKError, 'Key format is invalid for RSA' unless rsa_parameters[:n] && rsa_parameters[:e]
|
81
88
|
|
82
|
-
|
89
|
+
create_rsa_key(rsa_parameters)
|
83
90
|
end
|
84
91
|
|
85
|
-
if
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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
|
91
114
|
end
|
92
115
|
else
|
93
|
-
def
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
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
|
104
127
|
end
|
105
128
|
end
|
106
129
|
|
@@ -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
|
data/lib/jwt/jwk.rb
CHANGED