Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ignore casing of algorithm #405

Merged
merged 13 commits into from Feb 9, 2021
3 changes: 2 additions & 1 deletion .rubocop_todo.yml
Expand Up @@ -126,11 +126,12 @@ Style/MethodCallWithoutArgsParentheses:
Exclude:
- 'spec/jwt_spec.rb'

# Offense count: 1
# Offense count: 2
# Configuration parameters: EnforcedStyle.
# SupportedStyles: module_function, extend_self
Style/ModuleFunction:
Exclude:
- 'lib/jwt/algos.rb'
- 'lib/jwt/signature.rb'

# Offense count: 1
Expand Down
44 changes: 44 additions & 0 deletions lib/jwt/algos.rb
@@ -0,0 +1,44 @@
# frozen_string_literal: true

require 'jwt/algos/hmac'
require 'jwt/algos/eddsa'
require 'jwt/algos/ecdsa'
require 'jwt/algos/rsa'
require 'jwt/algos/ps'
require 'jwt/algos/none'
require 'jwt/algos/unsupported'

# JWT::Signature module
module JWT
# Signature logic for JWT
module Algos
extend self
johnnyshields marked this conversation as resolved.
Show resolved Hide resolved

ALGOS = [
Algos::Hmac,
Algos::Ecdsa,
Algos::Rsa,
Algos::Eddsa,
Algos::Ps,
Algos::None,
Algos::Unsupported
].freeze

def find(algorithm)
indexed[algorithm && algorithm.downcase]
end

private

def indexed
@indexed ||= begin
fallback = [Algos::Unsupported, nil]
ALGOS.each_with_object(Hash.new(fallback)) do |alg, hash|
alg.const_get(:SUPPORTED).each do |code|
hash[code.downcase] = [alg, code]
end
end
end
end
end
end
15 changes: 15 additions & 0 deletions lib/jwt/algos/none.rb
@@ -0,0 +1,15 @@
module JWT
module Algos
module None
module_function

SUPPORTED = %w[none].freeze

def sign(*); end

def verify(*)
true
end
end
end
end
9 changes: 5 additions & 4 deletions lib/jwt/algos/unsupported.rb
Expand Up @@ -3,14 +3,15 @@ module Algos
module Unsupported
module_function

SUPPORTED = Object.new.tap { |object| object.define_singleton_method(:include?) { |*| true } }
def verify(*)
raise JWT::VerificationError, 'Algorithm not supported'
end
SUPPORTED = [].freeze

def sign(*)
raise NotImplementedError, 'Unsupported signing method'
end

def verify(*)
raise JWT::VerificationError, 'Algorithm not supported'
end
end
end
end
13 changes: 7 additions & 6 deletions lib/jwt/decode.rb
Expand Up @@ -43,22 +43,23 @@ def verify_signature
end

def options_includes_algo_in_header?
allowed_algorithms.include? header['alg']
allowed_algorithms.any? { |alg| alg.casecmp(header['alg']).zero? }
end

def allowed_algorithms
# Order is very important - first check for string keys, next for symbols
if @options.key?('algorithm')
[@options['algorithm']]
algos = if @options.key?('algorithm')
@options['algorithm']
elsif @options.key?(:algorithm)
[@options[:algorithm]]
@options[:algorithm]
elsif @options.key?('algorithms')
@options['algorithms'] || []
@options['algorithms']
elsif @options.key?(:algorithms)
@options[:algorithms] || []
@options[:algorithms]
else
[]
end
Array(algos)
end

def find_key(&keyfinder)
Expand Down
9 changes: 5 additions & 4 deletions lib/jwt/encode.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true

require_relative './algos'
require_relative './claims_validator'

# JWT::Encode module
Expand All @@ -10,10 +11,10 @@ class Encode
ALG_KEY = 'alg'.freeze

def initialize(options)
@payload = options[:payload]
@key = options[:key]
@algorithm = options[:algorithm]
@headers = options[:headers].each_with_object({}) { |(key, value), headers| headers[key.to_s] = value }
@payload = options[:payload]
@key = options[:key]
_, @algorithm = Algos.find(options[:algorithm])
@headers = options[:headers].each_with_object({}) { |(key, value), headers| headers[key.to_s] = value }
end

def segments
Expand Down
29 changes: 6 additions & 23 deletions lib/jwt/signature.rb
Expand Up @@ -2,12 +2,7 @@

require 'jwt/security_utils'
require 'openssl'
require 'jwt/algos/hmac'
require 'jwt/algos/eddsa'
require 'jwt/algos/ecdsa'
require 'jwt/algos/rsa'
require 'jwt/algos/ps'
require 'jwt/algos/unsupported'
require 'jwt/algos'
begin
require 'rbnacl'
rescue LoadError
Expand All @@ -19,33 +14,21 @@ module JWT
# Signature logic for JWT
module Signature
extend self
ALGOS = [
Algos::Hmac,
Algos::Ecdsa,
Algos::Rsa,
Algos::Eddsa,
Algos::Ps,
Algos::Unsupported
].freeze
ToSign = Struct.new(:algorithm, :msg, :key)
ToVerify = Struct.new(:algorithm, :public_key, :signing_input, :signature)

def sign(algorithm, msg, key)
algo = ALGOS.find do |alg|
alg.const_get(:SUPPORTED).include? algorithm
end
algo.sign ToSign.new(algorithm, msg, key)
algo, code = Algos.find(algorithm)
algo.sign ToSign.new(code, msg, key)
end

def verify(algorithm, key, signing_input, signature)
return true if algorithm == 'none'
return true if algorithm.casecmp('none').zero?

raise JWT::DecodeError, 'No verification key available' unless key

algo = ALGOS.find do |alg|
alg.const_get(:SUPPORTED).include? algorithm
end
verified = algo.verify(ToVerify.new(algorithm, key, signing_input, signature))
algo, code = Algos.find(algorithm)
verified = algo.verify(ToVerify.new(code, key, signing_input, signature))
raise(JWT::VerificationError, 'Signature verification raised') unless verified
rescue OpenSSL::PKey::PKeyError
raise JWT::VerificationError, 'Signature verification raised'
Expand Down
21 changes: 21 additions & 0 deletions spec/jwt_spec.rb
Expand Up @@ -510,4 +510,25 @@
end.not_to raise_error
end
end

context 'algorithm case insensitivity' do
let(:payload) { { 'a' => 1, 'b' => 'b' } }

it 'ignores algorithm casing during encode/decode' do
enc = JWT.encode(payload, '', 'hs256')
expect(JWT.decode(enc, '')).to eq([payload, { 'alg' => 'HS256'}])

enc = JWT.encode(payload, data[:rsa_private], 'rs512')
expect(JWT.decode(enc, data[:rsa_public], true, algorithm: 'RS512')).to eq([payload, { 'alg' => 'RS512'}])

enc = JWT.encode(payload, data[:rsa_private], 'RS512')
expect(JWT.decode(enc, data[:rsa_public], true, algorithm: 'rs512')).to eq([payload, { 'alg' => 'RS512'}])
end

it 'raises error for invalid algorithm' do
expect do
JWT.encode(payload, '', 'xyz')
end.to raise_error(NotImplementedError)
end
end
end