jwt 2.2.1 → 2.7.1
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 +79 -44
- data/CHANGELOG.md +271 -20
- data/CODE_OF_CONDUCT.md +84 -0
- data/CONTRIBUTING.md +99 -0
- data/README.md +253 -35
- data/lib/jwt/algos/algo_wrapper.rb +26 -0
- data/lib/jwt/algos/ecdsa.rb +55 -14
- data/lib/jwt/algos/eddsa.rb +18 -8
- data/lib/jwt/algos/hmac.rb +57 -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 +19 -0
- data/lib/jwt/algos/ps.rb +10 -12
- data/lib/jwt/algos/rsa.rb +9 -5
- data/lib/jwt/algos/unsupported.rb +7 -4
- data/lib/jwt/algos.rb +66 -0
- data/lib/jwt/claims_validator.rb +12 -8
- 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 +85 -17
- data/lib/jwt/encode.rb +30 -19
- data/lib/jwt/error.rb +16 -14
- data/lib/jwt/jwk/ec.rb +236 -0
- data/lib/jwt/jwk/hmac.rb +103 -0
- data/lib/jwt/jwk/key_base.rb +55 -0
- data/lib/jwt/jwk/key_finder.rb +19 -30
- 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 +181 -25
- data/lib/jwt/jwk/set.rb +80 -0
- data/lib/jwt/jwk/thumbprint.rb +26 -0
- data/lib/jwt/jwk.rb +39 -15
- data/lib/jwt/verify.rb +18 -3
- data/lib/jwt/version.rb +23 -3
- data/lib/jwt/x5c_key_finder.rb +55 -0
- data/lib/jwt.rb +5 -4
- data/ruby-jwt.gemspec +15 -10
- metadata +30 -90
- data/.codeclimate.yml +0 -20
- data/.ebert.yml +0 -18
- data/.gitignore +0 -11
- data/.rspec +0 -1
- data/.rubocop.yml +0 -98
- data/.travis.yml +0 -20
- data/Appraisals +0 -14
- data/Gemfile +0 -3
- data/Rakefile +0 -11
- data/lib/jwt/default_options.rb +0 -15
- data/lib/jwt/security_utils.rb +0 -57
- data/lib/jwt/signature.rb +0 -52
data/lib/jwt/decode.rb
CHANGED
@@ -2,14 +2,16 @@
|
|
2
2
|
|
3
3
|
require 'json'
|
4
4
|
|
5
|
-
require 'jwt/signature'
|
6
5
|
require 'jwt/verify'
|
6
|
+
require 'jwt/x5c_key_finder'
|
7
|
+
|
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
|
@@ -22,51 +24,109 @@ module JWT
|
|
22
24
|
def decode_segments
|
23
25
|
validate_segment_count!
|
24
26
|
if @verify
|
25
|
-
|
27
|
+
decode_signature
|
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
|
36
|
-
@key
|
37
|
-
@key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks]).key_for(header['kid']) if @options[:jwks]
|
41
|
+
return unless @key || @verify
|
38
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
|
39
53
|
raise(JWT::IncorrectAlgorithm, 'An algorithm must be specified') if allowed_algorithms.empty?
|
40
|
-
raise(JWT::IncorrectAlgorithm, '
|
54
|
+
raise(JWT::IncorrectAlgorithm, 'Token is missing alg header') unless alg_in_header
|
55
|
+
raise(JWT::IncorrectAlgorithm, 'Expected a different algorithm') if allowed_and_valid_algorithms.empty?
|
56
|
+
end
|
57
|
+
|
58
|
+
def set_key
|
59
|
+
@key = find_key(&@keyfinder) if @keyfinder
|
60
|
+
@key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks], allow_nil_kid: @options[:allow_nil_kid]).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
|
65
|
+
|
66
|
+
def verify_signature_for?(key)
|
67
|
+
allowed_and_valid_algorithms.any? do |alg|
|
68
|
+
alg.verify(data: signing_input, signature: @signature, verification_key: key)
|
69
|
+
end
|
70
|
+
end
|
41
71
|
|
42
|
-
|
72
|
+
def allowed_and_valid_algorithms
|
73
|
+
@allowed_and_valid_algorithms ||= allowed_algorithms.select { |alg| alg.valid_alg?(alg_in_header) }
|
43
74
|
end
|
44
75
|
|
45
|
-
|
46
|
-
|
76
|
+
# Order is very important - first check for string keys, next for symbols
|
77
|
+
ALGORITHM_KEYS = ['algorithm',
|
78
|
+
:algorithm,
|
79
|
+
'algorithms',
|
80
|
+
:algorithms].freeze
|
81
|
+
|
82
|
+
def given_algorithms
|
83
|
+
ALGORITHM_KEYS.each do |alg_key|
|
84
|
+
alg = @options[alg_key]
|
85
|
+
return Array(alg) if alg
|
86
|
+
end
|
87
|
+
[]
|
47
88
|
end
|
48
89
|
|
49
90
|
def allowed_algorithms
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
91
|
+
@allowed_algorithms ||= resolve_allowed_algorithms
|
92
|
+
end
|
93
|
+
|
94
|
+
def resolve_allowed_algorithms
|
95
|
+
algs = given_algorithms.map do |alg|
|
96
|
+
if Algos.implementation?(alg)
|
97
|
+
alg
|
98
|
+
else
|
99
|
+
Algos.create(alg)
|
100
|
+
end
|
54
101
|
end
|
102
|
+
|
103
|
+
sort_by_alg_header(algs)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Move algorithms matching the JWT alg header to the beginning of the list
|
107
|
+
def sort_by_alg_header(algs)
|
108
|
+
return algs if algs.size <= 1
|
109
|
+
|
110
|
+
algs.partition { |alg| alg.valid_alg?(alg_in_header) }.flatten
|
55
111
|
end
|
56
112
|
|
57
113
|
def find_key(&keyfinder)
|
58
114
|
key = (keyfinder.arity == 2 ? yield(header, payload) : yield(header))
|
59
|
-
|
60
|
-
key
|
115
|
+
# key can be of type [string, nil, OpenSSL::PKey, Array]
|
116
|
+
return key if key && !Array(key).empty?
|
117
|
+
|
118
|
+
raise JWT::DecodeError, 'No verification key available'
|
61
119
|
end
|
62
120
|
|
63
121
|
def verify_claims
|
64
122
|
Verify.verify_claims(payload, @options)
|
123
|
+
Verify.verify_required_claims(payload, @options)
|
65
124
|
end
|
66
125
|
|
67
126
|
def validate_segment_count!
|
68
127
|
return if segment_length == 3
|
69
128
|
return if !@verify && segment_length == 2 # If no verifying required, the signature is not needed
|
129
|
+
return if segment_length == 2 && none_algorithm?
|
70
130
|
|
71
131
|
raise(JWT::DecodeError, 'Not enough or too many segments')
|
72
132
|
end
|
@@ -75,8 +135,16 @@ module JWT
|
|
75
135
|
@segments.count
|
76
136
|
end
|
77
137
|
|
78
|
-
def
|
79
|
-
|
138
|
+
def none_algorithm?
|
139
|
+
alg_in_header == 'none'
|
140
|
+
end
|
141
|
+
|
142
|
+
def decode_signature
|
143
|
+
@signature = ::JWT::Base64.url_decode(@segments[2] || '')
|
144
|
+
end
|
145
|
+
|
146
|
+
def alg_in_header
|
147
|
+
header['alg']
|
80
148
|
end
|
81
149
|
|
82
150
|
def header
|
@@ -92,7 +160,7 @@ module JWT
|
|
92
160
|
end
|
93
161
|
|
94
162
|
def parse_and_decode(segment)
|
95
|
-
JWT::JSON.parse(JWT::Base64.url_decode(segment))
|
163
|
+
JWT::JSON.parse(::JWT::Base64.url_decode(segment))
|
96
164
|
rescue ::JSON::ParserError
|
97
165
|
raise JWT::DecodeError, 'Invalid segment encoding'
|
98
166
|
end
|
data/lib/jwt/encode.rb
CHANGED
@@ -1,27 +1,35 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative '
|
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_KEY = 'alg'.freeze
|
10
|
+
ALG_KEY = 'alg'
|
11
11
|
|
12
12
|
def initialize(options)
|
13
|
-
@payload
|
14
|
-
@key
|
15
|
-
@algorithm
|
16
|
-
@headers
|
13
|
+
@payload = options[:payload]
|
14
|
+
@key = options[:key]
|
15
|
+
@algorithm = resolve_algorithm(options[:algorithm])
|
16
|
+
@headers = options[:headers].transform_keys(&:to_s)
|
17
|
+
@headers[ALG_KEY] = @algorithm.alg
|
17
18
|
end
|
18
19
|
|
19
20
|
def segments
|
20
|
-
|
21
|
+
validate_claims!
|
22
|
+
combine(encoded_header_and_payload, encoded_signature)
|
21
23
|
end
|
22
24
|
|
23
25
|
private
|
24
26
|
|
27
|
+
def resolve_algorithm(algorithm)
|
28
|
+
return algorithm if Algos.implementation?(algorithm)
|
29
|
+
|
30
|
+
Algos.create(algorithm)
|
31
|
+
end
|
32
|
+
|
25
33
|
def encoded_header
|
26
34
|
@encoded_header ||= encode_header
|
27
35
|
end
|
@@ -39,26 +47,29 @@ module JWT
|
|
39
47
|
end
|
40
48
|
|
41
49
|
def encode_header
|
42
|
-
@headers
|
43
|
-
encode(@headers)
|
50
|
+
encode_data(@headers)
|
44
51
|
end
|
45
52
|
|
46
53
|
def encode_payload
|
47
|
-
|
48
|
-
|
49
|
-
end
|
54
|
+
encode_data(@payload)
|
55
|
+
end
|
50
56
|
|
51
|
-
|
57
|
+
def signature
|
58
|
+
@algorithm.sign(data: encoded_header_and_payload, signing_key: @key)
|
52
59
|
end
|
53
60
|
|
54
|
-
def
|
55
|
-
return
|
61
|
+
def validate_claims!
|
62
|
+
return unless @payload.is_a?(Hash)
|
63
|
+
|
64
|
+
ClaimsValidator.new(@payload).validate!
|
65
|
+
end
|
56
66
|
|
57
|
-
|
67
|
+
def encode_signature
|
68
|
+
::JWT::Base64.url_encode(signature)
|
58
69
|
end
|
59
70
|
|
60
|
-
def
|
61
|
-
JWT::Base64.url_encode(JWT::JSON.generate(data))
|
71
|
+
def encode_data(data)
|
72
|
+
::JWT::Base64.url_encode(JWT::JSON.generate(data))
|
62
73
|
end
|
63
74
|
|
64
75
|
def combine(*parts)
|
data/lib/jwt/error.rb
CHANGED
@@ -1,20 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module JWT
|
4
|
-
EncodeError
|
5
|
-
DecodeError
|
6
|
-
RequiredDependencyError
|
4
|
+
class EncodeError < StandardError; end
|
5
|
+
class DecodeError < StandardError; end
|
6
|
+
class RequiredDependencyError < StandardError; end
|
7
7
|
|
8
|
-
VerificationError
|
9
|
-
ExpiredSignature
|
10
|
-
IncorrectAlgorithm
|
11
|
-
ImmatureSignature
|
12
|
-
InvalidIssuerError
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
8
|
+
class VerificationError < DecodeError; end
|
9
|
+
class ExpiredSignature < DecodeError; end
|
10
|
+
class IncorrectAlgorithm < DecodeError; end
|
11
|
+
class ImmatureSignature < DecodeError; end
|
12
|
+
class InvalidIssuerError < DecodeError; end
|
13
|
+
class UnsupportedEcdsaCurve < IncorrectAlgorithm; end
|
14
|
+
class InvalidIatError < DecodeError; end
|
15
|
+
class InvalidAudError < DecodeError; end
|
16
|
+
class InvalidSubError < DecodeError; end
|
17
|
+
class InvalidJtiError < DecodeError; end
|
18
|
+
class InvalidPayload < DecodeError; end
|
19
|
+
class MissingRequiredClaim < DecodeError; end
|
18
20
|
|
19
|
-
JWKError
|
21
|
+
class JWKError < DecodeError; end
|
20
22
|
end
|
data/lib/jwt/jwk/ec.rb
ADDED
@@ -0,0 +1,236 @@
|
|
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
|
+
KTY = 'EC'
|
9
|
+
KTYS = [KTY, OpenSSL::PKey::EC, JWT::JWK::EC].freeze
|
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)
|
25
|
+
|
26
|
+
super(options, key_params.merge(params))
|
27
|
+
end
|
28
|
+
|
29
|
+
def keypair
|
30
|
+
ec_key
|
31
|
+
end
|
32
|
+
|
33
|
+
def private?
|
34
|
+
ec_key.private_key?
|
35
|
+
end
|
36
|
+
|
37
|
+
def signing_key
|
38
|
+
ec_key
|
39
|
+
end
|
40
|
+
|
41
|
+
def verify_key
|
42
|
+
ec_key
|
43
|
+
end
|
44
|
+
|
45
|
+
def public_key
|
46
|
+
ec_key
|
47
|
+
end
|
48
|
+
|
49
|
+
def members
|
50
|
+
EC_PUBLIC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] }
|
51
|
+
end
|
52
|
+
|
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)
|
61
|
+
sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(x_octets, BINARY)),
|
62
|
+
OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(y_octets, BINARY))])
|
63
|
+
OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
|
64
|
+
end
|
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
|
+
|
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
|
+
|
147
|
+
x_octets = decode_octets(jwk_x)
|
148
|
+
y_octets = decode_octets(jwk_y)
|
149
|
+
|
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
|
+
)
|
154
|
+
|
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
|
+
])
|
175
|
+
end
|
176
|
+
|
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)
|
182
|
+
|
183
|
+
x_octets = decode_octets(jwk_x)
|
184
|
+
y_octets = decode_octets(jwk_y)
|
185
|
+
|
186
|
+
key = OpenSSL::PKey::EC.new(curve)
|
187
|
+
|
188
|
+
# The details of the `Point` instantiation are covered in:
|
189
|
+
# - https://docs.ruby-lang.org/en/2.4.0/OpenSSL/PKey/EC.html
|
190
|
+
# - https://www.openssl.org/docs/manmaster/man3/EC_POINT_new.html
|
191
|
+
# - https://tools.ietf.org/html/rfc5480#section-2.2
|
192
|
+
# - https://www.secg.org/SEC1-Ver-1.0.pdf
|
193
|
+
# Section 2.3.3 of the last of these references specifies that the
|
194
|
+
# encoding of an uncompressed point consists of the byte `0x04` followed
|
195
|
+
# by the x value then the y value.
|
196
|
+
point = OpenSSL::PKey::EC::Point.new(
|
197
|
+
OpenSSL::PKey::EC::Group.new(curve),
|
198
|
+
OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2)
|
199
|
+
)
|
200
|
+
|
201
|
+
key.public_key = point
|
202
|
+
key.private_key = OpenSSL::BN.new(decode_octets(jwk_d), 2) if jwk_d
|
203
|
+
|
204
|
+
key
|
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
|
215
|
+
|
216
|
+
class << self
|
217
|
+
def import(jwk_data)
|
218
|
+
new(jwk_data)
|
219
|
+
end
|
220
|
+
|
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
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
data/lib/jwt/jwk/hmac.rb
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module JWK
|
5
|
+
class HMAC < KeyBase
|
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
|
11
|
+
|
12
|
+
def initialize(key, params = nil, options = {})
|
13
|
+
params ||= {}
|
14
|
+
|
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
|
28
|
+
end
|
29
|
+
|
30
|
+
def private?
|
31
|
+
true
|
32
|
+
end
|
33
|
+
|
34
|
+
def public_key
|
35
|
+
nil
|
36
|
+
end
|
37
|
+
|
38
|
+
def verify_key
|
39
|
+
secret
|
40
|
+
end
|
41
|
+
|
42
|
+
def signing_key
|
43
|
+
secret
|
44
|
+
end
|
45
|
+
|
46
|
+
# See https://tools.ietf.org/html/rfc7517#appendix-A.3
|
47
|
+
def export(options = {})
|
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
|
62
|
+
|
63
|
+
def []=(key, value)
|
64
|
+
if HMAC_KEY_ELEMENTS.include?(key.to_sym)
|
65
|
+
raise ArgumentError, 'cannot overwrite cryptographic key attributes'
|
66
|
+
end
|
67
|
+
|
68
|
+
super(key, value)
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def secret
|
74
|
+
self[:k]
|
75
|
+
end
|
76
|
+
|
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
|
89
|
+
|
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
|
95
|
+
|
96
|
+
class << self
|
97
|
+
def import(jwk_data)
|
98
|
+
new(jwk_data)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,55 @@
|
|
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, 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
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|