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.
- checksums.yaml +5 -5
- data/.codeclimate.yml +6 -18
- data/.github/workflows/coverage.yml +27 -0
- data/.github/workflows/test.yml +67 -0
- data/.gitignore +3 -1
- data/.reek.yml +21 -39
- data/.rspec +1 -0
- data/.rubocop.yml +21 -52
- data/{.ebert.yml → .sourcelevel.yml} +3 -4
- data/AUTHORS +119 -0
- data/Appraisals +13 -0
- data/CHANGELOG.md +329 -19
- data/CODE_OF_CONDUCT.md +84 -0
- data/CONTRIBUTING.md +99 -0
- data/Gemfile +4 -0
- data/README.md +261 -100
- data/Rakefile +6 -1
- data/lib/jwt/algos/ecdsa.rb +37 -8
- data/lib/jwt/algos/eddsa.rb +16 -4
- data/lib/jwt/algos/hmac.rb +3 -0
- data/lib/jwt/algos/none.rb +17 -0
- data/lib/jwt/algos/ps.rb +43 -0
- data/lib/jwt/algos/rsa.rb +4 -1
- data/lib/jwt/algos/unsupported.rb +7 -4
- data/lib/jwt/algos.rb +44 -0
- data/lib/jwt/base64.rb +19 -0
- data/lib/jwt/claims_validator.rb +37 -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 +120 -24
- data/lib/jwt/encode.rb +43 -25
- data/lib/jwt/error.rb +6 -0
- data/lib/jwt/json.rb +18 -0
- data/lib/jwt/jwk/ec.rb +199 -0
- data/lib/jwt/jwk/hmac.rb +67 -0
- data/lib/jwt/jwk/key_base.rb +35 -0
- data/lib/jwt/jwk/key_finder.rb +62 -0
- data/lib/jwt/jwk/kid_as_key_digest.rb +15 -0
- data/lib/jwt/jwk/rsa.rb +138 -0
- data/lib/jwt/jwk/thumbprint.rb +26 -0
- data/lib/jwt/jwk.rb +52 -0
- data/lib/jwt/security_utils.rb +8 -0
- data/lib/jwt/signature.rb +7 -22
- data/lib/jwt/verify.rb +19 -8
- data/lib/jwt/version.rb +6 -2
- data/lib/jwt/x5c_key_finder.rb +55 -0
- data/lib/jwt.rb +12 -44
- data/ruby-jwt.gemspec +13 -9
- metadata +44 -97
- data/.travis.yml +0 -14
- data/Manifest +0 -8
- data/lib/jwt/default_options.rb +0 -15
- data/spec/fixtures/certs/ec256-private.pem +0 -8
- data/spec/fixtures/certs/ec256-public.pem +0 -4
- data/spec/fixtures/certs/ec256-wrong-private.pem +0 -8
- data/spec/fixtures/certs/ec256-wrong-public.pem +0 -4
- data/spec/fixtures/certs/ec384-private.pem +0 -9
- data/spec/fixtures/certs/ec384-public.pem +0 -5
- data/spec/fixtures/certs/ec384-wrong-private.pem +0 -9
- data/spec/fixtures/certs/ec384-wrong-public.pem +0 -5
- data/spec/fixtures/certs/ec512-private.pem +0 -10
- data/spec/fixtures/certs/ec512-public.pem +0 -6
- data/spec/fixtures/certs/ec512-wrong-private.pem +0 -10
- data/spec/fixtures/certs/ec512-wrong-public.pem +0 -6
- data/spec/fixtures/certs/rsa-1024-private.pem +0 -15
- data/spec/fixtures/certs/rsa-1024-public.pem +0 -6
- data/spec/fixtures/certs/rsa-2048-private.pem +0 -27
- data/spec/fixtures/certs/rsa-2048-public.pem +0 -9
- data/spec/fixtures/certs/rsa-2048-wrong-private.pem +0 -27
- data/spec/fixtures/certs/rsa-2048-wrong-public.pem +0 -9
- data/spec/fixtures/certs/rsa-4096-private.pem +0 -51
- data/spec/fixtures/certs/rsa-4096-public.pem +0 -14
- data/spec/integration/readme_examples_spec.rb +0 -202
- data/spec/jwt/verify_spec.rb +0 -232
- data/spec/jwt_spec.rb +0 -315
- 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
|
-
|
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
|
-
|
10
|
+
ALG_NONE = 'none'
|
11
|
+
ALG_KEY = 'alg'
|
10
12
|
|
11
|
-
def
|
12
|
-
|
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
|
16
|
-
@
|
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
|
-
|
27
|
-
Encode.base64url_encode(JSON.generate(header))
|
27
|
+
@encoded_header ||= encode_header
|
28
28
|
end
|
29
29
|
|
30
30
|
def encoded_payload
|
31
|
-
|
32
|
-
Encode.base64url_encode(JSON.generate(@payload))
|
31
|
+
@encoded_payload ||= encode_payload
|
33
32
|
end
|
34
33
|
|
35
|
-
def encoded_signature
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
45
|
-
|
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
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
|
data/lib/jwt/jwk/hmac.rb
ADDED
@@ -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
|
data/lib/jwt/jwk/rsa.rb
ADDED
@@ -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
|