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

Support algorithm to be given as a class #512

Merged
merged 6 commits into from
Oct 9, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Metrics/AbcSize:
Max: 25

Metrics/ClassLength:
Max: 112
Max: 121

Metrics/ModuleLength:
Max: 100
Expand Down
27 changes: 22 additions & 5 deletions lib/jwt/algos.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
# frozen_string_literal: true

begin
require 'rbnacl'
rescue LoadError
raise if defined?(RbNaCl)
end
require 'openssl'

require 'jwt/security_utils'
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'
require 'jwt/algos/algo_wrapper'

# JWT::Signature module
module JWT
# Signature logic for JWT
module Algos
extend self

Expand All @@ -28,14 +35,24 @@ def find(algorithm)
indexed[algorithm && algorithm.downcase]
end

def create(algorithm)
cls, alg = find(algorithm)
Algos::AlgoWrapper.new(alg, cls)
end

def implementation?(algorithm)
algorithm.respond_to?(:sign) &&
algorithm.respond_to?(:verify)
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]
ALGOS.each_with_object(Hash.new(fallback)) do |cls, hash|
cls.const_get(:SUPPORTED).each do |alg|
hash[alg.downcase] = [cls, alg]
end
end
end
Expand Down
30 changes: 30 additions & 0 deletions lib/jwt/algos/algo_wrapper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true

module JWT
module Algos
class AlgoWrapper
attr_reader :alg

def initialize(alg, cls)
@alg = alg
@cls = cls
end

def valid_alg?(alg)
alg && @alg.casecmp(alg).zero?
end

def sign(data:, signing_key:)
@cls.sign(@alg, data, signing_key)
end

def verify(data:, signature:, verification_key:)
@cls.verify(alg, verification_key, data, signature)
rescue OpenSSL::PKey::PKeyError # These should be moved to the algorithms that actually need this, but left here to ensure nothing will break.
raise JWT::VerificationError, 'Signature verification raised'
ensure
OpenSSL.errors.clear
end
end
end
end
6 changes: 2 additions & 4 deletions lib/jwt/algos/ecdsa.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ module Ecdsa

SUPPORTED = NAMED_CURVES.map { |_, c| c[:algorithm] }.uniq.freeze

def sign(to_sign)
algorithm, msg, key = to_sign.values
def sign(algorithm, msg, key)
curve_definition = curve_by_name(key.group.curve_name)
key_algorithm = curve_definition[:algorithm]
if algorithm != key_algorithm
Expand All @@ -42,8 +41,7 @@ def sign(to_sign)
SecurityUtils.asn1_to_raw(key.dsa_sign_asn1(digest.digest(msg)), key)
end

def verify(to_verify)
algorithm, public_key, signing_input, signature = to_verify.values
def verify(algorithm, public_key, signing_input, signature)
curve_definition = curve_by_name(public_key.group.curve_name)
key_algorithm = curve_definition[:algorithm]
if algorithm != key_algorithm
Expand Down
6 changes: 2 additions & 4 deletions lib/jwt/algos/eddsa.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ module Eddsa

SUPPORTED = %w[ED25519 EdDSA].freeze

def sign(to_sign)
algorithm, msg, key = to_sign.values
def sign(algorithm, msg, key)
if key.class != RbNaCl::Signatures::Ed25519::SigningKey
raise EncodeError, "Key given is a #{key.class} but has to be an RbNaCl::Signatures::Ed25519::SigningKey"
end
Expand All @@ -19,8 +18,7 @@ def sign(to_sign)
key.sign(msg)
end

def verify(to_verify)
algorithm, public_key, signing_input, signature = to_verify.values
def verify(algorithm, public_key, signing_input, signature)
unless SUPPORTED.map(&:downcase).map(&:to_sym).include?(algorithm.downcase.to_sym)
raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key.primitive} signing key was provided"
end
Expand Down
8 changes: 3 additions & 5 deletions lib/jwt/algos/hmac.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ module Hmac

SUPPORTED = %w[HS256 HS512256 HS384 HS512].freeze

def sign(to_sign)
algorithm, msg, key = to_sign.values
def sign(algorithm, msg, key)
key ||= ''
authenticator, padded_key = SecurityUtils.rbnacl_fixup(algorithm, key)
if authenticator && padded_key
Expand All @@ -18,8 +17,7 @@ def sign(to_sign)
end
end

def verify(to_verify)
algorithm, public_key, signing_input, signature = to_verify.values
def verify(algorithm, public_key, signing_input, signature)
authenticator, padded_key = SecurityUtils.rbnacl_fixup(algorithm, public_key)
if authenticator && padded_key
begin
Expand All @@ -28,7 +26,7 @@ def verify(to_verify)
false
end
else
SecurityUtils.secure_compare(signature, sign(JWT::Signature::ToSign.new(algorithm, signing_input, public_key)))
SecurityUtils.secure_compare(signature, sign(algorithm, signing_input, public_key))
end
end
end
Expand Down
4 changes: 3 additions & 1 deletion lib/jwt/algos/none.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ module None

