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