jwt 2.4.1 → 2.9.3
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/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
|