jwt 2.8.2 → 3.1.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/CHANGELOG.md +149 -31
- data/CODE_OF_CONDUCT.md +14 -14
- data/CONTRIBUTING.md +9 -10
- data/README.md +299 -234
- data/UPGRADING.md +47 -0
- data/lib/jwt/base64.rb +4 -10
- data/lib/jwt/claims/audience.rb +30 -0
- data/lib/jwt/claims/crit.rb +35 -0
- data/lib/jwt/claims/decode_verifier.rb +40 -0
- data/lib/jwt/claims/expiration.rb +32 -0
- data/lib/jwt/claims/issued_at.rb +22 -0
- data/lib/jwt/claims/issuer.rb +34 -0
- data/lib/jwt/claims/jwt_id.rb +35 -0
- data/lib/jwt/claims/not_before.rb +32 -0
- data/lib/jwt/claims/numeric.rb +45 -0
- data/lib/jwt/claims/required.rb +33 -0
- data/lib/jwt/claims/subject.rb +30 -0
- data/lib/jwt/claims/verifier.rb +61 -0
- data/lib/jwt/claims.rb +67 -0
- data/lib/jwt/configuration/container.rb +20 -1
- data/lib/jwt/configuration/decode_configuration.rb +24 -0
- data/lib/jwt/configuration/jwk_configuration.rb +1 -0
- data/lib/jwt/configuration.rb +8 -0
- data/lib/jwt/decode.rb +42 -81
- data/lib/jwt/encode.rb +17 -60
- data/lib/jwt/encoded_token.rb +236 -0
- data/lib/jwt/error.rb +32 -1
- data/lib/jwt/json.rb +1 -1
- data/lib/jwt/jwa/ecdsa.rb +59 -24
- data/lib/jwt/jwa/hmac.rb +22 -19
- data/lib/jwt/jwa/none.rb +8 -3
- data/lib/jwt/jwa/ps.rb +21 -15
- data/lib/jwt/jwa/rsa.rb +21 -10
- data/lib/jwt/jwa/signing_algorithm.rb +62 -0
- data/lib/jwt/jwa/unsupported.rb +9 -8
- data/lib/jwt/jwa.rb +76 -35
- data/lib/jwt/jwk/ec.rb +54 -65
- data/lib/jwt/jwk/hmac.rb +5 -6
- data/lib/jwt/jwk/key_base.rb +16 -1
- data/lib/jwt/jwk/key_finder.rb +35 -8
- data/lib/jwt/jwk/kid_as_key_digest.rb +1 -0
- data/lib/jwt/jwk/rsa.rb +7 -4
- data/lib/jwt/jwk/set.rb +2 -0
- data/lib/jwt/jwk.rb +1 -1
- data/lib/jwt/token.rb +131 -0
- data/lib/jwt/version.rb +24 -19
- data/lib/jwt.rb +18 -4
- data/ruby-jwt.gemspec +2 -0
- metadata +49 -15
- data/lib/jwt/claims_validator.rb +0 -37
- data/lib/jwt/deprecations.rb +0 -48
- data/lib/jwt/jwa/eddsa.rb +0 -42
- data/lib/jwt/jwa/hmac_rbnacl.rb +0 -50
- data/lib/jwt/jwa/hmac_rbnacl_fixed.rb +0 -46
- data/lib/jwt/jwa/wrapper.rb +0 -26
- data/lib/jwt/jwk/okp_rbnacl.rb +0 -110
- data/lib/jwt/verify.rb +0 -117
data/lib/jwt/jwa/ecdsa.rb
CHANGED
@@ -2,8 +2,40 @@
|
|
2
2
|
|
3
3
|
module JWT
|
4
4
|
module JWA
|
5
|
-
|
6
|
-
|
5
|
+
# ECDSA signing algorithm
|
6
|
+
class Ecdsa
|
7
|
+
include JWT::JWA::SigningAlgorithm
|
8
|
+
|
9
|
+
def initialize(alg, digest)
|
10
|
+
@alg = alg
|
11
|
+
@digest = OpenSSL::Digest.new(digest)
|
12
|
+
end
|
13
|
+
|
14
|
+
def sign(data:, signing_key:)
|
15
|
+
raise_sign_error!("The given key is a #{signing_key.class}. It has to be an OpenSSL::PKey::EC instance") unless signing_key.is_a?(::OpenSSL::PKey::EC)
|
16
|
+
raise_sign_error!('The given key is not a private key') unless signing_key.private?
|
17
|
+
|
18
|
+
curve_definition = curve_by_name(signing_key.group.curve_name)
|
19
|
+
key_algorithm = curve_definition[:algorithm]
|
20
|
+
|
21
|
+
raise IncorrectAlgorithm, "payload algorithm is #{alg} but #{key_algorithm} signing key was provided" if alg != key_algorithm
|
22
|
+
|
23
|
+
asn1_to_raw(signing_key.dsa_sign_asn1(digest.digest(data)), signing_key)
|
24
|
+
end
|
25
|
+
|
26
|
+
def verify(data:, signature:, verification_key:)
|
27
|
+
verification_key = self.class.create_public_key_from_point(verification_key) if verification_key.is_a?(::OpenSSL::PKey::EC::Point)
|
28
|
+
|
29
|
+
raise_verify_error!("The given key is a #{verification_key.class}. It has to be an OpenSSL::PKey::EC instance") unless verification_key.is_a?(::OpenSSL::PKey::EC)
|
30
|
+
|
31
|
+
curve_definition = curve_by_name(verification_key.group.curve_name)
|
32
|
+
key_algorithm = curve_definition[:algorithm]
|
33
|
+
raise IncorrectAlgorithm, "payload algorithm is #{alg} but #{key_algorithm} verification key was provided" if alg != key_algorithm
|
34
|
+
|
35
|
+
verification_key.dsa_verify_asn1(digest.digest(data), raw_to_asn1(signature, verification_key))
|
36
|
+
rescue OpenSSL::PKey::PKeyError
|
37
|
+
raise JWT::VerificationError, 'Signature verification raised'
|
38
|
+
end
|
7
39
|
|
8
40
|
NAMED_CURVES = {
|
9
41
|
'prime256v1' => {
|
@@ -28,36 +60,39 @@ module JWT
|
|
28
60
|
}
|
29
61
|
}.freeze
|
30
62
|
|
31
|
-
|
63
|
+
NAMED_CURVES.each_value do |v|
|
64
|
+
register_algorithm(new(v[:algorithm], v[:digest]))
|
65
|
+
end
|
32
66
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} signing key was provided"
|
67
|
+
# @api private
|
68
|
+
def self.curve_by_name(name)
|
69
|
+
NAMED_CURVES.fetch(name) do
|
70
|
+
raise UnsupportedEcdsaCurve, "The ECDSA curve '#{name}' is not supported"
|
38
71
|
end
|
39
|
-
|
40
|
-
digest = OpenSSL::Digest.new(curve_definition[:digest])
|
41
|
-
asn1_to_raw(key.dsa_sign_asn1(digest.digest(msg)), key)
|
42
72
|
end
|
43
73
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
74
|
+
if ::JWT.openssl_3?
|
75
|
+
def self.create_public_key_from_point(point)
|
76
|
+
sequence = OpenSSL::ASN1::Sequence([
|
77
|
+
OpenSSL::ASN1::Sequence([OpenSSL::ASN1::ObjectId('id-ecPublicKey'), OpenSSL::ASN1::ObjectId(point.group.curve_name)]),
|
78
|
+
OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed))
|
79
|
+
])
|
80
|
+
OpenSSL::PKey::EC.new(sequence.to_der)
|
81
|
+
end
|
82
|
+
else
|
83
|
+
def self.create_public_key_from_point(point)
|
84
|
+
OpenSSL::PKey::EC.new(point.group.curve_name).tap do |key|
|
85
|
+
key.public_key = point
|
86
|
+
end
|
49
87
|
end
|
50
|
-
|
51
|
-
digest = OpenSSL::Digest.new(curve_definition[:digest])
|
52
|
-
public_key.dsa_verify_asn1(digest.digest(signing_input), raw_to_asn1(signature, public_key))
|
53
|
-
rescue OpenSSL::PKey::PKeyError
|
54
|
-
raise JWT::VerificationError, 'Signature verification raised'
|
55
88
|
end
|
56
89
|
|
90
|
+
private
|
91
|
+
|
92
|
+
attr_reader :digest
|
93
|
+
|
57
94
|
def curve_by_name(name)
|
58
|
-
|
59
|
-
raise UnsupportedEcdsaCurve, "The ECDSA curve '#{name}' is not supported"
|
60
|
-
end
|
95
|
+
self.class.curve_by_name(name)
|
61
96
|
end
|
62
97
|
|
63
98
|
def raw_to_asn1(signature, private_key)
|
data/lib/jwt/jwa/hmac.rb
CHANGED
@@ -2,35 +2,38 @@
|
|
2
2
|
|
3
3
|
module JWT
|
4
4
|
module JWA
|
5
|
-
|
6
|
-
|
5
|
+
# Implementation of the HMAC family of algorithms
|
6
|
+
class Hmac
|
7
|
+
include JWT::JWA::SigningAlgorithm
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
}.freeze
|
13
|
-
|
14
|
-
SUPPORTED = MAPPING.keys
|
15
|
-
|
16
|
-
def sign(algorithm, msg, key)
|
17
|
-
key ||= ''
|
9
|
+
def initialize(alg, digest)
|
10
|
+
@alg = alg
|
11
|
+
@digest = digest
|
12
|
+
end
|
18
13
|
|
19
|
-
|
14
|
+
def sign(data:, signing_key:)
|
15
|
+
signing_key ||= ''
|
16
|
+
raise_verify_error!('HMAC key expected to be a String') unless signing_key.is_a?(String)
|
20
17
|
|
21
|
-
OpenSSL::HMAC.digest(
|
18
|
+
OpenSSL::HMAC.digest(digest.new, signing_key, data)
|
22
19
|
rescue OpenSSL::HMACError => e
|
23
|
-
if
|
24
|
-
raise JWT::DecodeError, 'OpenSSL 3.0 does not support nil or empty hmac_secret'
|
25
|
-
end
|
20
|
+
raise_verify_error!('OpenSSL 3.0 does not support nil or empty hmac_secret') if signing_key == '' && e.message == 'EVP_PKEY_new_mac_key: malloc failure'
|
26
21
|
|
27
22
|
raise e
|
28
23
|
end
|
29
24
|
|
30
|
-
def verify(
|
31
|
-
SecurityUtils.secure_compare(signature, sign(
|
25
|
+
def verify(data:, signature:, verification_key:)
|
26
|
+
SecurityUtils.secure_compare(signature, sign(data: data, signing_key: verification_key))
|
32
27
|
end
|
33
28
|
|
29
|
+
register_algorithm(new('HS256', OpenSSL::Digest::SHA256))
|
30
|
+
register_algorithm(new('HS384', OpenSSL::Digest::SHA384))
|
31
|
+
register_algorithm(new('HS512', OpenSSL::Digest::SHA512))
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
attr_reader :digest
|
36
|
+
|
34
37
|
# Copy of https://github.com/rails/rails/blob/v7.0.3.1/activesupport/lib/active_support/security_utils.rb
|
35
38
|
# rubocop:disable Naming/MethodParameterName, Style/StringLiterals, Style/NumericPredicate
|
36
39
|
module SecurityUtils
|
data/lib/jwt/jwa/none.rb
CHANGED
@@ -2,10 +2,13 @@
|
|
2
2
|
|
3
3
|
module JWT
|
4
4
|
module JWA
|
5
|
-
|
6
|
-
|
5
|
+
# Implementation of the none algorithm
|
6
|
+
class None
|
7
|
+
include JWT::JWA::SigningAlgorithm
|
7
8
|
|
8
|
-
|
9
|
+
def initialize
|
10
|
+
@alg = 'none'
|
11
|
+
end
|
9
12
|
|
10
13
|
def sign(*)
|
11
14
|
''
|
@@ -14,6 +17,8 @@ module JWT
|
|
14
17
|
def verify(*)
|
15
18
|
true
|
16
19
|
end
|
20
|
+
|
21
|
+
register_algorithm(new)
|
17
22
|
end
|
18
23
|
end
|
19
24
|
end
|
data/lib/jwt/jwa/ps.rb
CHANGED
@@ -2,29 +2,35 @@
|
|
2
2
|
|
3
3
|
module JWT
|
4
4
|
module JWA
|
5
|
-
|
6
|
-
|
5
|
+
# Implementation of the RSASSA-PSS family of algorithms
|
6
|
+
class Ps
|
7
|
+
include JWT::JWA::SigningAlgorithm
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
def sign(algorithm, msg, key)
|
13
|
-
unless key.is_a?(::OpenSSL::PKey::RSA)
|
14
|
-
raise EncodeError, "The given key is a #{key_class}. It has to be an OpenSSL::PKey::RSA instance."
|
15
|
-
end
|
9
|
+
def initialize(alg)
|
10
|
+
@alg = alg
|
11
|
+
@digest_algorithm = alg.sub('PS', 'sha')
|
12
|
+
end
|
16
13
|
|
17
|
-
|
14
|
+
def sign(data:, signing_key:)
|
15
|
+
raise_sign_error!("The given key is a #{signing_key.class}. It has to be an OpenSSL::PKey::RSA instance.") unless signing_key.is_a?(::OpenSSL::PKey::RSA)
|
16
|
+
raise_sign_error!('The key length must be greater than or equal to 2048 bits') if signing_key.n.num_bits < 2048
|
18
17
|
|
19
|
-
|
18
|
+
signing_key.sign_pss(digest_algorithm, data, salt_length: :digest, mgf1_hash: digest_algorithm)
|
20
19
|
end
|
21
20
|
|
22
|
-
def verify(
|
23
|
-
|
24
|
-
public_key.verify_pss(translated_algorithm, signature, signing_input, salt_length: :auto, mgf1_hash: translated_algorithm)
|
21
|
+
def verify(data:, signature:, verification_key:)
|
22
|
+
verification_key.verify_pss(digest_algorithm, signature, data, salt_length: :auto, mgf1_hash: digest_algorithm)
|
25
23
|
rescue OpenSSL::PKey::PKeyError
|
26
24
|
raise JWT::VerificationError, 'Signature verification raised'
|
27
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
|
28
34
|
end
|
29
35
|
end
|
30
36
|
end
|
data/lib/jwt/jwa/rsa.rb
CHANGED
@@ -2,24 +2,35 @@
|
|
2
2
|
|
3
3
|
module JWT
|
4
4
|
module JWA
|
5
|
-
|
6
|
-
|
5
|
+
# Implementation of the RSA family of algorithms
|
6
|
+
class Rsa
|
7
|
+
include JWT::JWA::SigningAlgorithm
|
7
8
|
|
8
|
-
|
9
|
+
def initialize(alg)
|
10
|
+
@alg = alg
|
11
|
+
@digest = OpenSSL::Digest.new(alg.sub('RS', 'SHA'))
|
12
|
+
end
|
9
13
|
|
10
|
-
def sign(
|
11
|
-
|
12
|
-
|
13
|
-
end
|
14
|
+
def sign(data:, signing_key:)
|
15
|
+
raise_sign_error!("The given key is a #{signing_key.class}. It has to be an OpenSSL::PKey::RSA instance") unless signing_key.is_a?(OpenSSL::PKey::RSA)
|
16
|
+
raise_sign_error!('The key length must be greater than or equal to 2048 bits') if signing_key.n.num_bits < 2048
|
14
17
|
|
15
|
-
|
18
|
+
signing_key.sign(digest, data)
|
16
19
|
end
|
17
20
|
|
18
|
-
def verify(
|
19
|
-
|
21
|
+
def verify(data:, signature:, verification_key:)
|
22
|
+
verification_key.verify(digest, signature, data)
|
20
23
|
rescue OpenSSL::PKey::PKeyError
|
21
24
|
raise JWT::VerificationError, 'Signature verification raised'
|
22
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
|
23
34
|
end
|
24
35
|
end
|
25
36
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
# JSON Web Algorithms
|
5
|
+
module JWA
|
6
|
+
# Base functionality for signing algorithms
|
7
|
+
module SigningAlgorithm
|
8
|
+
# Class methods for the SigningAlgorithm module
|
9
|
+
module ClassMethods
|
10
|
+
def register_algorithm(algo)
|
11
|
+
::JWT::JWA.register_algorithm(algo)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.included(klass)
|
16
|
+
klass.extend(ClassMethods)
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :alg
|
20
|
+
|
21
|
+
def valid_alg?(alg_to_check)
|
22
|
+
alg&.casecmp(alg_to_check)&.zero? == true
|
23
|
+
end
|
24
|
+
|
25
|
+
def header(*)
|
26
|
+
{ 'alg' => alg }
|
27
|
+
end
|
28
|
+
|
29
|
+
def sign(*)
|
30
|
+
raise_sign_error!('Algorithm implementation is missing the sign method')
|
31
|
+
end
|
32
|
+
|
33
|
+
def verify(*)
|
34
|
+
raise_verify_error!('Algorithm implementation is missing the verify method')
|
35
|
+
end
|
36
|
+
|
37
|
+
def raise_verify_error!(message)
|
38
|
+
raise(DecodeError.new(message).tap { |e| e.set_backtrace(caller(1)) })
|
39
|
+
end
|
40
|
+
|
41
|
+
def raise_sign_error!(message)
|
42
|
+
raise(EncodeError.new(message).tap { |e| e.set_backtrace(caller(1)) })
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class << self
|
47
|
+
def register_algorithm(algo)
|
48
|
+
algorithms[algo.alg.to_s.downcase] = algo
|
49
|
+
end
|
50
|
+
|
51
|
+
def find(algo)
|
52
|
+
algorithms.fetch(algo.to_s.downcase, Unsupported)
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def algorithms
|
58
|
+
@algorithms ||= {}
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
data/lib/jwt/jwa/unsupported.rb
CHANGED
@@ -2,17 +2,18 @@
|
|
2
2
|
|
3
3
|
module JWT
|
4
4
|
module JWA
|
5
|
+
# Represents an unsupported algorithm
|
5
6
|
module Unsupported
|
6
|
-
|
7
|
+
class << self
|
8
|
+
include JWT::JWA::SigningAlgorithm
|
7
9
|
|
8
|
-
|
10
|
+
def sign(*)
|
11
|
+
raise_sign_error!('Unsupported signing method')
|
12
|
+
end
|
9
13
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
def verify(*)
|
15
|
-
raise JWT::VerificationError, 'Algorithm not supported'
|
14
|
+
def verify(*)
|
15
|
+
raise JWT::VerificationError, 'Algorithm not supported'
|
16
|
+
end
|
16
17
|
end
|
17
18
|
end
|
18
19
|
end
|
data/lib/jwt/jwa.rb
CHANGED
@@ -2,60 +2,101 @@
|
|
2
2
|
|
3
3
|
require 'openssl'
|
4
4
|
|
5
|
-
|
6
|
-
require 'rbnacl'
|
7
|
-
rescue LoadError
|
8
|
-
raise if defined?(RbNaCl)
|
9
|
-
end
|
10
|
-
|
11
|
-
require_relative 'jwa/hmac'
|
12
|
-
require_relative 'jwa/eddsa'
|
5
|
+
require_relative 'jwa/signing_algorithm'
|
13
6
|
require_relative 'jwa/ecdsa'
|
14
|
-
require_relative 'jwa/
|
15
|
-
require_relative 'jwa/ps'
|
7
|
+
require_relative 'jwa/hmac'
|
16
8
|
require_relative 'jwa/none'
|
9
|
+
require_relative 'jwa/ps'
|
10
|
+
require_relative 'jwa/rsa'
|
17
11
|
require_relative 'jwa/unsupported'
|
18
|
-
require_relative 'jwa/wrapper'
|
19
12
|
|
20
13
|
module JWT
|
14
|
+
# The JWA module contains all supported algorithms.
|
21
15
|
module JWA
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
16
|
+
# @api private
|
17
|
+
class VerifierContext
|
18
|
+
attr_reader :jwa
|
19
|
+
|
20
|
+
def initialize(jwa:, keys:)
|
21
|
+
@jwa = jwa
|
22
|
+
@keys = Array(keys)
|
23
|
+
end
|
24
|
+
|
25
|
+
def verify(*args, **kwargs)
|
26
|
+
@keys.any? do |key|
|
27
|
+
@jwa.verify(*args, **kwargs, verification_key: key)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# @api private
|
33
|
+
class SignerContext
|
34
|
+
attr_reader :jwa
|
35
|
+
|
36
|
+
def initialize(jwa:, key:)
|
37
|
+
@jwa = jwa
|
38
|
+
@key = key
|
39
|
+
end
|
40
|
+
|
41
|
+
def sign(*args, **kwargs)
|
42
|
+
@jwa.sign(*args, **kwargs, signing_key: @key)
|
29
43
|
end
|
30
|
-
end
|
44
|
+
end
|
31
45
|
|
32
46
|
class << self
|
33
|
-
|
34
|
-
|
47
|
+
# @api private
|
48
|
+
def resolve(algorithm)
|
49
|
+
return find(algorithm) if algorithm.is_a?(String) || algorithm.is_a?(Symbol)
|
50
|
+
|
51
|
+
raise ArgumentError, 'Algorithm must be provided' if algorithm.nil?
|
52
|
+
|
53
|
+
raise ArgumentError, 'Custom algorithms are required to include JWT::JWA::SigningAlgorithm' unless algorithm.is_a?(SigningAlgorithm)
|
54
|
+
|
55
|
+
algorithm
|
35
56
|
end
|
36
57
|
|
37
|
-
|
38
|
-
|
58
|
+
# @api private
|
59
|
+
def resolve_and_sort(algorithms:, preferred_algorithm:)
|
60
|
+
Array(algorithms).map { |alg| JWA.resolve(alg) }
|
61
|
+
.partition { |alg| alg.valid_alg?(preferred_algorithm) }
|
62
|
+
.flatten
|
63
|
+
end
|
39
64
|
|
40
|
-
|
65
|
+
# @api private
|
66
|
+
def create_signer(algorithm:, key:)
|
67
|
+
if key.is_a?(JWK::KeyBase)
|
68
|
+
validate_jwk_algorithms!(key, algorithm, DecodeError)
|
69
|
+
|
70
|
+
return key
|
71
|
+
end
|
72
|
+
|
73
|
+
SignerContext.new(jwa: resolve(algorithm), key: key)
|
41
74
|
end
|
42
75
|
|
43
|
-
|
44
|
-
|
45
|
-
|
76
|
+
# @api private
|
77
|
+
def create_verifiers(algorithms:, keys:, preferred_algorithm:)
|
78
|
+
jwks, other_keys = keys.partition { |key| key.is_a?(JWK::KeyBase) }
|
79
|
+
|
80
|
+
validate_jwk_algorithms!(jwks, algorithms, VerificationError)
|
81
|
+
|
82
|
+
jwks + resolve_and_sort(algorithms: algorithms,
|
83
|
+
preferred_algorithm: preferred_algorithm)
|
84
|
+
.map { |jwa| VerifierContext.new(jwa: jwa, keys: other_keys) }
|
46
85
|
end
|
47
86
|
|
48
|
-
private
|
87
|
+
# @api private
|
88
|
+
def validate_jwk_algorithms!(jwks, algorithms, error_class)
|
89
|
+
algorithms = Array(algorithms)
|
90
|
+
|
91
|
+
return if algorithms.empty?
|
49
92
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
ALGOS.each_with_object(Hash.new(fallback)) do |cls, hash|
|
54
|
-
cls.const_get(:SUPPORTED).each do |alg|
|
55
|
-
hash[alg.downcase] = [alg, cls]
|
56
|
-
end
|
93
|
+
return if Array(jwks).all? do |jwk|
|
94
|
+
algorithms.any? do |alg|
|
95
|
+
jwk.jwa.valid_alg?(alg)
|
57
96
|
end
|
58
97
|
end
|
98
|
+
|
99
|
+
raise error_class, "Provided JWKs do not support one of the specified algorithms: #{algorithms.join(', ')}"
|
59
100
|
end
|
60
101
|
end
|
61
102
|
end
|
data/lib/jwt/jwk/ec.rb
CHANGED
@@ -4,6 +4,7 @@ require 'forwardable'
|
|
4
4
|
|
5
5
|
module JWT
|
6
6
|
module JWK
|
7
|
+
# JWK representation for Elliptic Curve (EC) keys
|
7
8
|
class EC < KeyBase # rubocop:disable Metrics/ClassLength
|
8
9
|
KTY = 'EC'
|
9
10
|
KTYS = [KTY, OpenSSL::PKey::EC, JWT::JWK::EC].freeze
|
@@ -65,15 +66,20 @@ module JWT
|
|
65
66
|
end
|
66
67
|
|
67
68
|
def []=(key, value)
|
68
|
-
if EC_KEY_ELEMENTS.include?(key.to_sym)
|
69
|
-
raise ArgumentError, 'cannot overwrite cryptographic key attributes'
|
70
|
-
end
|
69
|
+
raise ArgumentError, 'cannot overwrite cryptographic key attributes' if EC_KEY_ELEMENTS.include?(key.to_sym)
|
71
70
|
|
72
|
-
super
|
71
|
+
super
|
73
72
|
end
|
74
73
|
|
75
74
|
private
|
76
75
|
|
76
|
+
def jwa
|
77
|
+
return super if self[:alg]
|
78
|
+
|
79
|
+
curve_name = self.class.to_openssl_curve(self[:crv])
|
80
|
+
JWA.resolve(JWA::Ecdsa.curve_by_name(curve_name)[:algorithm])
|
81
|
+
end
|
82
|
+
|
77
83
|
def ec_key
|
78
84
|
@ec_key ||= create_ec_key(self[:crv], self[:x], self[:y], self[:d])
|
79
85
|
end
|
@@ -125,10 +131,6 @@ module JWT
|
|
125
131
|
::JWT::Base64.url_encode(octets)
|
126
132
|
end
|
127
133
|
|
128
|
-
def encode_open_ssl_bn(key_part)
|
129
|
-
::JWT::Base64.url_encode(key_part.to_s(BINARY))
|
130
|
-
end
|
131
|
-
|
132
134
|
def parse_ec_key(key)
|
133
135
|
crv, x_octets, y_octets = keypair_components(key)
|
134
136
|
octets = key.private_key&.to_bn&.to_s(BINARY)
|
@@ -141,67 +143,54 @@ module JWT
|
|
141
143
|
}.compact
|
142
144
|
end
|
143
145
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
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
|
146
|
+
def create_point(jwk_crv, jwk_x, jwk_y)
|
147
|
+
curve = EC.to_openssl_curve(jwk_crv)
|
148
|
+
x_octets = decode_octets(jwk_x)
|
149
|
+
y_octets = decode_octets(jwk_y)
|
176
150
|
|
151
|
+
# The details of the `Point` instantiation are covered in:
|
152
|
+
# - https://docs.ruby-lang.org/en/2.4.0/OpenSSL/PKey/EC.html
|
153
|
+
# - https://www.openssl.org/docs/manmaster/man3/EC_POINT_new.html
|
154
|
+
# - https://tools.ietf.org/html/rfc5480#section-2.2
|
155
|
+
# - https://www.secg.org/SEC1-Ver-1.0.pdf
|
156
|
+
# Section 2.3.3 of the last of these references specifies that the
|
157
|
+
# encoding of an uncompressed point consists of the byte `0x04` followed
|
158
|
+
# by the x value then the y value.
|
159
|
+
OpenSSL::PKey::EC::Point.new(
|
160
|
+
OpenSSL::PKey::EC::Group.new(curve),
|
161
|
+
OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2)
|
162
|
+
)
|
163
|
+
end
|
164
|
+
|
165
|
+
if ::JWT.openssl_3?
|
166
|
+
def create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d)
|
167
|
+
point = create_point(jwk_crv, jwk_x, jwk_y)
|
168
|
+
|
169
|
+
return ::JWT::JWA::Ecdsa.create_public_key_from_point(point) unless jwk_d
|
170
|
+
|
171
|
+
# https://datatracker.ietf.org/doc/html/rfc5915.html
|
172
|
+
# ECPrivateKey ::= SEQUENCE {
|
173
|
+
# version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1),
|
174
|
+
# privateKey OCTET STRING,
|
175
|
+
# parameters [0] ECParameters {{ NamedCurve }} OPTIONAL,
|
176
|
+
# publicKey [1] BIT STRING OPTIONAL
|
177
|
+
# }
|
178
|
+
|
179
|
+
sequence = OpenSSL::ASN1::Sequence([
|
180
|
+
OpenSSL::ASN1::Integer(1),
|
181
|
+
OpenSSL::ASN1::OctetString(OpenSSL::BN.new(decode_octets(jwk_d), 2).to_s(2)),
|
182
|
+
OpenSSL::ASN1::ObjectId(point.group.curve_name, 0, :EXPLICIT),
|
183
|
+
OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed), 1, :EXPLICIT)
|
184
|
+
])
|
177
185
|
OpenSSL::PKey::EC.new(sequence.to_der)
|
178
186
|
end
|
179
187
|
else
|
180
188
|
def create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d)
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
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
|
189
|
+
point = create_point(jwk_crv, jwk_x, jwk_y)
|
190
|
+
|
191
|
+
::JWT::JWA::Ecdsa.create_public_key_from_point(point).tap do |key|
|
192
|
+
key.private_key = OpenSSL::BN.new(decode_octets(jwk_d), 2) if jwk_d
|
193
|
+
end
|
205
194
|
end
|
206
195
|
end
|
207
196
|
|
@@ -210,7 +199,7 @@ module JWT
|
|
210
199
|
# Some base64 encoders on some platform omit a single 0-byte at
|
211
200
|
# the start of either Y or X coordinate of the elliptic curve point.
|
212
201
|
# This leads to an encoding error when data is passed to OpenSSL BN.
|
213
|
-
# It is know to have
|
202
|
+
# It is know to have happened to exported JWKs on a Java application and
|
214
203
|
# on a Flutter/Dart application (both iOS and Android). All that is
|
215
204
|
# needed to fix the problem is adding a leading 0-byte. We know the
|
216
205
|
# required byte is 0 because with any other byte the point is no longer
|