jwt 2.2.2 → 2.4.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.codeclimate.yml +6 -18
- data/.github/workflows/coverage.yml +27 -0
- data/.github/workflows/test.yml +66 -0
- data/.gitignore +2 -0
- data/.rspec +1 -0
- data/.rubocop.yml +20 -37
- data/.rubocop_todo.yml +22 -0
- data/{.ebert.yml → .sourcelevel.yml} +1 -1
- data/AUTHORS +79 -44
- data/Appraisals +7 -12
- data/CHANGELOG.md +143 -5
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +4 -0
- data/README.md +135 -29
- data/Rakefile +6 -1
- data/lib/jwt/algos/ecdsa.rb +23 -5
- data/lib/jwt/algos/eddsa.rb +14 -4
- data/lib/jwt/algos/hmac.rb +2 -0
- data/lib/jwt/algos/none.rb +17 -0
- data/lib/jwt/algos/ps.rb +3 -3
- 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/claims_validator.rb +12 -8
- data/lib/jwt/decode.rb +50 -12
- data/lib/jwt/default_options.rb +4 -1
- data/lib/jwt/encode.rb +10 -9
- data/lib/jwt/error.rb +2 -0
- data/lib/jwt/jwk/ec.rb +150 -0
- data/lib/jwt/jwk/hmac.rb +58 -0
- data/lib/jwt/jwk/key_base.rb +19 -0
- data/lib/jwt/jwk/key_finder.rb +6 -1
- data/lib/jwt/jwk/rsa.rb +85 -23
- data/lib/jwt/jwk.rb +32 -11
- data/lib/jwt/security_utils.rb +2 -0
- data/lib/jwt/signature.rb +7 -26
- data/lib/jwt/verify.rb +18 -3
- data/lib/jwt/version.rb +3 -4
- data/lib/jwt/x5c_key_finder.rb +55 -0
- data/lib/jwt.rb +1 -1
- data/ruby-jwt.gemspec +9 -9
- metadata +20 -80
- data/.travis.yml +0 -29
- data/lib/jwt/base64.rb +0 -19
data/lib/jwt/algos/ecdsa.rb
CHANGED
@@ -1,35 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module JWT
|
2
4
|
module Algos
|
3
5
|
module Ecdsa
|
4
6
|
module_function
|
5
7
|
|
6
|
-
SUPPORTED = %w[ES256 ES384 ES512].freeze
|
7
8
|
NAMED_CURVES = {
|
8
9
|
'prime256v1' => 'ES256',
|
10
|
+
'secp256r1' => 'ES256', # alias for prime256v1
|
9
11
|
'secp384r1' => 'ES384',
|
10
12
|
'secp521r1' => 'ES512'
|
11
13
|
}.freeze
|
12
14
|
|
15
|
+
SUPPORTED = NAMED_CURVES.values.uniq.freeze
|
16
|
+
|
13
17
|
def sign(to_sign)
|
14
18
|
algorithm, msg, key = to_sign.values
|
15
|
-
|
19
|
+
curve_definition = curve_by_name(key.group.curve_name)
|
20
|
+
key_algorithm = curve_definition[:algorithm]
|
16
21
|
if algorithm != key_algorithm
|
17
22
|
raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} signing key was provided"
|
18
23
|
end
|
19
24
|
|
20
|
-
digest = OpenSSL::Digest.new(
|
25
|
+
digest = OpenSSL::Digest.new(curve_definition[:digest])
|
21
26
|
SecurityUtils.asn1_to_raw(key.dsa_sign_asn1(digest.digest(msg)), key)
|
22
27
|
end
|
23
28
|
|
24
29
|
def verify(to_verify)
|
25
30
|
algorithm, public_key, signing_input, signature = to_verify.values
|
26
|
-
|
31
|
+
curve_definition = curve_by_name(public_key.group.curve_name)
|
32
|
+
key_algorithm = curve_definition[:algorithm]
|
27
33
|
if algorithm != key_algorithm
|
28
34
|
raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} verification key was provided"
|
29
35
|
end
|
30
|
-
|
36
|
+
|
37
|
+
digest = OpenSSL::Digest.new(curve_definition[:digest])
|
31
38
|
public_key.dsa_verify_asn1(digest.digest(signing_input), SecurityUtils.raw_to_asn1(signature, public_key))
|
32
39
|
end
|
40
|
+
|
41
|
+
def curve_by_name(name)
|
42
|
+
algorithm = NAMED_CURVES.fetch(name) do
|
43
|
+
raise UnsupportedEcdsaCurve, "The ECDSA curve '#{name}' is not supported"
|
44
|
+
end
|
45
|
+
|
46
|
+
{
|
47
|
+
algorithm: algorithm,
|
48
|
+
digest: algorithm.sub('ES', 'sha')
|
49
|
+
}
|
50
|
+
end
|
33
51
|
end
|
34
52
|
end
|
35
53
|
end
|
data/lib/jwt/algos/eddsa.rb
CHANGED
@@ -1,21 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module JWT
|
2
4
|
module Algos
|
3
5
|
module Eddsa
|
4
6
|
module_function
|
5
7
|
|
6
|
-
SUPPORTED = %w[ED25519].freeze
|
8
|
+
SUPPORTED = %w[ED25519 EdDSA].freeze
|
7
9
|
|
8
10
|
def sign(to_sign)
|
9
11
|
algorithm, msg, key = to_sign.values
|
10
|
-
|
11
|
-
|
12
|
+
if key.class != RbNaCl::Signatures::Ed25519::SigningKey
|
13
|
+
raise EncodeError, "Key given is a #{key.class} but has to be an RbNaCl::Signatures::Ed25519::SigningKey"
|
14
|
+
end
|
15
|
+
unless SUPPORTED.map(&:downcase).map(&:to_sym).include?(algorithm.downcase.to_sym)
|
16
|
+
raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key.primitive} signing key was provided"
|
17
|
+
end
|
18
|
+
|
12
19
|
key.sign(msg)
|
13
20
|
end
|
14
21
|
|
15
22
|
def verify(to_verify)
|
16
23
|
algorithm, public_key, signing_input, signature = to_verify.values
|
17
|
-
|
24
|
+
unless SUPPORTED.map(&:downcase).map(&:to_sym).include?(algorithm.downcase.to_sym)
|
25
|
+
raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key.primitive} signing key was provided"
|
26
|
+
end
|
18
27
|
raise DecodeError, "key given is a #{public_key.class} but has to be a RbNaCl::Signatures::Ed25519::VerifyKey" if public_key.class != RbNaCl::Signatures::Ed25519::VerifyKey
|
28
|
+
|
19
29
|
public_key.verify(signature, signing_input)
|
20
30
|
end
|
21
31
|
end
|
data/lib/jwt/algos/hmac.rb
CHANGED
data/lib/jwt/algos/ps.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module JWT
|
2
4
|
module Algos
|
3
5
|
module Ps
|
@@ -29,9 +31,7 @@ module JWT
|
|
29
31
|
|
30
32
|
def require_openssl!
|
31
33
|
if Object.const_defined?('OpenSSL')
|
32
|
-
|
33
|
-
|
34
|
-
unless major.to_i >= 2 && minor.to_i >= 1
|
34
|
+
if ::Gem::Version.new(OpenSSL::VERSION) < ::Gem::Version.new('2.1')
|
35
35
|
raise JWT::RequiredDependencyError, "You currently have OpenSSL #{OpenSSL::VERSION}. PS support requires >= 2.1"
|
36
36
|
end
|
37
37
|
else
|
data/lib/jwt/algos/rsa.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module JWT
|
2
4
|
module Algos
|
3
5
|
module Rsa
|
@@ -7,7 +9,8 @@ module JWT
|
|
7
9
|
|
8
10
|
def sign(to_sign)
|
9
11
|
algorithm, msg, key = to_sign.values
|
10
|
-
raise EncodeError, "The given key is a #{key.class}. It has to be an OpenSSL::PKey::RSA instance." if key.
|
12
|
+
raise EncodeError, "The given key is a #{key.class}. It has to be an OpenSSL::PKey::RSA instance." if key.instance_of?(String)
|
13
|
+
|
11
14
|
key.sign(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), msg)
|
12
15
|
end
|
13
16
|
|
@@ -1,16 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module JWT
|
2
4
|
module Algos
|
3
5
|
module Unsupported
|
4
6
|
module_function
|
5
7
|
|
6
|
-
SUPPORTED =
|
7
|
-
def verify(*)
|
8
|
-
raise JWT::VerificationError, 'Algorithm not supported'
|
9
|
-
end
|
8
|
+
SUPPORTED = [].freeze
|
10
9
|
|
11
10
|
def sign(*)
|
12
11
|
raise NotImplementedError, 'Unsupported signing method'
|
13
12
|
end
|
13
|
+
|
14
|
+
def verify(*)
|
15
|
+
raise JWT::VerificationError, 'Algorithm not supported'
|
16
|
+
end
|
14
17
|
end
|
15
18
|
end
|
16
19
|
end
|
data/lib/jwt/algos.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'jwt/algos/hmac'
|
4
|
+
require 'jwt/algos/eddsa'
|
5
|
+
require 'jwt/algos/ecdsa'
|
6
|
+
require 'jwt/algos/rsa'
|
7
|
+
require 'jwt/algos/ps'
|
8
|
+
require 'jwt/algos/none'
|
9
|
+
require 'jwt/algos/unsupported'
|
10
|
+
|
11
|
+
# JWT::Signature module
|
12
|
+
module JWT
|
13
|
+
# Signature logic for JWT
|
14
|
+
module Algos
|
15
|
+
extend self
|
16
|
+
|
17
|
+
ALGOS = [
|
18
|
+
Algos::Hmac,
|
19
|
+
Algos::Ecdsa,
|
20
|
+
Algos::Rsa,
|
21
|
+
Algos::Eddsa,
|
22
|
+
Algos::Ps,
|
23
|
+
Algos::None,
|
24
|
+
Algos::Unsupported
|
25
|
+
].freeze
|
26
|
+
|
27
|
+
def find(algorithm)
|
28
|
+
indexed[algorithm && algorithm.downcase]
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def indexed
|
34
|
+
@indexed ||= begin
|
35
|
+
fallback = [Algos::Unsupported, nil]
|
36
|
+
ALGOS.each_with_object(Hash.new(fallback)) do |alg, hash|
|
37
|
+
alg.const_get(:SUPPORTED).each do |code|
|
38
|
+
hash[code.downcase] = [alg, code]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/jwt/claims_validator.rb
CHANGED
@@ -1,33 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require_relative './error'
|
2
4
|
|
3
5
|
module JWT
|
4
6
|
class ClaimsValidator
|
5
|
-
|
7
|
+
NUMERIC_CLAIMS = %i[
|
6
8
|
exp
|
7
9
|
iat
|
8
10
|
nbf
|
9
11
|
].freeze
|
10
12
|
|
11
13
|
def initialize(payload)
|
12
|
-
@payload = payload.
|
14
|
+
@payload = payload.transform_keys(&:to_sym)
|
13
15
|
end
|
14
16
|
|
15
17
|
def validate!
|
16
|
-
|
18
|
+
validate_numeric_claims
|
17
19
|
|
18
20
|
true
|
19
21
|
end
|
20
22
|
|
21
23
|
private
|
22
24
|
|
23
|
-
def
|
24
|
-
|
25
|
-
|
25
|
+
def validate_numeric_claims
|
26
|
+
NUMERIC_CLAIMS.each do |claim|
|
27
|
+
validate_is_numeric(claim) if @payload.key?(claim)
|
26
28
|
end
|
27
29
|
end
|
28
30
|
|
29
|
-
def
|
30
|
-
|
31
|
+
def validate_is_numeric(claim)
|
32
|
+
return if @payload[claim].is_a?(Numeric)
|
33
|
+
|
34
|
+
raise InvalidPayload, "#{claim} claim must be a Numeric value but it is a #{@payload[claim].class}"
|
31
35
|
end
|
32
36
|
end
|
33
37
|
end
|
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,57 +25,85 @@ 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?
|
54
|
+
raise(JWT::IncorrectAlgorithm, 'Token is missing alg header') unless algorithm
|
37
55
|
raise(JWT::IncorrectAlgorithm, 'Expected a different algorithm') unless options_includes_algo_in_header?
|
56
|
+
end
|
38
57
|
|
58
|
+
def set_key
|
39
59
|
@key = find_key(&@keyfinder) if @keyfinder
|
40
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
|
41
65
|
|
42
|
-
|
66
|
+
def verify_signature_for?(key)
|
67
|
+
Signature.verify(algorithm, key, signing_input, @signature)
|
43
68
|
end
|
44
69
|
|
45
70
|
def options_includes_algo_in_header?
|
46
|
-
allowed_algorithms.
|
71
|
+
allowed_algorithms.any? { |alg| alg.casecmp(algorithm).zero? }
|
47
72
|
end
|
48
73
|
|
49
74
|
def allowed_algorithms
|
50
75
|
# Order is very important - first check for string keys, next for symbols
|
51
|
-
if @options.key?('algorithm')
|
52
|
-
|
76
|
+
algos = if @options.key?('algorithm')
|
77
|
+
@options['algorithm']
|
53
78
|
elsif @options.key?(:algorithm)
|
54
|
-
|
79
|
+
@options[:algorithm]
|
55
80
|
elsif @options.key?('algorithms')
|
56
|
-
@options['algorithms']
|
81
|
+
@options['algorithms']
|
57
82
|
elsif @options.key?(:algorithms)
|
58
|
-
@options[:algorithms]
|
83
|
+
@options[:algorithms]
|
59
84
|
else
|
60
85
|
[]
|
61
86
|
end
|
87
|
+
Array(algos)
|
62
88
|
end
|
63
89
|
|
64
90
|
def find_key(&keyfinder)
|
65
91
|
key = (keyfinder.arity == 2 ? yield(header, payload) : yield(header))
|
66
|
-
|
67
|
-
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'
|
68
96
|
end
|
69
97
|
|
70
98
|
def verify_claims
|
71
99
|
Verify.verify_claims(payload, @options)
|
100
|
+
Verify.verify_required_claims(payload, @options)
|
72
101
|
end
|
73
102
|
|
74
103
|
def validate_segment_count!
|
75
104
|
return if segment_length == 3
|
76
105
|
return if !@verify && segment_length == 2 # If no verifying required, the signature is not needed
|
106
|
+
return if segment_length == 2 && none_algorithm?
|
77
107
|
|
78
108
|
raise(JWT::DecodeError, 'Not enough or too many segments')
|
79
109
|
end
|
@@ -82,8 +112,16 @@ module JWT
|
|
82
112
|
@segments.count
|
83
113
|
end
|
84
114
|
|
115
|
+
def none_algorithm?
|
116
|
+
algorithm.casecmp('none').zero?
|
117
|
+
end
|
118
|
+
|
85
119
|
def decode_crypto
|
86
|
-
@signature =
|
120
|
+
@signature = Base64.urlsafe_decode64(@segments[2] || '')
|
121
|
+
end
|
122
|
+
|
123
|
+
def algorithm
|
124
|
+
header['alg']
|
87
125
|
end
|
88
126
|
|
89
127
|
def header
|
@@ -99,8 +137,8 @@ module JWT
|
|
99
137
|
end
|
100
138
|
|
101
139
|
def parse_and_decode(segment)
|
102
|
-
JWT::JSON.parse(
|
103
|
-
rescue ::JSON::ParserError
|
140
|
+
JWT::JSON.parse(Base64.urlsafe_decode64(segment))
|
141
|
+
rescue ::JSON::ParserError, ArgumentError
|
104
142
|
raise JWT::DecodeError, 'Invalid segment encoding'
|
105
143
|
end
|
106
144
|
end
|
data/lib/jwt/default_options.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module JWT
|
2
4
|
module DefaultOptions
|
3
5
|
DEFAULT_OPTIONS = {
|
@@ -9,7 +11,8 @@ module JWT
|
|
9
11
|
verify_aud: false,
|
10
12
|
verify_sub: false,
|
11
13
|
leeway: 0,
|
12
|
-
algorithms: ['HS256']
|
14
|
+
algorithms: ['HS256'],
|
15
|
+
required_claims: []
|
13
16
|
}.freeze
|
14
17
|
end
|
15
18
|
end
|
data/lib/jwt/encode.rb
CHANGED
@@ -1,19 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative './algos'
|
3
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
|
-
ALG_NONE = 'none'
|
10
|
-
ALG_KEY = 'alg'
|
10
|
+
ALG_NONE = 'none'
|
11
|
+
ALG_KEY = 'alg'
|
11
12
|
|
12
13
|
def initialize(options)
|
13
|
-
@payload
|
14
|
-
@key
|
15
|
-
@algorithm = options[:algorithm]
|
16
|
-
@headers
|
14
|
+
@payload = options[:payload]
|
15
|
+
@key = options[:key]
|
16
|
+
_, @algorithm = Algos.find(options[:algorithm])
|
17
|
+
@headers = options[:headers].transform_keys(&:to_s)
|
17
18
|
end
|
18
19
|
|
19
20
|
def segments
|
@@ -44,7 +45,7 @@ module JWT
|
|
44
45
|
end
|
45
46
|
|
46
47
|
def encode_payload
|
47
|
-
if @payload
|
48
|
+
if @payload.is_a?(Hash)
|
48
49
|
ClaimsValidator.new(@payload).validate!
|
49
50
|
end
|
50
51
|
|
@@ -54,11 +55,11 @@ module JWT
|
|
54
55
|
def encode_signature
|
55
56
|
return '' if @algorithm == ALG_NONE
|
56
57
|
|
57
|
-
|
58
|
+
Base64.urlsafe_encode64(JWT::Signature.sign(@algorithm, encoded_header_and_payload, @key), padding: false)
|
58
59
|
end
|
59
60
|
|
60
61
|
def encode(data)
|
61
|
-
|
62
|
+
Base64.urlsafe_encode64(JWT::JSON.generate(data), padding: false)
|
62
63
|
end
|
63
64
|
|
64
65
|
def combine(*parts)
|
data/lib/jwt/error.rb
CHANGED
@@ -10,11 +10,13 @@ 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
|
16
17
|
class InvalidJtiError < DecodeError; end
|
17
18
|
class InvalidPayload < DecodeError; end
|
19
|
+
class MissingRequiredClaim < DecodeError; end
|
18
20
|
|
19
21
|
class JWKError < DecodeError; end
|
20
22
|
end
|
data/lib/jwt/jwk/ec.rb
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module JWT
|
6
|
+
module JWK
|
7
|
+
class EC < KeyBase
|
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
|
+
def initialize(keypair, kid = nil)
|
16
|
+
raise ArgumentError, 'keypair must be of type OpenSSL::PKey::EC' unless keypair.is_a?(OpenSSL::PKey::EC)
|
17
|
+
|
18
|
+
kid ||= generate_kid(keypair)
|
19
|
+
super(keypair, kid)
|
20
|
+
end
|
21
|
+
|
22
|
+
def private?
|
23
|
+
@keypair.private_key?
|
24
|
+
end
|
25
|
+
|
26
|
+
def export(options = {})
|
27
|
+
crv, x_octets, y_octets = keypair_components(keypair)
|
28
|
+
exported_hash = {
|
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
|
36
|
+
|
37
|
+
append_private_parts(exported_hash)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def append_private_parts(the_hash)
|
43
|
+
octets = keypair.private_key.to_bn.to_s(BINARY)
|
44
|
+
the_hash.merge(
|
45
|
+
d: encode_octets(octets)
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
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
|
+
def keypair_components(ec_keypair)
|
57
|
+
encoded_point = ec_keypair.public_key.to_bn.to_s(BINARY)
|
58
|
+
case ec_keypair.group.curve_name
|
59
|
+
when 'prime256v1'
|
60
|
+
crv = 'P-256'
|
61
|
+
x_octets, y_octets = encoded_point.unpack('xa32a32')
|
62
|
+
when 'secp384r1'
|
63
|
+
crv = 'P-384'
|
64
|
+
x_octets, y_octets = encoded_point.unpack('xa48a48')
|
65
|
+
when 'secp521r1'
|
66
|
+
crv = 'P-521'
|
67
|
+
x_octets, y_octets = encoded_point.unpack('xa66a66')
|
68
|
+
else
|
69
|
+
raise JWT::JWKError, "Unsupported curve '#{ec_keypair.group.curve_name}'"
|
70
|
+
end
|
71
|
+
[crv, x_octets, y_octets]
|
72
|
+
end
|
73
|
+
|
74
|
+
def encode_octets(octets)
|
75
|
+
Base64.urlsafe_encode64(octets, padding: false)
|
76
|
+
end
|
77
|
+
|
78
|
+
def encode_open_ssl_bn(key_part)
|
79
|
+
Base64.urlsafe_encode64(key_part.to_s(BINARY), padding: false)
|
80
|
+
end
|
81
|
+
|
82
|
+
class << self
|
83
|
+
def import(jwk_data)
|
84
|
+
# See https://tools.ietf.org/html/rfc7518#section-6.2.1 for an
|
85
|
+
# explanation of the relevant parameters.
|
86
|
+
|
87
|
+
jwk_crv, jwk_x, jwk_y, jwk_d, jwk_kid = jwk_attrs(jwk_data, %i[crv x y d kid])
|
88
|
+
raise JWT::JWKError, 'Key format is invalid for EC' unless jwk_crv && jwk_x && jwk_y
|
89
|
+
|
90
|
+
new(ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d), jwk_kid)
|
91
|
+
end
|
92
|
+
|
93
|
+
def to_openssl_curve(crv)
|
94
|
+
# The JWK specs and OpenSSL use different names for the same curves.
|
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
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def jwk_attrs(jwk_data, attrs)
|
108
|
+
attrs.map do |attr|
|
109
|
+
jwk_data[attr] || jwk_data[attr.to_s]
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d)
|
114
|
+
curve = to_openssl_curve(jwk_crv)
|
115
|
+
|
116
|
+
x_octets = decode_octets(jwk_x)
|
117
|
+
y_octets = decode_octets(jwk_y)
|
118
|
+
|
119
|
+
key = OpenSSL::PKey::EC.new(curve)
|
120
|
+
|
121
|
+
# The details of the `Point` instantiation are covered in:
|
122
|
+
# - https://docs.ruby-lang.org/en/2.4.0/OpenSSL/PKey/EC.html
|
123
|
+
# - https://www.openssl.org/docs/manmaster/man3/EC_POINT_new.html
|
124
|
+
# - https://tools.ietf.org/html/rfc5480#section-2.2
|
125
|
+
# - https://www.secg.org/SEC1-Ver-1.0.pdf
|
126
|
+
# Section 2.3.3 of the last of these references specifies that the
|
127
|
+
# encoding of an uncompressed point consists of the byte `0x04` followed
|
128
|
+
# by the x value then the y value.
|
129
|
+
point = OpenSSL::PKey::EC::Point.new(
|
130
|
+
OpenSSL::PKey::EC::Group.new(curve),
|
131
|
+
OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2)
|
132
|
+
)
|
133
|
+
|
134
|
+
key.public_key = point
|
135
|
+
key.private_key = OpenSSL::BN.new(decode_octets(jwk_d), 2) if jwk_d
|
136
|
+
|
137
|
+
key
|
138
|
+
end
|
139
|
+
|
140
|
+
def decode_octets(jwk_data)
|
141
|
+
Base64.urlsafe_decode64(jwk_data)
|
142
|
+
end
|
143
|
+
|
144
|
+
def decode_open_ssl_bn(jwk_data)
|
145
|
+
OpenSSL::BN.new(Base64.urlsafe_decode64(jwk_data), BINARY)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|