jwt 2.2.1 → 2.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/AUTHORS +79 -44
  3. data/CHANGELOG.md +305 -20
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/CONTRIBUTING.md +99 -0
  6. data/README.md +268 -40
  7. data/lib/jwt/base64.rb +16 -2
  8. data/lib/jwt/claims_validator.rb +13 -9
  9. data/lib/jwt/configuration/container.rb +32 -0
  10. data/lib/jwt/configuration/decode_configuration.rb +46 -0
  11. data/lib/jwt/configuration/jwk_configuration.rb +27 -0
  12. data/lib/jwt/configuration.rb +15 -0
  13. data/lib/jwt/decode.rb +80 -18
  14. data/lib/jwt/deprecations.rb +29 -0
  15. data/lib/jwt/encode.rb +24 -19
  16. data/lib/jwt/error.rb +17 -14
  17. data/lib/jwt/jwa/ecdsa.rb +76 -0
  18. data/lib/jwt/jwa/eddsa.rb +42 -0
  19. data/lib/jwt/jwa/hmac.rb +75 -0
  20. data/lib/jwt/jwa/hmac_rbnacl.rb +50 -0
  21. data/lib/jwt/jwa/hmac_rbnacl_fixed.rb +46 -0
  22. data/lib/jwt/jwa/none.rb +19 -0
  23. data/lib/jwt/jwa/ps.rb +30 -0
  24. data/lib/jwt/jwa/rsa.rb +25 -0
  25. data/lib/jwt/{algos → jwa}/unsupported.rb +8 -5
  26. data/lib/jwt/jwa/wrapper.rb +26 -0
  27. data/lib/jwt/jwa.rb +62 -0
  28. data/lib/jwt/jwk/ec.rb +251 -0
  29. data/lib/jwt/jwk/hmac.rb +103 -0
  30. data/lib/jwt/jwk/key_base.rb +57 -0
  31. data/lib/jwt/jwk/key_finder.rb +19 -30
  32. data/lib/jwt/jwk/kid_as_key_digest.rb +15 -0
  33. data/lib/jwt/jwk/okp_rbnacl.rb +110 -0
  34. data/lib/jwt/jwk/rsa.rb +181 -25
  35. data/lib/jwt/jwk/set.rb +80 -0
  36. data/lib/jwt/jwk/thumbprint.rb +26 -0
  37. data/lib/jwt/jwk.rb +39 -15
  38. data/lib/jwt/verify.rb +25 -6
  39. data/lib/jwt/version.rb +24 -3
  40. data/lib/jwt/x5c_key_finder.rb +52 -0
  41. data/lib/jwt.rb +6 -4
  42. data/ruby-jwt.gemspec +18 -10
  43. metadata +45 -76
  44. data/.codeclimate.yml +0 -20
  45. data/.ebert.yml +0 -18
  46. data/.gitignore +0 -11
  47. data/.rspec +0 -1
  48. data/.rubocop.yml +0 -98
  49. data/.travis.yml +0 -20
  50. data/Appraisals +0 -14
  51. data/Gemfile +0 -3
  52. data/Rakefile +0 -11
  53. data/lib/jwt/algos/ecdsa.rb +0 -35
  54. data/lib/jwt/algos/eddsa.rb +0 -23
  55. data/lib/jwt/algos/hmac.rb +0 -33
  56. data/lib/jwt/algos/ps.rb +0 -43
  57. data/lib/jwt/algos/rsa.rb +0 -19
  58. data/lib/jwt/default_options.rb +0 -15
  59. data/lib/jwt/security_utils.rb +0 -57
  60. data/lib/jwt/signature.rb +0 -52
@@ -1,33 +1,37 @@
1
- require_relative './error'
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'error'
2
4
 
3
5
  module JWT
4
6
  class ClaimsValidator
5
- INTEGER_CLAIMS = %i[
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.each_with_object({}) { |(k, v), h| h[k.to_sym] = v }
14
+ @payload = payload.transform_keys(&:to_sym)
13
15
  end
14
16
 
15
17
  def validate!
16
- validate_int_claims
18
+ validate_numeric_claims
17
19
 
18
20
  true
19
21
  end
20
22
 
21
23
  private
22
24
 
23
- def validate_int_claims
24
- INTEGER_CLAIMS.each do |claim|
25
- validate_is_int(claim) if @payload.key?(claim)
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 validate_is_int(claim)
30
- raise InvalidPayload, "#{claim} claim must be an Integer but it is a #{@payload[claim].class}" unless @payload[claim].is_a?(Integer)
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
- decode_crypto
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 = find_key(&@keyfinder) if @keyfinder
37
- @key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks]).key_for(header['kid']) if @options[:jwks]
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, 'Expected a different algorithm') unless options_includes_algo_in_header?
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
- Signature.verify(header['alg'], @key, signing_input, @signature)
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 options_includes_algo_in_header?
46
- allowed_algorithms.include? header['alg']
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
- def allowed_algorithms
50
- if @options.key?(:algorithm)
51
- [@options[:algorithm]]
52
- else
53
- @options[:algorithms] || []
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
- raise JWT::DecodeError, 'No verification key available' unless key
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 decode_crypto
79
- @signature = JWT::Base64.url_decode(@segments[2])
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 './claims_validator'
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
- ALG_NONE = 'none'.freeze
10
- ALG_KEY = 'alg'.freeze
10
+ ALG_KEY = 'alg'
11
11
 
12
12
  def initialize(options)
13
- @payload = options[:payload]
14
- @key = options[:key]
15
- @algorithm = options[:algorithm]
16
- @headers = options[:headers].each_with_object({}) { |(key, value), headers| headers[key.to_s] = value }
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
- @segments ||= combine(encoded_header_and_payload, encoded_signature)
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[ALG_KEY] = @algorithm
43
- encode(@headers)
44
+ encode_data(@headers)
44
45
  end
45
46
 
46
47
  def encode_payload
47
- if @payload && @payload.is_a?(Hash)
48
- ClaimsValidator.new(@payload).validate!
49
- end
48
+ encode_data(@payload)
49
+ end
50
50
 
51
- encode(@payload)
51
+ def signature
52
+ @algorithm.sign(data: encoded_header_and_payload, signing_key: @key)
52
53
  end
53
54
 
54
- def encode_signature
55
- return '' if @algorithm == ALG_NONE
55
+ def validate_claims!
56
+ return unless @payload.is_a?(Hash)
56
57
 
57
- JWT::Base64.url_encode(JWT::Signature.sign(@algorithm, encoded_header_and_payload, @key))
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 encode(data)
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 = Class.new(StandardError)
5
- DecodeError = Class.new(StandardError)
6
- RequiredDependencyError = Class.new(StandardError)
4
+ class EncodeError < StandardError; end
5
+ class DecodeError < StandardError; end
6
+ class RequiredDependencyError < StandardError; end
7
7
 
8
- VerificationError = Class.new(DecodeError)
9
- ExpiredSignature = Class.new(DecodeError)
10
- IncorrectAlgorithm = Class.new(DecodeError)
11
- ImmatureSignature = Class.new(DecodeError)
12
- InvalidIssuerError = Class.new(DecodeError)
13
- InvalidIatError = Class.new(DecodeError)
14
- InvalidAudError = Class.new(DecodeError)
15
- InvalidSubError = Class.new(DecodeError)
16
- InvalidJtiError = Class.new(DecodeError)
17
- InvalidPayload = Class.new(DecodeError)
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 = Class.new(DecodeError)
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
@@ -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