jwt 2.2.2 → 2.4.0.beta1
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/.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
|