jwt 2.4.1 → 2.9.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +177 -14
- data/CONTRIBUTING.md +7 -7
- data/README.md +180 -37
- data/lib/jwt/base64.rb +33 -0
- data/lib/jwt/claims/audience.rb +20 -0
- data/lib/jwt/claims/decode_verifier.rb +40 -0
- data/lib/jwt/claims/expiration.rb +22 -0
- data/lib/jwt/claims/issued_at.rb +15 -0
- data/lib/jwt/claims/issuer.rb +24 -0
- data/lib/jwt/claims/jwt_id.rb +25 -0
- data/lib/jwt/claims/not_before.rb +22 -0
- data/lib/jwt/claims/numeric.rb +55 -0
- data/lib/jwt/claims/required.rb +23 -0
- data/lib/jwt/claims/subject.rb +20 -0
- data/lib/jwt/claims/verifier.rb +62 -0
- data/lib/jwt/claims.rb +82 -0
- data/lib/jwt/claims_validator.rb +3 -24
- data/lib/jwt/configuration/container.rb +32 -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 +54 -41
- data/lib/jwt/deprecations.rb +48 -0
- data/lib/jwt/encode.rb +21 -21
- data/lib/jwt/error.rb +1 -0
- data/lib/jwt/jwa/compat.rb +29 -0
- data/lib/jwt/jwa/ecdsa.rb +93 -0
- data/lib/jwt/jwa/eddsa.rb +34 -0
- data/lib/jwt/jwa/hmac.rb +83 -0
- data/lib/jwt/jwa/hmac_rbnacl.rb +49 -0
- data/lib/jwt/jwa/hmac_rbnacl_fixed.rb +46 -0
- data/lib/jwt/jwa/none.rb +23 -0
- data/lib/jwt/jwa/ps.rb +36 -0
- data/lib/jwt/jwa/rsa.rb +36 -0
- data/lib/jwt/jwa/signing_algorithm.rb +60 -0
- data/lib/jwt/jwa/unsupported.rb +19 -0
- data/lib/jwt/jwa/wrapper.rb +43 -0
- data/lib/jwt/jwa.rb +50 -0
- data/lib/jwt/jwk/ec.rb +162 -65
- data/lib/jwt/jwk/hmac.rb +69 -24
- data/lib/jwt/jwk/key_base.rb +45 -7
- data/lib/jwt/jwk/key_finder.rb +19 -35
- 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 +141 -54
- data/lib/jwt/jwk/set.rb +80 -0
- data/lib/jwt/jwk/thumbprint.rb +26 -0
- data/lib/jwt/jwk.rb +14 -11
- data/lib/jwt/verify.rb +10 -89
- data/lib/jwt/version.rb +24 -2
- data/lib/jwt/x5c_key_finder.rb +3 -6
- data/lib/jwt.rb +12 -4
- data/ruby-jwt.gemspec +11 -4
- metadata +59 -31
- data/.codeclimate.yml +0 -8
- data/.github/workflows/coverage.yml +0 -27
- data/.github/workflows/test.yml +0 -66
- data/.gitignore +0 -13
- data/.reek.yml +0 -22
- data/.rspec +0 -2
- data/.rubocop.yml +0 -67
- data/.sourcelevel.yml +0 -17
- data/Appraisals +0 -13
- data/Gemfile +0 -7
- data/Rakefile +0 -16
- data/lib/jwt/algos/ecdsa.rb +0 -64
- data/lib/jwt/algos/eddsa.rb +0 -33
- data/lib/jwt/algos/hmac.rb +0 -36
- data/lib/jwt/algos/none.rb +0 -17
- data/lib/jwt/algos/ps.rb +0 -43
- data/lib/jwt/algos/rsa.rb +0 -22
- data/lib/jwt/algos/unsupported.rb +0 -19
- data/lib/jwt/algos.rb +0 -44
- data/lib/jwt/default_options.rb +0 -18
- data/lib/jwt/security_utils.rb +0 -59
- data/lib/jwt/signature.rb +0 -35
data/lib/jwt/jwa/ps.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module JWA
|
5
|
+
class Ps
|
6
|
+
include JWT::JWA::SigningAlgorithm
|
7
|
+
|
8
|
+
def initialize(alg)
|
9
|
+
@alg = alg
|
10
|
+
@digest_algorithm = alg.sub('PS', 'sha')
|
11
|
+
end
|
12
|
+
|
13
|
+
def sign(data:, signing_key:)
|
14
|
+
unless signing_key.is_a?(::OpenSSL::PKey::RSA)
|
15
|
+
raise_sign_error!("The given key is a #{signing_key.class}. It has to be an OpenSSL::PKey::RSA instance.")
|
16
|
+
end
|
17
|
+
|
18
|
+
signing_key.sign_pss(digest_algorithm, data, salt_length: :digest, mgf1_hash: digest_algorithm)
|
19
|
+
end
|
20
|
+
|
21
|
+
def verify(data:, signature:, verification_key:)
|
22
|
+
verification_key.verify_pss(digest_algorithm, signature, data, salt_length: :auto, mgf1_hash: digest_algorithm)
|
23
|
+
rescue OpenSSL::PKey::PKeyError
|
24
|
+
raise JWT::VerificationError, 'Signature verification raised'
|
25
|
+
end
|
26
|
+
|
27
|
+
register_algorithm(new('PS256'))
|
28
|
+
register_algorithm(new('PS384'))
|
29
|
+
register_algorithm(new('PS512'))
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_reader :digest_algorithm
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/jwt/jwa/rsa.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module JWA
|
5
|
+
class Rsa
|
6
|
+
include JWT::JWA::SigningAlgorithm
|
7
|
+
|
8
|
+
def initialize(alg)
|
9
|
+
@alg = alg
|
10
|
+
@digest = OpenSSL::Digest.new(alg.sub('RS', 'SHA'))
|
11
|
+
end
|
12
|
+
|
13
|
+
def sign(data:, signing_key:)
|
14
|
+
unless signing_key.is_a?(OpenSSL::PKey::RSA)
|
15
|
+
raise_sign_error!("The given key is a #{signing_key.class}. It has to be an OpenSSL::PKey::RSA instance")
|
16
|
+
end
|
17
|
+
|
18
|
+
signing_key.sign(digest, data)
|
19
|
+
end
|
20
|
+
|
21
|
+
def verify(data:, signature:, verification_key:)
|
22
|
+
verification_key.verify(digest, signature, data)
|
23
|
+
rescue OpenSSL::PKey::PKeyError
|
24
|
+
raise JWT::VerificationError, 'Signature verification raised'
|
25
|
+
end
|
26
|
+
|
27
|
+
register_algorithm(new('RS256'))
|
28
|
+
register_algorithm(new('RS384'))
|
29
|
+
register_algorithm(new('RS512'))
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_reader :digest
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module JWA
|
5
|
+
module SigningAlgorithm
|
6
|
+
module ClassMethods
|
7
|
+
def register_algorithm(algo)
|
8
|
+
::JWT::JWA.register_algorithm(algo)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.included(klass)
|
13
|
+
klass.extend(ClassMethods)
|
14
|
+
klass.include(JWT::JWA::Compat)
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :alg
|
18
|
+
|
19
|
+
def valid_alg?(alg_to_check)
|
20
|
+
alg&.casecmp(alg_to_check)&.zero? == true
|
21
|
+
end
|
22
|
+
|
23
|
+
def header(*)
|
24
|
+
{ 'alg' => alg }
|
25
|
+
end
|
26
|
+
|
27
|
+
def sign(*)
|
28
|
+
raise_sign_error!('Algorithm implementation is missing the sign method')
|
29
|
+
end
|
30
|
+
|
31
|
+
def verify(*)
|
32
|
+
raise_verify_error!('Algorithm implementation is missing the verify method')
|
33
|
+
end
|
34
|
+
|
35
|
+
def raise_verify_error!(message)
|
36
|
+
raise(DecodeError.new(message).tap { |e| e.set_backtrace(caller(1)) })
|
37
|
+
end
|
38
|
+
|
39
|
+
def raise_sign_error!(message)
|
40
|
+
raise(EncodeError.new(message).tap { |e| e.set_backtrace(caller(1)) })
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class << self
|
45
|
+
def register_algorithm(algo)
|
46
|
+
algorithms[algo.alg.to_s.downcase] = algo
|
47
|
+
end
|
48
|
+
|
49
|
+
def find(algo)
|
50
|
+
algorithms.fetch(algo.to_s.downcase, Unsupported)
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def algorithms
|
56
|
+
@algorithms ||= {}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module JWA
|
5
|
+
module Unsupported
|
6
|
+
class << self
|
7
|
+
include JWT::JWA::SigningAlgorithm
|
8
|
+
|
9
|
+
def sign(*)
|
10
|
+
raise_sign_error!('Unsupported signing method')
|
11
|
+
end
|
12
|
+
|
13
|
+
def verify(*)
|
14
|
+
raise JWT::VerificationError, 'Algorithm not supported'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module JWA
|
5
|
+
class Wrapper
|
6
|
+
include SigningAlgorithm
|
7
|
+
|
8
|
+
def initialize(algorithm)
|
9
|
+
@algorithm = algorithm
|
10
|
+
end
|
11
|
+
|
12
|
+
def alg
|
13
|
+
return @algorithm.alg if @algorithm.respond_to?(:alg)
|
14
|
+
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
def valid_alg?(alg_to_check)
|
19
|
+
return @algorithm.valid_alg?(alg_to_check) if @algorithm.respond_to?(:valid_alg?)
|
20
|
+
|
21
|
+
super
|
22
|
+
end
|
23
|
+
|
24
|
+
def header(*args, **kwargs)
|
25
|
+
return @algorithm.header(*args, **kwargs) if @algorithm.respond_to?(:header)
|
26
|
+
|
27
|
+
super
|
28
|
+
end
|
29
|
+
|
30
|
+
def sign(*args, **kwargs)
|
31
|
+
return @algorithm.sign(*args, **kwargs) if @algorithm.respond_to?(:sign)
|
32
|
+
|
33
|
+
super
|
34
|
+
end
|
35
|
+
|
36
|
+
def verify(*args, **kwargs)
|
37
|
+
return @algorithm.verify(*args, **kwargs) if @algorithm.respond_to?(:verify)
|
38
|
+
|
39
|
+
super
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/jwt/jwa.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'rbnacl'
|
7
|
+
rescue LoadError
|
8
|
+
raise if defined?(RbNaCl)
|
9
|
+
end
|
10
|
+
|
11
|
+
require_relative 'jwa/compat'
|
12
|
+
require_relative 'jwa/signing_algorithm'
|
13
|
+
require_relative 'jwa/ecdsa'
|
14
|
+
require_relative 'jwa/hmac'
|
15
|
+
require_relative 'jwa/none'
|
16
|
+
require_relative 'jwa/ps'
|
17
|
+
require_relative 'jwa/rsa'
|
18
|
+
require_relative 'jwa/unsupported'
|
19
|
+
require_relative 'jwa/wrapper'
|
20
|
+
|
21
|
+
if JWT.rbnacl?
|
22
|
+
require_relative 'jwa/eddsa'
|
23
|
+
end
|
24
|
+
|
25
|
+
if JWT.rbnacl_6_or_greater?
|
26
|
+
require_relative 'jwa/hmac_rbnacl'
|
27
|
+
elsif JWT.rbnacl?
|
28
|
+
require_relative 'jwa/hmac_rbnacl_fixed'
|
29
|
+
end
|
30
|
+
|
31
|
+
module JWT
|
32
|
+
module JWA
|
33
|
+
class << self
|
34
|
+
def resolve(algorithm)
|
35
|
+
return find(algorithm) if algorithm.is_a?(String) || algorithm.is_a?(Symbol)
|
36
|
+
|
37
|
+
unless algorithm.is_a?(SigningAlgorithm)
|
38
|
+
Deprecations.warning('Custom algorithms are required to include JWT::JWA::SigningAlgorithm. Custom algorithms that do not include this module may stop working in the next major version of ruby-jwt.')
|
39
|
+
return Wrapper.new(algorithm)
|
40
|
+
end
|
41
|
+
|
42
|
+
algorithm
|
43
|
+
end
|
44
|
+
|
45
|
+
def create(algorithm)
|
46
|
+
resolve(algorithm)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/jwt/jwk/ec.rb
CHANGED
@@ -4,55 +4,100 @@ require 'forwardable'
|
|
4
4
|
|
5
5
|
module JWT
|
6
6
|
module JWK
|
7
|
-
class EC < KeyBase
|
8
|
-
extend Forwardable
|
9
|
-
def_delegators :@keypair, :public_key
|
10
|
-
|
7
|
+
class EC < KeyBase # rubocop:disable Metrics/ClassLength
|
11
8
|
KTY = 'EC'
|
12
|
-
KTYS = [KTY, OpenSSL::PKey::EC].freeze
|
9
|
+
KTYS = [KTY, OpenSSL::PKey::EC, JWT::JWK::EC].freeze
|
13
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
|
+
ZERO_BYTE = "\0".b.freeze
|
15
|
+
|
16
|
+
def initialize(key, params = nil, options = {})
|
17
|
+
params ||= {}
|
18
|
+
|
19
|
+
# For backwards compatibility when kid was a String
|
20
|
+
params = { kid: params } if params.is_a?(String)
|
14
21
|
|
15
|
-
|
16
|
-
|
22
|
+
key_params = extract_key_params(key)
|
23
|
+
|
24
|
+
params = params.transform_keys(&:to_sym)
|
25
|
+
check_jwk_params!(key_params, params)
|
26
|
+
|
27
|
+
super(options, key_params.merge(params))
|
28
|
+
end
|
17
29
|
|
18
|
-
|
19
|
-
|
30
|
+
def keypair
|
31
|
+
ec_key
|
20
32
|
end
|
21
33
|
|
22
34
|
def private?
|
23
|
-
|
35
|
+
ec_key.private_key?
|
24
36
|
end
|
25
37
|
|
26
|
-
def
|
27
|
-
|
28
|
-
|
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
|
38
|
+
def signing_key
|
39
|
+
ec_key
|
40
|
+
end
|
36
41
|
|
37
|
-
|
42
|
+
def verify_key
|
43
|
+
ec_key
|
38
44
|
end
|
39
45
|
|
40
|
-
|
46
|
+
def public_key
|
47
|
+
ec_key
|
48
|
+
end
|
41
49
|
|
42
|
-
def
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
50
|
+
def members
|
51
|
+
EC_PUBLIC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] }
|
52
|
+
end
|
53
|
+
|
54
|
+
def export(options = {})
|
55
|
+
exported = parameters.clone
|
56
|
+
exported.reject! { |k, _| EC_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true
|
57
|
+
exported
|
47
58
|
end
|
48
59
|
|
49
|
-
def
|
50
|
-
_crv, x_octets, y_octets = keypair_components(
|
60
|
+
def key_digest
|
61
|
+
_crv, x_octets, y_octets = keypair_components(ec_key)
|
51
62
|
sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(x_octets, BINARY)),
|
52
63
|
OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(y_octets, BINARY))])
|
53
64
|
OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
|
54
65
|
end
|
55
66
|
|
67
|
+
def []=(key, value)
|
68
|
+
if EC_KEY_ELEMENTS.include?(key.to_sym)
|
69
|
+
raise ArgumentError, 'cannot overwrite cryptographic key attributes'
|
70
|
+
end
|
71
|
+
|
72
|
+
super(key, value)
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def ec_key
|
78
|
+
@ec_key ||= create_ec_key(self[:crv], self[:x], self[:y], self[:d])
|
79
|
+
end
|
80
|
+
|
81
|
+
def extract_key_params(key)
|
82
|
+
case key
|
83
|
+
when JWT::JWK::EC
|
84
|
+
key.export(include_private: true)
|
85
|
+
when OpenSSL::PKey::EC # Accept OpenSSL key as input
|
86
|
+
@ec_key = key # Preserve the object to avoid recreation
|
87
|
+
parse_ec_key(key)
|
88
|
+
when Hash
|
89
|
+
key.transform_keys(&:to_sym)
|
90
|
+
else
|
91
|
+
raise ArgumentError, 'key must be of type OpenSSL::PKey::EC or Hash with key parameters'
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def check_jwk_params!(key_params, params)
|
96
|
+
raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (EC_KEY_ELEMENTS & params.keys).empty?
|
97
|
+
raise JWT::JWKError, "Incorrect 'kty' value: #{key_params[:kty]}, expected #{KTY}" unless key_params[:kty] == KTY
|
98
|
+
raise JWT::JWKError, 'Key format is invalid for EC' unless key_params[:crv] && key_params[:x] && key_params[:y]
|
99
|
+
end
|
100
|
+
|
56
101
|
def keypair_components(ec_keypair)
|
57
102
|
encoded_point = ec_keypair.public_key.to_bn.to_s(BINARY)
|
58
103
|
case ec_keypair.group.curve_name
|
@@ -75,47 +120,65 @@ module JWT
|
|
75
120
|
end
|
76
121
|
|
77
122
|
def encode_octets(octets)
|
78
|
-
|
123
|
+
return unless octets
|
124
|
+
|
125
|
+
::JWT::Base64.url_encode(octets)
|
79
126
|
end
|
80
127
|
|
81
128
|
def encode_open_ssl_bn(key_part)
|
82
|
-
Base64.
|
129
|
+
::JWT::Base64.url_encode(key_part.to_s(BINARY))
|
83
130
|
end
|
84
131
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
132
|
+
def parse_ec_key(key)
|
133
|
+
crv, x_octets, y_octets = keypair_components(key)
|
134
|
+
octets = key.private_key&.to_bn&.to_s(BINARY)
|
135
|
+
{
|
136
|
+
kty: KTY,
|
137
|
+
crv: crv,
|
138
|
+
x: encode_octets(x_octets),
|
139
|
+
y: encode_octets(y_octets),
|
140
|
+
d: encode_octets(octets)
|
141
|
+
}.compact
|
142
|
+
end
|
95
143
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
when 'P-256' then 'prime256v1'
|
102
|
-
when 'P-384' then 'secp384r1'
|
103
|
-
when 'P-521' then 'secp521r1'
|
104
|
-
when 'P-256K' then 'secp256k1'
|
105
|
-
else raise JWT::JWKError, 'Invalid curve provided'
|
106
|
-
end
|
107
|
-
end
|
144
|
+
if ::JWT.openssl_3?
|
145
|
+
def create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d) # rubocop:disable Metrics/MethodLength
|
146
|
+
curve = EC.to_openssl_curve(jwk_crv)
|
147
|
+
x_octets = decode_octets(jwk_x)
|
148
|
+
y_octets = decode_octets(jwk_y)
|
108
149
|
|
109
|
-
|
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
|
+
)
|
110
154
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
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)
|
115
178
|
end
|
116
|
-
|
117
|
-
def
|
118
|
-
curve = to_openssl_curve(jwk_crv)
|
179
|
+
else
|
180
|
+
def create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d)
|
181
|
+
curve = EC.to_openssl_curve(jwk_crv)
|
119
182
|
|
120
183
|
x_octets = decode_octets(jwk_x)
|
121
184
|
y_octets = decode_octets(jwk_y)
|
@@ -140,13 +203,47 @@ module JWT
|
|
140
203
|
|
141
204
|
key
|
142
205
|
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def decode_octets(base64_encoded_coordinate)
|
209
|
+
bytes = ::JWT::Base64.url_decode(base64_encoded_coordinate)
|
210
|
+
# Some base64 encoders on some platform omit a single 0-byte at
|
211
|
+
# the start of either Y or X coordinate of the elliptic curve point.
|
212
|
+
# This leads to an encoding error when data is passed to OpenSSL BN.
|
213
|
+
# It is know to have happend to exported JWKs on a Java application and
|
214
|
+
# on a Flutter/Dart application (both iOS and Android). All that is
|
215
|
+
# needed to fix the problem is adding a leading 0-byte. We know the
|
216
|
+
# required byte is 0 because with any other byte the point is no longer
|
217
|
+
# on the curve - and OpenSSL will actually communicate this via another
|
218
|
+
# exception. The indication of a stripped byte will be the fact that the
|
219
|
+
# coordinates - once decoded into bytes - should always be an even
|
220
|
+
# bytesize. For example, with a P-521 curve, both x and y must be 66 bytes.
|
221
|
+
# With a P-256 curve, both x and y must be 32 and so on. The simplest way
|
222
|
+
# to check for this truncation is thus to check whether the number of bytes
|
223
|
+
# is odd, and restore the leading 0-byte if it is.
|
224
|
+
if bytes.bytesize.odd?
|
225
|
+
ZERO_BYTE + bytes
|
226
|
+
else
|
227
|
+
bytes
|
228
|
+
end
|
229
|
+
end
|
143
230
|
|
144
|
-
|
145
|
-
|
231
|
+
class << self
|
232
|
+
def import(jwk_data)
|
233
|
+
new(jwk_data)
|
146
234
|
end
|
147
235
|
|
148
|
-
def
|
149
|
-
OpenSSL
|
236
|
+
def to_openssl_curve(crv)
|
237
|
+
# The JWK specs and OpenSSL use different names for the same curves.
|
238
|
+
# See https://tools.ietf.org/html/rfc5480#section-2.1.1.1 for some
|
239
|
+
# pointers on different names for common curves.
|
240
|
+
case crv
|
241
|
+
when 'P-256' then 'prime256v1'
|
242
|
+
when 'P-384' then 'secp384r1'
|
243
|
+
when 'P-521' then 'secp521r1'
|
244
|
+
when 'P-256K' then 'secp256k1'
|
245
|
+
else raise JWT::JWKError, 'Invalid curve provided'
|
246
|
+
end
|
150
247
|
end
|
151
248
|
end
|
152
249
|
end
|
data/lib/jwt/jwk/hmac.rb
CHANGED
@@ -3,14 +3,28 @@
|
|
3
3
|
module JWT
|
4
4
|
module JWK
|
5
5
|
class HMAC < KeyBase
|
6
|
-
KTY
|
7
|
-
KTYS = [KTY, String].freeze
|
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
|
8
11
|
|
9
|
-
def initialize(
|
10
|
-
|
12
|
+
def initialize(key, params = nil, options = {})
|
13
|
+
params ||= {}
|
11
14
|
|
12
|
-
|
13
|
-
|
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
|
14
28
|
end
|
15
29
|
|
16
30
|
def private?
|
@@ -21,36 +35,67 @@ module JWT
|
|
21
35
|
nil
|
22
36
|
end
|
23
37
|
|
38
|
+
def verify_key
|
39
|
+
secret
|
40
|
+
end
|
41
|
+
|
42
|
+
def signing_key
|
43
|
+
secret
|
44
|
+
end
|
45
|
+
|
24
46
|
# See https://tools.ietf.org/html/rfc7517#appendix-A.3
|
25
47
|
def export(options = {})
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
30
62
|
|
31
|
-
|
63
|
+
def []=(key, value)
|
64
|
+
if HMAC_KEY_ELEMENTS.include?(key.to_sym)
|
65
|
+
raise ArgumentError, 'cannot overwrite cryptographic key attributes'
|
66
|
+
end
|
32
67
|
|
33
|
-
|
34
|
-
k: keypair
|
35
|
-
)
|
68
|
+
super(key, value)
|
36
69
|
end
|
37
70
|
|
38
71
|
private
|
39
72
|
|
40
|
-
def
|
41
|
-
|
42
|
-
OpenSSL::ASN1::UTF8String.new(KTY)])
|
43
|
-
OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
|
73
|
+
def secret
|
74
|
+
self[:k]
|
44
75
|
end
|
45
76
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
50
89
|
|
51
|
-
|
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
|
52
95
|
|
53
|
-
|
96
|
+
class << self
|
97
|
+
def import(jwk_data)
|
98
|
+
new(jwk_data)
|
54
99
|
end
|
55
100
|
end
|
56
101
|
end
|