SUPPORTED = %w[none].freeze

def sign(*); end
def sign(*)
''
end

def verify(*)
true
Expand Down
8 changes: 3 additions & 5 deletions lib/jwt/algos/ps.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,9 @@ module Ps

SUPPORTED = %w[PS256 PS384 PS512].freeze

def sign(to_sign)
def sign(algorithm, msg, key)
require_openssl!

algorithm, msg, key = to_sign.values

key_class = key.class

raise EncodeError, "The given key is a #{key_class}. It has to be an OpenSSL::PKey::RSA instance." if key_class == String
Expand All @@ -23,10 +21,10 @@ def sign(to_sign)
key.sign_pss(translated_algorithm, msg, salt_length: :digest, mgf1_hash: translated_algorithm)
end

def verify(to_verify)
def verify(algorithm, public_key, signing_input, signature)
require_openssl!

SecurityUtils.verify_ps(to_verify.algorithm, to_verify.public_key, to_verify.signing_input, to_verify.signature)
SecurityUtils.verify_ps(algorithm, public_key, signing_input, signature)
end

def require_openssl!
Expand Down
7 changes: 3 additions & 4 deletions lib/jwt/algos/rsa.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@ module Rsa

SUPPORTED = %w[RS256 RS384 RS512].freeze

def sign(to_sign)
algorithm, msg, key = to_sign.values
def sign(algorithm, msg, key)
raise EncodeError, "The given key is a #{key.class}. It has to be an OpenSSL::PKey::RSA instance." if key.instance_of?(String)

key.sign(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), msg)
end

def verify(to_verify)
SecurityUtils.verify_rsa(to_verify.algorithm, to_verify.public_key, to_verify.signing_input, to_verify.signature)
def verify(algorithm, public_key, signing_input, signature)
SecurityUtils.verify_rsa(algorithm, public_key, signing_input, signature)
end
end
end
Expand Down
67 changes: 45 additions & 22 deletions lib/jwt/decode.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

require 'json'

require 'jwt/signature'
require 'jwt/verify'
require 'jwt/x5c_key_finder'

# JWT::Decode module
module JWT
# Decoding logic for JWT
Expand All @@ -24,7 +24,7 @@ def initialize(jwt, key, verify, options, &keyfinder)
def decode_segments
validate_segment_count!
if @verify
decode_crypto
decode_signature
verify_algo
set_key
verify_signature
Expand All @@ -51,8 +51,8 @@ def verify_signature

def verify_algo
raise(JWT::IncorrectAlgorithm, 'An algorithm must be specified') if allowed_algorithms.empty?
raise(JWT::IncorrectAlgorithm, 'Token is missing alg header') unless algorithm
raise(JWT::IncorrectAlgorithm, 'Expected a different algorithm') unless options_includes_algo_in_header?
raise(JWT::IncorrectAlgorithm, 'Token is missing alg header') unless alg_in_header
raise(JWT::IncorrectAlgorithm, 'Expected a different algorithm') unless valid_alg_in_header?
end

def set_key
Expand All @@ -64,27 +64,50 @@ def set_key
end

def verify_signature_for?(key)
Signature.verify(algorithm, key, signing_input, @signature)
allowed_algorithms.any? do |alg|
alg.verify(data: signing_input, signature: @signature, verification_key: key)
end
end

def valid_alg_in_header?
allowed_algorithms.any? { |alg| alg.valid_alg?(alg_in_header) }
end

def options_includes_algo_in_header?
allowed_algorithms.any? { |alg| alg.casecmp(algorithm).zero? }
# Order is very important - first check for string keys, next for symbols
ALGORITHM_KEYS = ['algorithm',
:algorithm,
'algorithms',
:algorithms].freeze

def given_algorithms
ALGORITHM_KEYS.each do |alg_key|
alg = @options[alg_key]
return Array(alg) if alg
end
[]
end

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

def resolve_allowed_algorithms
algs = given_algorithms.map do |alg|
if Algos.implementation?(alg)
alg
else
Algos.create(alg)
end
end
Array(algos)

sort_by_alg_header(algs)
end

# Move algorithms matching the JWT alg header to the beginning of the list
def sort_by_alg_header(algs)
return algs if algs.size <= 1

algs.partition { |alg| alg.valid_alg?(alg_in_header) }.flatten
end

def find_key(&keyfinder)
Expand Down Expand Up @@ -113,14 +136,14 @@ def segment_length
end

def none_algorithm?
algorithm == 'none'
alg_in_header == 'none'
end

def decode_crypto
def decode_signature
@signature = ::JWT::Base64.url_decode(@segments[2] || '')
end

def algorithm
def alg_in_header
header['alg']
end

Expand Down
Loading