jwt 2.2.1 → 2.8.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/AUTHORS +79 -44
- data/CHANGELOG.md +305 -20
- data/CODE_OF_CONDUCT.md +84 -0
- data/CONTRIBUTING.md +99 -0
- data/README.md +268 -40
- data/lib/jwt/base64.rb +16 -2
- data/lib/jwt/claims_validator.rb +13 -9
- 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 +80 -18
- data/lib/jwt/deprecations.rb +29 -0
- data/lib/jwt/encode.rb +24 -19
- data/lib/jwt/error.rb +17 -14
- data/lib/jwt/jwa/ecdsa.rb +76 -0
- data/lib/jwt/jwa/eddsa.rb +42 -0
- data/lib/jwt/jwa/hmac.rb +75 -0
- data/lib/jwt/jwa/hmac_rbnacl.rb +50 -0
- data/lib/jwt/jwa/hmac_rbnacl_fixed.rb +46 -0
- data/lib/jwt/jwa/none.rb +19 -0
- data/lib/jwt/jwa/ps.rb +30 -0
- data/lib/jwt/jwa/rsa.rb +25 -0
- data/lib/jwt/{algos → jwa}/unsupported.rb +8 -5
- data/lib/jwt/jwa/wrapper.rb +26 -0
- data/lib/jwt/jwa.rb +62 -0
- data/lib/jwt/jwk/ec.rb +251 -0
- data/lib/jwt/jwk/hmac.rb +103 -0
- data/lib/jwt/jwk/key_base.rb +57 -0
- data/lib/jwt/jwk/key_finder.rb +19 -30
- 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 +181 -25
- data/lib/jwt/jwk/set.rb +80 -0
- data/lib/jwt/jwk/thumbprint.rb +26 -0
- data/lib/jwt/jwk.rb +39 -15
- data/lib/jwt/verify.rb +25 -6
- data/lib/jwt/version.rb +24 -3
- data/lib/jwt/x5c_key_finder.rb +52 -0
- data/lib/jwt.rb +6 -4
- data/ruby-jwt.gemspec +18 -10
- metadata +45 -76
- data/.codeclimate.yml +0 -20
- data/.ebert.yml +0 -18
- data/.gitignore +0 -11
- data/.rspec +0 -1
- data/.rubocop.yml +0 -98
- data/.travis.yml +0 -20
- data/Appraisals +0 -14
- data/Gemfile +0 -3
- data/Rakefile +0 -11
- data/lib/jwt/algos/ecdsa.rb +0 -35
- data/lib/jwt/algos/eddsa.rb +0 -23
- data/lib/jwt/algos/hmac.rb +0 -33
- data/lib/jwt/algos/ps.rb +0 -43
- data/lib/jwt/algos/rsa.rb +0 -19
- data/lib/jwt/default_options.rb +0 -15
- data/lib/jwt/security_utils.rb +0 -57
- data/lib/jwt/signature.rb +0 -52
data/lib/jwt/claims_validator.rb
CHANGED
@@ -1,33 +1,37 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'error'
|
2
4
|
|
3
5
|
module JWT
|
4
6
|
class ClaimsValidator
|
5
|
-
|
7
|
+
NUMERIC_CLAIMS = %i[
|
6
8
|
exp
|
7
9
|
iat
|
8
10
|
nbf
|
9
11
|
].freeze
|
10
12
|
|
11
13
|
def initialize(payload)
|
12
|
-
@payload = payload.
|
14
|
+
@payload = payload.transform_keys(&:to_sym)
|
13
15
|
end
|
14
16
|
|
15
17
|
def validate!
|
16
|
-
|
18
|
+
validate_numeric_claims
|
17
19
|
|
18
20
|
true
|
19
21
|
end
|
20
22
|
|
21
23
|
private
|
22
24
|
|
23
|
-
def
|
24
|
-
|
25
|
-
|
25
|
+
def validate_numeric_claims
|
26
|
+
NUMERIC_CLAIMS.each do |claim|
|
27
|
+
validate_is_numeric(claim) if @payload.key?(claim)
|
26
28
|
end
|
27
29
|
end
|
28
30
|
|
29
|
-
def
|
30
|
-
|
31
|
+
def validate_is_numeric(claim)
|
32
|
+
return if @payload[claim].is_a?(Numeric)
|
33
|
+
|
34
|
+
raise InvalidPayload, "#{claim} claim must be a Numeric value but it is a #{@payload[claim].class}"
|
31
35
|
end
|
32
36
|
end
|
33
37
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'decode_configuration'
|
4
|
+
require_relative 'jwk_configuration'
|
5
|
+
|
6
|
+
module JWT
|
7
|
+
module Configuration
|
8
|
+
class Container
|
9
|
+
attr_accessor :decode, :jwk, :strict_base64_decoding
|
10
|
+
attr_reader :deprecation_warnings
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
reset!
|
14
|
+
end
|
15
|
+
|
16
|
+
def reset!
|
17
|
+
@decode = DecodeConfiguration.new
|
18
|
+
@jwk = JwkConfiguration.new
|
19
|
+
@strict_base64_decoding = false
|
20
|
+
|
21
|
+
self.deprecation_warnings = :once
|
22
|
+
end
|
23
|
+
|
24
|
+
DEPRECATION_WARNINGS_VALUES = %i[once warn silent].freeze
|
25
|
+
def deprecation_warnings=(value)
|
26
|
+
raise ArgumentError, "Invalid deprecation_warnings value #{value}. Supported values: #{DEPRECATION_WARNINGS_VALUES}" unless DEPRECATION_WARNINGS_VALUES.include?(value)
|
27
|
+
|
28
|
+
@deprecation_warnings = value
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module Configuration
|
5
|
+
class DecodeConfiguration
|
6
|
+
attr_accessor :verify_expiration,
|
7
|
+
:verify_not_before,
|
8
|
+
:verify_iss,
|
9
|
+
:verify_iat,
|
10
|
+
:verify_jti,
|
11
|
+
:verify_aud,
|
12
|
+
:verify_sub,
|
13
|
+
:leeway,
|
14
|
+
:algorithms,
|
15
|
+
:required_claims
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@verify_expiration = true
|
19
|
+
@verify_not_before = true
|
20
|
+
@verify_iss = false
|
21
|
+
@verify_iat = false
|
22
|
+
@verify_jti = false
|
23
|
+
@verify_aud = false
|
24
|
+
@verify_sub = false
|
25
|
+
@leeway = 0
|
26
|
+
@algorithms = ['HS256']
|
27
|
+
@required_claims = []
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_h
|
31
|
+
{
|
32
|
+
verify_expiration: verify_expiration,
|
33
|
+
verify_not_before: verify_not_before,
|
34
|
+
verify_iss: verify_iss,
|
35
|
+
verify_iat: verify_iat,
|
36
|
+
verify_jti: verify_jti,
|
37
|
+
verify_aud: verify_aud,
|
38
|
+
verify_sub: verify_sub,
|
39
|
+
leeway: leeway,
|
40
|
+
algorithms: algorithms,
|
41
|
+
required_claims: required_claims
|
42
|
+
}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../jwk/kid_as_key_digest'
|
4
|
+
require_relative '../jwk/thumbprint'
|
5
|
+
|
6
|
+
module JWT
|
7
|
+
module Configuration
|
8
|
+
class JwkConfiguration
|
9
|
+
def initialize
|
10
|
+
self.kid_generator_type = :key_digest
|
11
|
+
end
|
12
|
+
|
13
|
+
def kid_generator_type=(value)
|
14
|
+
self.kid_generator = case value
|
15
|
+
when :key_digest
|
16
|
+
JWT::JWK::KidAsKeyDigest
|
17
|
+
when :rfc7638_thumbprint
|
18
|
+
JWT::JWK::Thumbprint
|
19
|
+
else
|
20
|
+
raise ArgumentError, "#{value} is not a valid kid generator type."
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_accessor :kid_generator
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'configuration/container'
|
4
|
+
|
5
|
+
module JWT
|
6
|
+
module Configuration
|
7
|
+
def configure
|
8
|
+
yield(configuration)
|
9
|
+
end
|
10
|
+
|
11
|
+
def configuration
|
12
|
+
@configuration ||= ::JWT::Configuration::Container.new
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/jwt/decode.rb
CHANGED
@@ -2,14 +2,16 @@
|
|
2
2
|
|
3
3
|
require 'json'
|
4
4
|
|
5
|
-
require 'jwt/signature'
|
6
5
|
require 'jwt/verify'
|
6
|
+
require 'jwt/x5c_key_finder'
|
7
|
+
|
7
8
|
# JWT::Decode module
|
8
9
|
module JWT
|
9
10
|
# Decoding logic for JWT
|
10
11
|
class Decode
|
11
12
|
def initialize(jwt, key, verify, options, &keyfinder)
|
12
13
|
raise(JWT::DecodeError, 'Nil JSON web token') unless jwt
|
14
|
+
|
13
15
|
@jwt = jwt
|
14
16
|
@key = key
|
15
17
|
@options = options
|
@@ -22,51 +24,103 @@ module JWT
|
|
22
24
|
def decode_segments
|
23
25
|
validate_segment_count!
|
24
26
|
if @verify
|
25
|
-
|
27
|
+
decode_signature
|
28
|
+
verify_algo
|
29
|
+
set_key
|
26
30
|
verify_signature
|
27
31
|
verify_claims
|
28
32
|
end
|
29
33
|
raise(JWT::DecodeError, 'Not enough or too many segments') unless header && payload
|
34
|
+
|
30
35
|
[payload, header]
|
31
36
|
end
|
32
37
|
|
33
38
|
private
|
34
39
|
|
35
40
|
def verify_signature
|
36
|
-
@key
|
37
|
-
|
41
|
+
return unless @key || @verify
|
42
|
+
|
43
|
+
return if none_algorithm?
|
44
|
+
|
45
|
+
raise JWT::DecodeError, 'No verification key available' unless @key
|
46
|
+
|
47
|
+
return if Array(@key).any? { |key| verify_signature_for?(key) }
|
48
|
+
|
49
|
+
raise(JWT::VerificationError, 'Signature verification failed')
|
50
|
+
end
|
38
51
|
|
52
|
+
def verify_algo
|
39
53
|
raise(JWT::IncorrectAlgorithm, 'An algorithm must be specified') if allowed_algorithms.empty?
|
40
|
-
raise(JWT::IncorrectAlgorithm, '
|
54
|
+
raise(JWT::IncorrectAlgorithm, 'Token is missing alg header') unless alg_in_header
|
55
|
+
raise(JWT::IncorrectAlgorithm, 'Expected a different algorithm') if allowed_and_valid_algorithms.empty?
|
56
|
+
end
|
57
|
+
|
58
|
+
def set_key
|
59
|
+
@key = find_key(&@keyfinder) if @keyfinder
|
60
|
+
@key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks], allow_nil_kid: @options[:allow_nil_kid]).key_for(header['kid']) if @options[:jwks]
|
61
|
+
if (x5c_options = @options[:x5c])
|
62
|
+
@key = X5cKeyFinder.new(x5c_options[:root_certificates], x5c_options[:crls]).from(header['x5c'])
|
63
|
+
end
|
64
|
+
end
|
41
65
|
|
42
|
-
|
66
|
+
def verify_signature_for?(key)
|
67
|
+
allowed_and_valid_algorithms.any? do |alg|
|
68
|
+
alg.verify(data: signing_input, signature: @signature, verification_key: key)
|
69
|
+
end
|
43
70
|
end
|
44
71
|
|
45
|
-
def
|
46
|
-
allowed_algorithms.
|
72
|
+
def allowed_and_valid_algorithms
|
73
|
+
@allowed_and_valid_algorithms ||= allowed_algorithms.select { |alg| alg.valid_alg?(alg_in_header) }
|
47
74
|
end
|
48
75
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
76
|
+
# Order is very important - first check for string keys, next for symbols
|
77
|
+
ALGORITHM_KEYS = ['algorithm',
|
78
|
+
:algorithm,
|
79
|
+
'algorithms',
|
80
|
+
:algorithms].freeze
|
81
|
+
|
82
|
+
def given_algorithms
|
83
|
+
ALGORITHM_KEYS.each do |alg_key|
|
84
|
+
alg = @options[alg_key]
|
85
|
+
return Array(alg) if alg
|
54
86
|
end
|
87
|
+
[]
|
88
|
+
end
|
89
|
+
|
90
|
+
def allowed_algorithms
|
91
|
+
@allowed_algorithms ||= resolve_allowed_algorithms
|
92
|
+
end
|
93
|
+
|
94
|
+
def resolve_allowed_algorithms
|
95
|
+
algs = given_algorithms.map { |alg| JWA.create(alg) }
|
96
|
+
|
97
|
+
sort_by_alg_header(algs)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Move algorithms matching the JWT alg header to the beginning of the list
|
101
|
+
def sort_by_alg_header(algs)
|
102
|
+
return algs if algs.size <= 1
|
103
|
+
|
104
|
+
algs.partition { |alg| alg.valid_alg?(alg_in_header) }.flatten
|
55
105
|
end
|
56
106
|
|
57
107
|
def find_key(&keyfinder)
|
58
108
|
key = (keyfinder.arity == 2 ? yield(header, payload) : yield(header))
|
59
|
-
|
60
|
-
key
|
109
|
+
# key can be of type [string, nil, OpenSSL::PKey, Array]
|
110
|
+
return key if key && !Array(key).empty?
|
111
|
+
|
112
|
+
raise JWT::DecodeError, 'No verification key available'
|
61
113
|
end
|
62
114
|
|
63
115
|
def verify_claims
|
64
116
|
Verify.verify_claims(payload, @options)
|
117
|
+
Verify.verify_required_claims(payload, @options)
|
65
118
|
end
|
66
119
|
|
67
120
|
def validate_segment_count!
|
68
121
|
return if segment_length == 3
|
69
122
|
return if !@verify && segment_length == 2 # If no verifying required, the signature is not needed
|
123
|
+
return if segment_length == 2 && none_algorithm?
|
70
124
|
|
71
125
|
raise(JWT::DecodeError, 'Not enough or too many segments')
|
72
126
|
end
|
@@ -75,8 +129,16 @@ module JWT
|
|
75
129
|
@segments.count
|
76
130
|
end
|
77
131
|
|
78
|
-
def
|
79
|
-
|
132
|
+
def none_algorithm?
|
133
|
+
alg_in_header == 'none'
|
134
|
+
end
|
135
|
+
|
136
|
+
def decode_signature
|
137
|
+
@signature = ::JWT::Base64.url_decode(@segments[2] || '')
|
138
|
+
end
|
139
|
+
|
140
|
+
def alg_in_header
|
141
|
+
header['alg']
|
80
142
|
end
|
81
143
|
|
82
144
|
def header
|
@@ -92,7 +154,7 @@ module JWT
|
|
92
154
|
end
|
93
155
|
|
94
156
|
def parse_and_decode(segment)
|
95
|
-
JWT::JSON.parse(JWT::Base64.url_decode(segment))
|
157
|
+
JWT::JSON.parse(::JWT::Base64.url_decode(segment))
|
96
158
|
rescue ::JSON::ParserError
|
97
159
|
raise JWT::DecodeError, 'Invalid segment encoding'
|
98
160
|
end
|
@@ -0,0 +1,29 @@
|
|
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 warning(message)
|
8
|
+
case JWT.configuration.deprecation_warnings
|
9
|
+
when :warn
|
10
|
+
warn("[DEPRECATION WARNING] #{message}")
|
11
|
+
when :once
|
12
|
+
return if record_warned(message)
|
13
|
+
|
14
|
+
warn("[DEPRECATION WARNING] #{message}")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def record_warned(message)
|
21
|
+
@warned ||= []
|
22
|
+
return true if @warned.include?(message)
|
23
|
+
|
24
|
+
@warned << message
|
25
|
+
false
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/jwt/encode.rb
CHANGED
@@ -1,23 +1,25 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative '
|
3
|
+
require_relative 'jwa'
|
4
|
+
require_relative 'claims_validator'
|
4
5
|
|
5
6
|
# JWT::Encode module
|
6
7
|
module JWT
|
7
8
|
# Encoding logic for JWT
|
8
9
|
class Encode
|
9
|
-
|
10
|
-
ALG_KEY = 'alg'.freeze
|
10
|
+
ALG_KEY = 'alg'
|
11
11
|
|
12
12
|
def initialize(options)
|
13
|
-
@payload
|
14
|
-
@key
|
15
|
-
@algorithm
|
16
|
-
@headers
|
13
|
+
@payload = options[:payload]
|
14
|
+
@key = options[:key]
|
15
|
+
@algorithm = JWA.create(options[:algorithm])
|
16
|
+
@headers = options[:headers].transform_keys(&:to_s)
|
17
|
+
@headers[ALG_KEY] = @algorithm.alg
|
17
18
|
end
|
18
19
|
|
19
20
|
def segments
|
20
|
-
|
21
|
+
validate_claims!
|
22
|
+
combine(encoded_header_and_payload, encoded_signature)
|
21
23
|
end
|
22
24
|
|
23
25
|
private
|
@@ -39,26 +41,29 @@ module JWT
|
|
39
41
|
end
|
40
42
|
|
41
43
|
def encode_header
|
42
|
-
@headers
|
43
|
-
encode(@headers)
|
44
|
+
encode_data(@headers)
|
44
45
|
end
|
45
46
|
|
46
47
|
def encode_payload
|
47
|
-
|
48
|
-
|
49
|
-
end
|
48
|
+
encode_data(@payload)
|
49
|
+
end
|
50
50
|
|
51
|
-
|
51
|
+
def signature
|
52
|
+
@algorithm.sign(data: encoded_header_and_payload, signing_key: @key)
|
52
53
|
end
|
53
54
|
|
54
|
-
def
|
55
|
-
return
|
55
|
+
def validate_claims!
|
56
|
+
return unless @payload.is_a?(Hash)
|
56
57
|
|
57
|
-
|
58
|
+
ClaimsValidator.new(@payload).validate!
|
59
|
+
end
|
60
|
+
|
61
|
+
def encode_signature
|
62
|
+
::JWT::Base64.url_encode(signature)
|
58
63
|
end
|
59
64
|
|
60
|
-
def
|
61
|
-
JWT::Base64.url_encode(JWT::JSON.generate(data))
|
65
|
+
def encode_data(data)
|
66
|
+
::JWT::Base64.url_encode(JWT::JSON.generate(data))
|
62
67
|
end
|
63
68
|
|
64
69
|
def combine(*parts)
|
data/lib/jwt/error.rb
CHANGED
@@ -1,20 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module JWT
|
4
|
-
EncodeError
|
5
|
-
DecodeError
|
6
|
-
RequiredDependencyError
|
4
|
+
class EncodeError < StandardError; end
|
5
|
+
class DecodeError < StandardError; end
|
6
|
+
class RequiredDependencyError < StandardError; end
|
7
7
|
|
8
|
-
VerificationError
|
9
|
-
ExpiredSignature
|
10
|
-
IncorrectAlgorithm
|
11
|
-
ImmatureSignature
|
12
|
-
InvalidIssuerError
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
8
|
+
class VerificationError < DecodeError; end
|
9
|
+
class ExpiredSignature < DecodeError; end
|
10
|
+
class IncorrectAlgorithm < DecodeError; end
|
11
|
+
class ImmatureSignature < DecodeError; end
|
12
|
+
class InvalidIssuerError < DecodeError; end
|
13
|
+
class UnsupportedEcdsaCurve < IncorrectAlgorithm; end
|
14
|
+
class InvalidIatError < DecodeError; end
|
15
|
+
class InvalidAudError < DecodeError; end
|
16
|
+
class InvalidSubError < DecodeError; end
|
17
|
+
class InvalidJtiError < DecodeError; end
|
18
|
+
class InvalidPayload < DecodeError; end
|
19
|
+
class MissingRequiredClaim < DecodeError; end
|
20
|
+
class Base64DecodeError < DecodeError; end
|
18
21
|
|
19
|
-
JWKError
|
22
|
+
class JWKError < DecodeError; end
|
20
23
|
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module JWA
|
5
|
+
module Ecdsa
|
6
|
+
module_function
|
7
|
+
|
8
|
+
NAMED_CURVES = {
|
9
|
+
'prime256v1' => {
|
10
|
+
algorithm: 'ES256',
|
11
|
+
digest: 'sha256'
|
12
|
+
},
|
13
|
+
'secp256r1' => { # alias for prime256v1
|
14
|
+
algorithm: 'ES256',
|
15
|
+
digest: 'sha256'
|
16
|
+
},
|
17
|
+
'secp384r1' => {
|
18
|
+
algorithm: 'ES384',
|
19
|
+
digest: 'sha384'
|
20
|
+
},
|
21
|
+
'secp521r1' => {
|
22
|
+
algorithm: 'ES512',
|
23
|
+
digest: 'sha512'
|
24
|
+
},
|
25
|
+
'secp256k1' => {
|
26
|
+
algorithm: 'ES256K',
|
27
|
+
digest: 'sha256'
|
28
|
+
}
|
29
|
+
}.freeze
|
30
|
+
|
31
|
+
SUPPORTED = NAMED_CURVES.map { |_, c| c[:algorithm] }.uniq.freeze
|
32
|
+
|
33
|
+
def sign(algorithm, msg, key)
|
34
|
+
curve_definition = curve_by_name(key.group.curve_name)
|
35
|
+
key_algorithm = curve_definition[:algorithm]
|
36
|
+
if algorithm != key_algorithm
|
37
|
+
raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} signing key was provided"
|
38
|
+
end
|
39
|
+
|
40
|
+
digest = OpenSSL::Digest.new(curve_definition[:digest])
|
41
|
+
asn1_to_raw(key.dsa_sign_asn1(digest.digest(msg)), key)
|
42
|
+
end
|
43
|
+
|
44
|
+
def verify(algorithm, public_key, signing_input, signature)
|
45
|
+
curve_definition = curve_by_name(public_key.group.curve_name)
|
46
|
+
key_algorithm = curve_definition[:algorithm]
|
47
|
+
if algorithm != key_algorithm
|
48
|
+
raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} verification key was provided"
|
49
|
+
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
|
+
end
|
56
|
+
|
57
|
+
def curve_by_name(name)
|
58
|
+
NAMED_CURVES.fetch(name) do
|
59
|
+
raise UnsupportedEcdsaCurve, "The ECDSA curve '#{name}' is not supported"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def raw_to_asn1(signature, private_key)
|
64
|
+
byte_size = (private_key.group.degree + 7) / 8
|
65
|
+
sig_bytes = signature[0..(byte_size - 1)]
|
66
|
+
sig_char = signature[byte_size..-1] || ''
|
67
|
+
OpenSSL::ASN1::Sequence.new([sig_bytes, sig_char].map { |int| OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(int, 2)) }).to_der
|
68
|
+
end
|
69
|
+
|
70
|
+
def asn1_to_raw(signature, public_key)
|
71
|
+
byte_size = (public_key.group.degree + 7) / 8
|
72
|
+
OpenSSL::ASN1.decode(signature).value.map { |value| value.value.to_s(2).rjust(byte_size, "\x00") }.join
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module JWA
|
5
|
+
module Eddsa
|
6
|
+
SUPPORTED = %w[ED25519 EdDSA].freeze
|
7
|
+
SUPPORTED_DOWNCASED = SUPPORTED.map(&:downcase).freeze
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def sign(algorithm, msg, key)
|
11
|
+
unless key.is_a?(RbNaCl::Signatures::Ed25519::SigningKey)
|
12
|
+
raise EncodeError, "Key given is a #{key.class} but has to be an RbNaCl::Signatures::Ed25519::SigningKey"
|
13
|
+
end
|
14
|
+
|
15
|
+
validate_algorithm!(algorithm)
|
16
|
+
|
17
|
+
key.sign(msg)
|
18
|
+
end
|
19
|
+
|
20
|
+
def verify(algorithm, public_key, signing_input, signature)
|
21
|
+
unless public_key.is_a?(RbNaCl::Signatures::Ed25519::VerifyKey)
|
22
|
+
raise DecodeError, "key given is a #{public_key.class} but has to be a RbNaCl::Signatures::Ed25519::VerifyKey"
|
23
|
+
end
|
24
|
+
|
25
|
+
validate_algorithm!(algorithm)
|
26
|
+
|
27
|
+
public_key.verify(signature, signing_input)
|
28
|
+
rescue RbNaCl::CryptoError
|
29
|
+
false
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def validate_algorithm!(algorithm)
|
35
|
+
return if SUPPORTED_DOWNCASED.include?(algorithm.downcase)
|
36
|
+
|
37
|
+
raise IncorrectAlgorithm, "Algorithm #{algorithm} not supported. Supported algoritms are #{SUPPORTED.join(', ')}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/jwt/jwa/hmac.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JWT
|
4
|
+
module JWA
|
5
|
+
module Hmac
|
6
|
+
module_function
|
7
|
+
|
8
|
+
MAPPING = {
|
9
|
+
'HS256' => OpenSSL::Digest::SHA256,
|
10
|
+
'HS384' => OpenSSL::Digest::SHA384,
|
11
|
+
'HS512' => OpenSSL::Digest::SHA512
|
12
|
+
}.freeze
|
13
|
+
|
14
|
+
SUPPORTED = MAPPING.keys
|
15
|
+
|
16
|
+
def sign(algorithm, msg, key)
|
17
|
+
key ||= ''
|
18
|
+
|
19
|
+
raise JWT::DecodeError, 'HMAC key expected to be a String' unless key.is_a?(String)
|
20
|
+
|
21
|
+
OpenSSL::HMAC.digest(MAPPING[algorithm].new, key, msg)
|
22
|
+
rescue OpenSSL::HMACError => e
|
23
|
+
if key == '' && e.message == 'EVP_PKEY_new_mac_key: malloc failure'
|
24
|
+
raise JWT::DecodeError, 'OpenSSL 3.0 does not support nil or empty hmac_secret'
|
25
|
+
end
|
26
|
+
|
27
|
+
raise e
|
28
|
+
end
|
29
|
+
|
30
|
+
def verify(algorithm, key, signing_input, signature)
|
31
|
+
SecurityUtils.secure_compare(signature, sign(algorithm, signing_input, key))
|
32
|
+
end
|
33
|
+
|
34
|
+
# Copy of https://github.com/rails/rails/blob/v7.0.3.1/activesupport/lib/active_support/security_utils.rb
|
35
|
+
# rubocop:disable Naming/MethodParameterName, Style/StringLiterals, Style/NumericPredicate
|
36
|
+
module SecurityUtils
|
37
|
+
# Constant time string comparison, for fixed length strings.
|
38
|
+
#
|
39
|
+
# The values compared should be of fixed length, such as strings
|
40
|
+
# that have already been processed by HMAC. Raises in case of length mismatch.
|
41
|
+
|
42
|
+
if defined?(OpenSSL.fixed_length_secure_compare)
|
43
|
+
def fixed_length_secure_compare(a, b)
|
44
|
+
OpenSSL.fixed_length_secure_compare(a, b)
|
45
|
+
end
|
46
|
+
else
|
47
|
+
# :nocov:
|
48
|
+
def fixed_length_secure_compare(a, b)
|
49
|
+
raise ArgumentError, "string length mismatch." unless a.bytesize == b.bytesize
|
50
|
+
|
51
|
+
l = a.unpack "C#{a.bytesize}"
|
52
|
+
|
53
|
+
res = 0
|
54
|
+
b.each_byte { |byte| res |= byte ^ l.shift }
|
55
|
+
res == 0
|
56
|
+
end
|
57
|
+
# :nocov:
|
58
|
+
end
|
59
|
+
module_function :fixed_length_secure_compare
|
60
|
+
|
61
|
+
# Secure string comparison for strings of variable length.
|
62
|
+
#
|
63
|
+
# While a timing attack would not be able to discern the content of
|
64
|
+
# a secret compared via secure_compare, it is possible to determine
|
65
|
+
# the secret length. This should be considered when using secure_compare
|
66
|
+
# to compare weak, short secrets to user input.
|
67
|
+
def secure_compare(a, b)
|
68
|
+
a.bytesize == b.bytesize && fixed_length_secure_compare(a, b)
|
69
|
+
end
|
70
|
+
module_function :secure_compare
|
71
|
+
end
|
72
|
+
# rubocop:enable Naming/MethodParameterName, Style/StringLiterals, Style/NumericPredicate
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|