diff --git a/lib/jwt/algos/ecdsa.rb b/lib/jwt/algos/ecdsa.rb new file mode 100644 index 00000000..d7a5ea75 --- /dev/null +++ b/lib/jwt/algos/ecdsa.rb @@ -0,0 +1,35 @@ +module JWT + module Algos + module Ecdsa + module_function + + SUPPORTED = %(ES256 ES384 ES512).freeze + NAMED_CURVES = { + 'prime256v1' => 'ES256', + 'secp384r1' => 'ES384', + 'secp521r1' => 'ES512' + }.freeze + + def sign(to_sign) + algorithm, msg, key = to_sign.values + key_algorithm = NAMED_CURVES[key.group.curve_name] + if algorithm != key_algorithm + raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} signing key was provided" + end + + digest = OpenSSL::Digest.new(algorithm.sub('ES', 'sha')) + 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 + key_algorithm = NAMED_CURVES[public_key.group.curve_name] + if algorithm != key_algorithm + raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} verification key was provided" + end + digest = OpenSSL::Digest.new(algorithm.sub('ES', 'sha')) + public_key.dsa_verify_asn1(digest.digest(signing_input), SecurityUtils.raw_to_asn1(signature, public_key)) + end + end + end +end diff --git a/lib/jwt/algos/eddsa.rb b/lib/jwt/algos/eddsa.rb new file mode 100644 index 00000000..82091051 --- /dev/null +++ b/lib/jwt/algos/eddsa.rb @@ -0,0 +1,23 @@ +module JWT + module Algos + module Eddsa + module_function + + SUPPORTED = %w(ED25519).freeze + + def sign(to_sign) + algorithm, msg, key = to_sign.values + raise EncodeError, "Key given is a #{key.class} but has to be an RbNaCl::Signatures::Ed25519::SigningKey" if key.class != RbNaCl::Signatures::Ed25519::SigningKey + raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key.primitive} signing key was provided" if algorithm.downcase.to_sym != key.primitive + key.sign(msg) + end + + def verify(to_verify) + algorithm, public_key, signing_input, signature = to_verify.values + raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{public_key.primitive} verification key was provided" if algorithm.downcase.to_sym != public_key.primitive + raise DecodeError, "key given is a #{public_key.class} but has to be a RbNaCl::Signatures::Ed25519::VerifyKey" if public_key.class != RbNaCl::Signatures::Ed25519::VerifyKey + public_key.verify(signature, signing_input) + end + end + end +end diff --git a/lib/jwt/algos/hmac.rb b/lib/jwt/algos/hmac.rb new file mode 100644 index 00000000..fbbbe65e --- /dev/null +++ b/lib/jwt/algos/hmac.rb @@ -0,0 +1,33 @@ +module JWT + module Algos + module Hmac + module_function + + SUPPORTED = %w(HS256 HS512256 HS384 HS512).freeze + + def sign(to_sign) + algorithm, msg, key = to_sign.values + authenticator, padded_key = SecurityUtils.rbnacl_fixup(algorithm, key) + if authenticator && padded_key + authenticator.auth(padded_key, msg.encode('binary')) + else + OpenSSL::HMAC.digest(OpenSSL::Digest.new(algorithm.sub('HS', 'sha')), key, msg) + end + end + + def verify(to_verify) + algorithm, public_key, signing_input, signature = to_verify.values + authenticator, padded_key = SecurityUtils.rbnacl_fixup(algorithm, public_key) + if authenticator && padded_key + begin + authenticator.verify(padded_key, signature.encode('binary'), signing_input.encode('binary')) + rescue RbNaCl::BadAuthenticatorError + false + end + else + SecurityUtils.secure_compare(signature, sign(JWT::Signature::ToSign.new(algorithm, signing_input, public_key))) + end + end + end + end +end diff --git a/lib/jwt/algos/rsa.rb b/lib/jwt/algos/rsa.rb new file mode 100644 index 00000000..7656a3e6 --- /dev/null +++ b/lib/jwt/algos/rsa.rb @@ -0,0 +1,19 @@ +module JWT + module Algos + module Rsa + module_function + + SUPPORTED = %w(RS256 RS384 RS512).freeze + + def sign(to_sign) + algorithm, msg, key = to_sign.values + raise EncodeError, "The given key is a #{key.class}. It has to be an OpenSSL::PKey::RSA instance." if key.class == 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) + end + end + end +end diff --git a/lib/jwt/algos/unsupported.rb b/lib/jwt/algos/unsupported.rb new file mode 100644 index 00000000..99ddcb71 --- /dev/null +++ b/lib/jwt/algos/unsupported.rb @@ -0,0 +1,16 @@ +module JWT + 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 + + def sign(*) + raise NotImplementedError, 'Unsupported signing method' + end + end + end +end diff --git a/lib/jwt/signature.rb b/lib/jwt/signature.rb index 69f03599..b030c2fa 100644 --- a/lib/jwt/signature.rb +++ b/lib/jwt/signature.rb @@ -2,6 +2,11 @@ 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/unsupported' begin require 'rbnacl' rescue LoadError => e @@ -13,110 +18,33 @@ module JWT # Signature logic for JWT module Signature extend self - - HMAC_ALGORITHMS = %w[HS256 HS512256 HS384 HS512].freeze - RSA_ALGORITHMS = %w[RS256 RS384 RS512].freeze - ECDSA_ALGORITHMS = %w[ES256 ES384 ES512].freeze - EDDSA_ALGORITHMS = %w[ED25519].freeze - - NAMED_CURVES = { - 'prime256v1' => 'ES256', - 'secp384r1' => 'ES384', - 'secp521r1' => 'ES512' - }.freeze + ALGOS = [ + Algos::Hmac, + Algos::Ecdsa, + Algos::Rsa, + Algos::Eddsa, + Algos::Unsupported + ].freeze + ToSign = Struct.new(:algorithm, :msg, :key) + ToVerify = Struct.new(:algorithm, :public_key, :signing_input, :signature) def sign(algorithm, msg, key) - if HMAC_ALGORITHMS.include?(algorithm) - sign_hmac(algorithm, msg, key) - elsif RSA_ALGORITHMS.include?(algorithm) - sign_rsa(algorithm, msg, key) - elsif ECDSA_ALGORITHMS.include?(algorithm) - sign_ecdsa(algorithm, msg, key) - elsif EDDSA_ALGORITHMS.include?(algorithm) - sign_eddsa(algorithm, msg, key) - else - raise NotImplementedError, 'Unsupported signing method' + algo = ALGOS.find do |alg| + alg.const_get(:SUPPORTED).include? algorithm end + algo.sign ToSign.new(algorithm, msg, key) end - def verify(algo, key, signing_input, signature) - verified = if HMAC_ALGORITHMS.include?(algo) - verify_hmac(algo, key, signing_input, signature) - elsif RSA_ALGORITHMS.include?(algo) - SecurityUtils.verify_rsa(algo, key, signing_input, signature) - elsif ECDSA_ALGORITHMS.include?(algo) - verify_ecdsa(algo, key, signing_input, signature) - elsif EDDSA_ALGORITHMS.include?(algo) - verify_eddsa(algo, key, signing_input, signature) - else - raise JWT::VerificationError, 'Algorithm not supported' + def verify(algorithm, key, signing_input, signature) + algo = ALGOS.find do |alg| + alg.const_get(:SUPPORTED).include? algorithm end - + verified = algo.verify(ToVerify.new(algorithm, key, signing_input, signature)) raise(JWT::VerificationError, 'Signature verification raised') unless verified rescue OpenSSL::PKey::PKeyError raise JWT::VerificationError, 'Signature verification raised' ensure OpenSSL.errors.clear end - - private - - def sign_rsa(algorithm, msg, private_key) - raise EncodeError, "The given key is a #{private_key.class}. It has to be an OpenSSL::PKey::RSA instance." if private_key.class == String - private_key.sign(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), msg) - end - - def sign_ecdsa(algorithm, msg, private_key) - key_algorithm = NAMED_CURVES[private_key.group.curve_name] - if algorithm != key_algorithm - raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} signing key was provided" - end - - digest = OpenSSL::Digest.new(algorithm.sub('ES', 'sha')) - SecurityUtils.asn1_to_raw(private_key.dsa_sign_asn1(digest.digest(msg)), private_key) - end - - def sign_eddsa(algorithm, msg, private_key) - raise EncodeError, "Key given is a #{private_key.class} but has to be an RbNaCl::Signatures::Ed25519::SigningKey" if private_key.class != RbNaCl::Signatures::Ed25519::SigningKey - raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{private_key.primitive} signing key was provided" if algorithm.downcase.to_sym != private_key.primitive - private_key.sign(msg) - end - - def sign_hmac(algorithm, msg, key) - authenticator, padded_key = SecurityUtils.rbnacl_fixup(algorithm, key) - if authenticator && padded_key - authenticator.auth(padded_key, msg.encode('binary')) - else - OpenSSL::HMAC.digest(OpenSSL::Digest.new(algorithm.sub('HS', 'sha')), key, msg) - end - end - - def verify_eddsa(algorithm, public_key, signing_input, signature) - raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{public_key.primitive} verification key was provided" if algorithm.downcase.to_sym != public_key.primitive - raise DecodeError, "key given is a #{public_key.class} but has to be a RbNaCl::Signatures::Ed25519::VerifyKey" if public_key.class != RbNaCl::Signatures::Ed25519::VerifyKey - public_key.verify(signature, signing_input) - end - - def verify_ecdsa(algorithm, public_key, signing_input, signature) - key_algorithm = NAMED_CURVES[public_key.group.curve_name] - if algorithm != key_algorithm - raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} verification key was provided" - end - digest = OpenSSL::Digest.new(algorithm.sub('ES', 'sha')) - public_key.dsa_verify_asn1(digest.digest(signing_input), SecurityUtils.raw_to_asn1(signature, public_key)) - end - - def verify_hmac(algorithm, public_key, signing_input, signature) - authenticator, padded_key = SecurityUtils.rbnacl_fixup(algorithm, public_key) - if authenticator && padded_key - begin - authenticator.verify(padded_key, signature.encode('binary'), signing_input.encode('binary')) - rescue RbNaCl::BadAuthenticatorError - false - end - else - SecurityUtils.secure_compare(signature, sign_hmac(algorithm, signing_input, public_key)) - end - end end end