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
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
algos = ALGOS.dup
fallback = [algos.pop, nil]
johnnyshields marked this conversation as resolved.
Show resolved Hide resolved
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
2 changes: 1 addition & 1 deletion lib/jwt/algos/eddsa.rb
Expand Up @@ -3,7 +3,7 @@ module Algos
module Eddsa
module_function

SUPPORTED = %w[ED25519].freeze
SUPPORTED = %w[Ed25519].freeze
johnnyshields marked this conversation as resolved.
Show resolved Hide resolved

def sign(to_sign)
algorithm, msg, key = to_sign.values
Expand Down
14 changes: 14 additions & 0 deletions lib/jwt/algos/none.rb
@@ -0,0 +1,14 @@
module JWT
module Algos
module None
module_function

SUPPORTED = ['none']
johnnyshields marked this conversation as resolved.
Show resolved Hide resolved
def verify(*)
johnnyshields marked this conversation as resolved.
Show resolved Hide resolved
end

def sign(*)
johnnyshields marked this conversation as resolved.
Show resolved Hide resolved
end
end
end
end
2 changes: 1 addition & 1 deletion lib/jwt/algos/unsupported.rb
Expand Up @@ -3,7 +3,7 @@ module Algos
module Unsupported
module_function

SUPPORTED = Object.new.tap { |object| object.define_singleton_method(:include?) { |*| true } }
SUPPORTED = []
johnnyshields marked this conversation as resolved.
Show resolved Hide resolved
def verify(*)
raise JWT::VerificationError, 'Algorithm not supported'
end
Expand Down
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']) }
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
3 changes: 2 additions & 1 deletion lib/jwt/encode.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true

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

# JWT::Encode module
Expand All @@ -12,7 +13,7 @@ class Encode
def initialize(options)
@payload = options[:payload]
@key = options[:key]
@algorithm = options[:algorithm]
_, @algorithm = Algos.find(options[:algorithm])
@headers = options[:headers].each_with_object({}) { |(key, value), headers| headers[key.to_s] = value }
end

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')

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
27 changes: 24 additions & 3 deletions spec/jwt_spec.rb
Expand Up @@ -37,8 +37,8 @@

if defined?(RbNaCl)
data.merge!(
'ED25519_private' => RbNaCl::Signatures::Ed25519::SigningKey.new('abcdefghijklmnopqrstuvwxyzABCDEF'),
'ED25519_public' => RbNaCl::Signatures::Ed25519::SigningKey.new('abcdefghijklmnopqrstuvwxyzABCDEF').verify_key,
'Ed25519_private' => RbNaCl::Signatures::Ed25519::SigningKey.new('abcdefghijklmnopqrstuvwxyzABCDEF'),
'Ed25519_public' => RbNaCl::Signatures::Ed25519::SigningKey.new('abcdefghijklmnopqrstuvwxyzABCDEF').verify_key,
)
end
data
Expand Down Expand Up @@ -190,7 +190,7 @@
end

if defined?(RbNaCl)
%w[ED25519].each do |alg|
%w[Ed25519].each do |alg|
context "alg: #{alg}" do
before(:each) do
data[alg] = JWT.encode payload, data["#{alg}_private"], alg
Expand Down 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], 'ps384')
johnnyshields marked this conversation as resolved.
Show resolved Hide resolved
expect(JWT.decode(enc, data[:rsa_public], true, algorithm: 'PS384')).to eq([payload, { 'alg' => 'PS384'}])

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