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/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
|