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/decode.rb
CHANGED
@@ -1,16 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'json'
|
4
|
-
|
5
|
-
require 'jwt/signature'
|
6
|
-
require 'jwt/verify'
|
7
4
|
require 'jwt/x5c_key_finder'
|
5
|
+
|
8
6
|
# JWT::Decode module
|
9
7
|
module JWT
|
10
8
|
# Decoding logic for JWT
|
11
9
|
class Decode
|
12
10
|
def initialize(jwt, key, verify, options, &keyfinder)
|
13
|
-
raise
|
11
|
+
raise JWT::DecodeError, 'Nil JSON web token' unless jwt
|
14
12
|
|
15
13
|
@jwt = jwt
|
16
14
|
@key = key
|
@@ -24,13 +22,13 @@ module JWT
|
|
24
22
|
def decode_segments
|
25
23
|
validate_segment_count!
|
26
24
|
if @verify
|
27
|
-
|
25
|
+
decode_signature
|
28
26
|
verify_algo
|
29
27
|
set_key
|
30
28
|
verify_signature
|
31
29
|
verify_claims
|
32
30
|
end
|
33
|
-
raise
|
31
|
+
raise JWT::DecodeError, 'Not enough or too many segments' unless header && payload
|
34
32
|
|
35
33
|
[payload, header]
|
36
34
|
end
|
@@ -46,45 +44,63 @@ module JWT
|
|
46
44
|
|
47
45
|
return if Array(@key).any? { |key| verify_signature_for?(key) }
|
48
46
|
|
49
|
-
raise
|
47
|
+
raise JWT::VerificationError, 'Signature verification failed'
|
50
48
|
end
|
51
49
|
|
52
50
|
def verify_algo
|
53
|
-
raise
|
54
|
-
raise
|
55
|
-
raise
|
51
|
+
raise JWT::IncorrectAlgorithm, 'An algorithm must be specified' if allowed_algorithms.empty?
|
52
|
+
raise JWT::DecodeError, 'Token header not a JSON object' unless header.is_a?(Hash)
|
53
|
+
raise JWT::IncorrectAlgorithm, 'Token is missing alg header' unless alg_in_header
|
54
|
+
raise JWT::IncorrectAlgorithm, 'Expected a different algorithm' if allowed_and_valid_algorithms.empty?
|
56
55
|
end
|
57
56
|
|
58
57
|
def set_key
|
59
58
|
@key = find_key(&@keyfinder) if @keyfinder
|
60
|
-
@key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks]).key_for(header['kid']) if @options[:jwks]
|
61
|
-
|
62
|
-
|
63
|
-
|
59
|
+
@key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks], allow_nil_kid: @options[:allow_nil_kid]).key_for(header['kid']) if @options[:jwks]
|
60
|
+
return unless (x5c_options = @options[:x5c])
|
61
|
+
|
62
|
+
@key = X5cKeyFinder.new(x5c_options[:root_certificates], x5c_options[:crls]).from(header['x5c'])
|
64
63
|
end
|
65
64
|
|
66
65
|
def verify_signature_for?(key)
|
67
|
-
|
66
|
+
allowed_and_valid_algorithms.any? do |alg|
|
67
|
+
alg.verify(data: signing_input, signature: @signature, verification_key: key)
|
68
|
+
end
|
68
69
|
end
|
69
70
|
|
70
|
-
def
|
71
|
-
allowed_algorithms.
|
71
|
+
def allowed_and_valid_algorithms
|
72
|
+
@allowed_and_valid_algorithms ||= allowed_algorithms.select { |alg| alg.valid_alg?(alg_in_header) }
|
72
73
|
end
|
73
74
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
else
|
85
|
-
[]
|
75
|
+
# Order is very important - first check for string keys, next for symbols
|
76
|
+
ALGORITHM_KEYS = ['algorithm',
|
77
|
+
:algorithm,
|
78
|
+
'algorithms',
|
79
|
+
:algorithms].freeze
|
80
|
+
|
81
|
+
def given_algorithms
|
82
|
+
ALGORITHM_KEYS.each do |alg_key|
|
83
|
+
alg = @options[alg_key]
|
84
|
+
return Array(alg) if alg
|
86
85
|
end
|
87
|
-
|
86
|
+
[]
|
87
|
+
end
|
88
|
+
|
89
|
+
def allowed_algorithms
|
90
|
+
@allowed_algorithms ||= resolve_allowed_algorithms
|
91
|
+
end
|
92
|
+
|
93
|
+
def resolve_allowed_algorithms
|
94
|
+
algs = given_algorithms.map { |alg| JWA.resolve(alg) }
|
95
|
+
|
96
|
+
sort_by_alg_header(algs)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Move algorithms matching the JWT alg header to the beginning of the list
|
100
|
+
def sort_by_alg_header(algs)
|
101
|
+
return algs if algs.size <= 1
|
102
|
+
|
103
|
+
algs.partition { |alg| alg.valid_alg?(alg_in_header) }.flatten
|
88
104
|
end
|
89
105
|
|
90
106
|
def find_key(&keyfinder)
|
@@ -96,8 +112,7 @@ module JWT
|
|
96
112
|
end
|
97
113
|
|
98
114
|
def verify_claims
|
99
|
-
|
100
|
-
Verify.verify_required_claims(payload, @options)
|
115
|
+
Claims::DecodeVerifier.verify!(payload, @options)
|
101
116
|
end
|
102
117
|
|
103
118
|
def validate_segment_count!
|
@@ -105,7 +120,7 @@ module JWT
|
|
105
120
|
return if !@verify && segment_length == 2 # If no verifying required, the signature is not needed
|
106
121
|
return if segment_length == 2 && none_algorithm?
|
107
122
|
|
108
|
-
raise
|
123
|
+
raise JWT::DecodeError, 'Not enough or too many segments'
|
109
124
|
end
|
110
125
|
|
111
126
|
def segment_length
|
@@ -113,16 +128,14 @@ module JWT
|
|
113
128
|
end
|
114
129
|
|
115
130
|
def none_algorithm?
|
116
|
-
|
131
|
+
alg_in_header == 'none'
|
117
132
|
end
|
118
133
|
|
119
|
-
def
|
120
|
-
@signature = Base64.
|
121
|
-
rescue ArgumentError
|
122
|
-
raise(JWT::DecodeError, 'Invalid segment encoding')
|
134
|
+
def decode_signature
|
135
|
+
@signature = ::JWT::Base64.url_decode(@segments[2] || '')
|
123
136
|
end
|
124
137
|
|
125
|
-
def
|
138
|
+
def alg_in_header
|
126
139
|
header['alg']
|
127
140
|
end
|
128
141
|
|
@@ -139,8 +152,8 @@ module JWT
|
|
139
152
|
end
|
140
153
|
|
141
154
|
def parse_and_decode(segment)
|
142
|
-
JWT::JSON.parse(Base64.
|
143
|
-
rescue ::JSON::ParserError
|
155
|
+
JWT::JSON.parse(::JWT::Base64.url_decode(segment))
|
156
|
+
rescue ::JSON::ParserError
|
144
157
|
raise JWT::DecodeError, 'Invalid segment encoding'
|
145
158
|
end
|
146
159
|
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
# Deprecations module to handle deprecation warnings in the gem
|
5
|
+
module Deprecations
|
6
|
+
class << self
|
7
|
+
def context
|
8
|
+
yield.tap { emit_warnings }
|
9
|
+
ensure
|
10
|
+
Thread.current[:jwt_warning_store] = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def warning(message, only_if_valid: false)
|
14
|
+
method_name = only_if_valid ? :store : :warn
|
15
|
+
case JWT.configuration.deprecation_warnings
|
16
|
+
when :once
|
17
|
+
return if record_warned(message)
|
18
|
+
when :warn
|
19
|
+
# noop
|
20
|
+
else
|
21
|
+
return
|
22
|
+
end
|
23
|
+
|
24
|
+
send(method_name, "[DEPRECATION WARNING] #{message}")
|
25
|
+
end
|
26
|
+
|
27
|
+
def store(message)
|
28
|
+
(Thread.current[:jwt_warning_store] ||= []) << message
|
29
|
+
end
|
30
|
+
|
31
|
+
def emit_warnings
|
32
|
+
return if Thread.current[:jwt_warning_store].nil?
|
33
|
+
|
34
|
+
Thread.current[:jwt_warning_store].each { |warning| warn(warning) }
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def record_warned(message)
|
40
|
+
@warned ||= []
|
41
|
+
return true if @warned.include?(message)
|
42
|
+
|
43
|
+
@warned << message
|
44
|
+
false
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/jwt/encode.rb
CHANGED
@@ -1,24 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative '
|
4
|
-
require_relative './claims_validator'
|
3
|
+
require_relative 'jwa'
|
5
4
|
|
6
5
|
# JWT::Encode module
|
7
6
|
module JWT
|
8
7
|
# Encoding logic for JWT
|
9
8
|
class Encode
|
10
|
-
ALG_NONE = 'none'
|
11
|
-
ALG_KEY = 'alg'
|
12
|
-
|
13
9
|
def initialize(options)
|
14
|
-
@payload
|
15
|
-
@key
|
16
|
-
|
17
|
-
@headers
|
10
|
+
@payload = options[:payload]
|
11
|
+
@key = options[:key]
|
12
|
+
@algorithm = JWA.resolve(options[:algorithm])
|
13
|
+
@headers = options[:headers].transform_keys(&:to_s)
|
18
14
|
end
|
19
15
|
|
20
16
|
def segments
|
21
|
-
|
17
|
+
validate_claims!
|
18
|
+
combine(encoded_header_and_payload, encoded_signature)
|
22
19
|
end
|
23
20
|
|
24
21
|
private
|
@@ -40,26 +37,29 @@ module JWT
|
|
40
37
|
end
|
41
38
|
|
42
39
|
def encode_header
|
43
|
-
@headers
|
44
|
-
encode(@headers)
|
40
|
+
encode_data(@headers.merge(@algorithm.header(signing_key: @key)))
|
45
41
|
end
|
46
42
|
|
47
43
|
def encode_payload
|
48
|
-
|
49
|
-
|
50
|
-
end
|
44
|
+
encode_data(@payload)
|
45
|
+
end
|
51
46
|
|
52
|
-
|
47
|
+
def signature
|
48
|
+
@algorithm.sign(data: encoded_header_and_payload, signing_key: @key)
|
53
49
|
end
|
54
50
|
|
55
|
-
def
|
56
|
-
return
|
51
|
+
def validate_claims!
|
52
|
+
return unless @payload.is_a?(Hash)
|
57
53
|
|
58
|
-
|
54
|
+
Claims.verify_payload!(@payload, :numeric)
|
55
|
+
end
|
56
|
+
|
57
|
+
def encode_signature
|
58
|
+
::JWT::Base64.url_encode(signature)
|
59
59
|
end
|
60
60
|
|
61
|
-
def
|
62
|
-
Base64.
|
61
|
+
def encode_data(data)
|
62
|
+
::JWT::Base64.url_encode(JWT::JSON.generate(data))
|
63
63
|
end
|
64
64
|
|
65
65
|
def combine(*parts)
|
data/lib/jwt/error.rb
CHANGED
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module JWA
|
5
|
+
module Compat
|
6
|
+
module ClassMethods
|
7
|
+
def from_algorithm(algorithm)
|
8
|
+
new(algorithm)
|
9
|
+
end
|
10
|
+
|
11
|
+
def sign(algorithm, msg, key)
|
12
|
+
Deprecations.warning('Support for calling sign with positional arguments will be removed in future ruby-jwt versions')
|
13
|
+
|
14
|
+
from_algorithm(algorithm).sign(data: msg, signing_key: key)
|
15
|
+
end
|
16
|
+
|
17
|
+
def verify(algorithm, key, signing_input, signature)
|
18
|
+
Deprecations.warning('Support for calling verify with positional arguments will be removed in future ruby-jwt versions')
|
19
|
+
|
20
|
+
from_algorithm(algorithm).verify(data: signing_input, signature: signature, verification_key: key)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.included(klass)
|
25
|
+
klass.extend(ClassMethods)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module JWA
|
5
|
+
class Ecdsa
|
6
|
+
include JWT::JWA::SigningAlgorithm
|
7
|
+
|
8
|
+
def initialize(alg, digest)
|
9
|
+
@alg = alg
|
10
|
+
@digest = OpenSSL::Digest.new(digest)
|
11
|
+
end
|
12
|
+
|
13
|
+
def sign(data:, signing_key:)
|
14
|
+
curve_definition = curve_by_name(signing_key.group.curve_name)
|
15
|
+
key_algorithm = curve_definition[:algorithm]
|
16
|
+
if alg != key_algorithm
|
17
|
+
raise IncorrectAlgorithm, "payload algorithm is #{alg} but #{key_algorithm} signing key was provided"
|
18
|
+
end
|
19
|
+
|
20
|
+
asn1_to_raw(signing_key.dsa_sign_asn1(digest.digest(data)), signing_key)
|
21
|
+
end
|
22
|
+
|
23
|
+
def verify(data:, signature:, verification_key:)
|
24
|
+
curve_definition = curve_by_name(verification_key.group.curve_name)
|
25
|
+
key_algorithm = curve_definition[:algorithm]
|
26
|
+
if alg != key_algorithm
|
27
|
+
raise IncorrectAlgorithm, "payload algorithm is #{alg} but #{key_algorithm} verification key was provided"
|
28
|
+
end
|
29
|
+
|
30
|
+
verification_key.dsa_verify_asn1(digest.digest(data), raw_to_asn1(signature, verification_key))
|
31
|
+
rescue OpenSSL::PKey::PKeyError
|
32
|
+
raise JWT::VerificationError, 'Signature verification raised'
|
33
|
+
end
|
34
|
+
|
35
|
+
NAMED_CURVES = {
|
36
|
+
'prime256v1' => {
|
37
|
+
algorithm: 'ES256',
|
38
|
+
digest: 'sha256'
|
39
|
+
},
|
40
|
+
'secp256r1' => { # alias for prime256v1
|
41
|
+
algorithm: 'ES256',
|
42
|
+
digest: 'sha256'
|
43
|
+
},
|
44
|
+
'secp384r1' => {
|
45
|
+
algorithm: 'ES384',
|
46
|
+
digest: 'sha384'
|
47
|
+
},
|
48
|
+
'secp521r1' => {
|
49
|
+
algorithm: 'ES512',
|
50
|
+
digest: 'sha512'
|
51
|
+
},
|
52
|
+
'secp256k1' => {
|
53
|
+
algorithm: 'ES256K',
|
54
|
+
digest: 'sha256'
|
55
|
+
}
|
56
|
+
}.freeze
|
57
|
+
|
58
|
+
NAMED_CURVES.each_value do |v|
|
59
|
+
register_algorithm(new(v[:algorithm], v[:digest]))
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.from_algorithm(algorithm)
|
63
|
+
new(algorithm, algorithm.downcase.gsub('es', 'sha'))
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.curve_by_name(name)
|
67
|
+
NAMED_CURVES.fetch(name) do
|
68
|
+
raise UnsupportedEcdsaCurve, "The ECDSA curve '#{name}' is not supported"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
attr_reader :digest
|
75
|
+
|
76
|
+
def curve_by_name(name)
|
77
|
+
self.class.curve_by_name(name)
|
78
|
+
end
|
79
|
+
|
80
|
+
def raw_to_asn1(signature, private_key)
|
81
|
+
byte_size = (private_key.group.degree + 7) / 8
|
82
|
+
sig_bytes = signature[0..(byte_size - 1)]
|
83
|
+
sig_char = signature[byte_size..-1] || ''
|
84
|
+
OpenSSL::ASN1::Sequence.new([sig_bytes, sig_char].map { |int| OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(int, 2)) }).to_der
|
85
|
+
end
|
86
|
+
|
87
|
+
def asn1_to_raw(signature, public_key)
|
88
|
+
byte_size = (public_key.group.degree + 7) / 8
|
89
|
+
OpenSSL::ASN1.decode(signature).value.map { |value| value.value.to_s(2).rjust(byte_size, "\x00") }.join
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module JWA
|
5
|
+
class Eddsa
|
6
|
+
include JWT::JWA::SigningAlgorithm
|
7
|
+
|
8
|
+
def initialize(alg)
|
9
|
+
@alg = alg
|
10
|
+
end
|
11
|
+
|
12
|
+
def sign(data:, signing_key:)
|
13
|
+
unless signing_key.is_a?(RbNaCl::Signatures::Ed25519::SigningKey)
|
14
|
+
raise_encode_error!("Key given is a #{signing_key.class} but has to be an RbNaCl::Signatures::Ed25519::SigningKey")
|
15
|
+
end
|
16
|
+
|
17
|
+
signing_key.sign(data)
|
18
|
+
end
|
19
|
+
|
20
|
+
def verify(data:, signature:, verification_key:)
|
21
|
+
unless verification_key.is_a?(RbNaCl::Signatures::Ed25519::VerifyKey)
|
22
|
+
raise_decode_error!("key given is a #{verification_key.class} but has to be a RbNaCl::Signatures::Ed25519::VerifyKey")
|
23
|
+
end
|
24
|
+
|
25
|
+
verification_key.verify(signature, data)
|
26
|
+
rescue RbNaCl::CryptoError
|
27
|
+
false
|
28
|
+
end
|
29
|
+
|
30
|
+
register_algorithm(new('ED25519'))
|
31
|
+
register_algorithm(new('EdDSA'))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/jwt/jwa/hmac.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module JWA
|
5
|
+
class Hmac
|
6
|
+
include JWT::JWA::SigningAlgorithm
|
7
|
+
|
8
|
+
def self.from_algorithm(algorithm)
|
9
|
+
new(algorithm, OpenSSL::Digest.new(algorithm.downcase.gsub('hs', 'sha')))
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(alg, digest)
|
13
|
+
@alg = alg
|
14
|
+
@digest = digest
|
15
|
+
end
|
16
|
+
|
17
|
+
def sign(data:, signing_key:)
|
18
|
+
signing_key ||= ''
|
19
|
+
raise_verify_error!('HMAC key expected to be a String') unless signing_key.is_a?(String)
|
20
|
+
|
21
|
+
OpenSSL::HMAC.digest(digest.new, signing_key, data)
|
22
|
+
rescue OpenSSL::HMACError => e
|
23
|
+
if signing_key == '' && e.message == 'EVP_PKEY_new_mac_key: malloc failure'
|
24
|
+
raise_verify_error!('OpenSSL 3.0 does not support nil or empty hmac_secret')
|
25
|
+
end
|
26
|
+
|
27
|
+
raise e
|
28
|
+
end
|
29
|
+
|
30
|
+
def verify(data:, signature:, verification_key:)
|
31
|
+
SecurityUtils.secure_compare(signature, sign(data: data, signing_key: verification_key))
|
32
|
+
end
|
33
|
+
|
34
|
+
register_algorithm(new('HS256', OpenSSL::Digest::SHA256))
|
35
|
+
register_algorithm(new('HS384', OpenSSL::Digest::SHA384))
|
36
|
+
register_algorithm(new('HS512', OpenSSL::Digest::SHA512))
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
attr_reader :digest
|
41
|
+
|
42
|
+
# Copy of https://github.com/rails/rails/blob/v7.0.3.1/activesupport/lib/active_support/security_utils.rb
|
43
|
+
# rubocop:disable Naming/MethodParameterName, Style/StringLiterals, Style/NumericPredicate
|
44
|
+
module SecurityUtils
|
45
|
+
# Constant time string comparison, for fixed length strings.
|
46
|
+
#
|
47
|
+
# The values compared should be of fixed length, such as strings
|
48
|
+
# that have already been processed by HMAC. Raises in case of length mismatch.
|
49
|
+
|
50
|
+
if defined?(OpenSSL.fixed_length_secure_compare)
|
51
|
+
def fixed_length_secure_compare(a, b)
|
52
|
+
OpenSSL.fixed_length_secure_compare(a, b)
|
53
|
+
end
|
54
|
+
else
|
55
|
+
# :nocov:
|
56
|
+
def fixed_length_secure_compare(a, b)
|
57
|
+
raise ArgumentError, "string length mismatch." unless a.bytesize == b.bytesize
|
58
|
+
|
59
|
+
l = a.unpack "C#{a.bytesize}"
|
60
|
+
|
61
|
+
res = 0
|
62
|
+
b.each_byte { |byte| res |= byte ^ l.shift }
|
63
|
+
res == 0
|
64
|
+
end
|
65
|
+
# :nocov:
|
66
|
+
end
|
67
|
+
module_function :fixed_length_secure_compare
|
68
|
+
|
69
|
+
# Secure string comparison for strings of variable length.
|
70
|
+
#
|
71
|
+
# While a timing attack would not be able to discern the content of
|
72
|
+
# a secret compared via secure_compare, it is possible to determine
|
73
|
+
# the secret length. This should be considered when using secure_compare
|
74
|
+
# to compare weak, short secrets to user input.
|
75
|
+
def secure_compare(a, b)
|
76
|
+
a.bytesize == b.bytesize && fixed_length_secure_compare(a, b)
|
77
|
+
end
|
78
|
+
module_function :secure_compare
|
79
|
+
end
|
80
|
+
# rubocop:enable Naming/MethodParameterName, Style/StringLiterals, Style/NumericPredicate
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module JWA
|
5
|
+
class HmacRbNaCl
|
6
|
+
include JWT::JWA::SigningAlgorithm
|
7
|
+
|
8
|
+
def self.from_algorithm(algorithm)
|
9
|
+
new(algorithm, ::RbNaCl::HMAC.const_get(algorithm.upcase.gsub('HS', 'SHA')))
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(alg, hmac)
|
13
|
+
@alg = alg
|
14
|
+
@hmac = hmac
|
15
|
+
end
|
16
|
+
|
17
|
+
def sign(data:, signing_key:)
|
18
|
+
Deprecations.warning("The use of the algorithm #{alg} is deprecated and will be removed in the next major version of ruby-jwt")
|
19
|
+
hmac.auth(key_for_rbnacl(hmac, signing_key).encode('binary'), data.encode('binary'))
|
20
|
+
end
|
21
|
+
|
22
|
+
def verify(data:, signature:, verification_key:)
|
23
|
+
Deprecations.warning("The use of the algorithm #{alg} is deprecated and will be removed in the next major version of ruby-jwt")
|
24
|
+
hmac.verify(key_for_rbnacl(hmac, verification_key).encode('binary'), signature.encode('binary'), data.encode('binary'))
|
25
|
+
rescue ::RbNaCl::BadAuthenticatorError, ::RbNaCl::LengthError
|
26
|
+
false
|
27
|
+
end
|
28
|
+
|
29
|
+
register_algorithm(new('HS512256', ::RbNaCl::HMAC::SHA512256))
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_reader :hmac
|
34
|
+
|
35
|
+
def key_for_rbnacl(hmac, key)
|
36
|
+
key ||= ''
|
37
|
+
raise JWT::DecodeError, 'HMAC key expected to be a String' unless key.is_a?(String)
|
38
|
+
|
39
|
+
return padded_empty_key(hmac.key_bytes) if key == ''
|
40
|
+
|
41
|
+
key
|
42
|
+
end
|
43
|
+
|
44
|
+
def padded_empty_key(length)
|
45
|
+
Array.new(length, 0x0).pack('C*').encode('binary')
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module JWA
|
5
|
+
class HmacRbNaClFixed
|
6
|
+
include JWT::JWA::SigningAlgorithm
|
7
|
+
|
8
|
+
def self.from_algorithm(algorithm)
|
9
|
+
new(algorithm, ::RbNaCl::HMAC.const_get(algorithm.upcase.gsub('HS', 'SHA')))
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(alg, hmac)
|
13
|
+
@alg = alg
|
14
|
+
@hmac = hmac
|
15
|
+
end
|
16
|
+
|
17
|
+
def sign(data:, signing_key:)
|
18
|
+
signing_key ||= ''
|
19
|
+
Deprecations.warning("The use of the algorithm #{alg} is deprecated and will be removed in the next major version of ruby-jwt")
|
20
|
+
raise JWT::DecodeError, 'HMAC key expected to be a String' unless signing_key.is_a?(String)
|
21
|
+
|
22
|
+
hmac.auth(padded_key_bytes(signing_key, hmac.key_bytes), data.encode('binary'))
|
23
|
+
end
|
24
|
+
|
25
|
+
def verify(data:, signature:, verification_key:)
|
26
|
+
verification_key ||= ''
|
27
|
+
Deprecations.warning("The use of the algorithm #{alg} is deprecated and will be removed in the next major version of ruby-jwt")
|
28
|
+
raise JWT::DecodeError, 'HMAC key expected to be a String' unless verification_key.is_a?(String)
|
29
|
+
|
30
|
+
hmac.verify(padded_key_bytes(verification_key, hmac.key_bytes), signature.encode('binary'), data.encode('binary'))
|
31
|
+
rescue ::RbNaCl::BadAuthenticatorError, ::RbNaCl::LengthError
|
32
|
+
false
|
33
|
+
end
|
34
|
+
|
35
|
+
register_algorithm(new('HS512256', ::RbNaCl::HMAC::SHA512256))
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
attr_reader :hmac
|
40
|
+
|
41
|
+
def padded_key_bytes(key, bytesize)
|
42
|
+
key.bytes.fill(0, key.bytesize...bytesize).pack('C*')
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/jwt/jwa/none.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module JWA
|
5
|
+
class None
|
6
|
+
include JWT::JWA::SigningAlgorithm
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@alg = 'none'
|
10
|
+
end
|
11
|
+
|
12
|
+
def sign(*)
|
13
|
+
''
|
14
|
+
end
|
15
|
+
|
16
|
+
def verify(*)
|
17
|
+
true
|
18
|
+
end
|
19
|
+
|
20
|
+
register_algorithm(new)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|