diff --git a/lib/jwt/jwa.rb b/lib/jwt/jwa.rb index 6e88f514..4a8b4774 100644 --- a/lib/jwt/jwa.rb +++ b/lib/jwt/jwa.rb @@ -7,7 +7,7 @@ rescue LoadError raise if defined?(RbNaCl) end - +require_relative 'jwa/algorithm' require_relative 'jwa/hmac' require_relative 'jwa/eddsa' require_relative 'jwa/ecdsa' @@ -30,10 +30,6 @@ module JWA end.freeze class << self - def find(algorithm) - indexed[algorithm&.downcase] - end - def create(algorithm) return algorithm if JWA.implementation?(algorithm) @@ -44,19 +40,6 @@ def implementation?(algorithm) (algorithm.respond_to?(:valid_alg?) && algorithm.respond_to?(:verify)) || (algorithm.respond_to?(:alg) && algorithm.respond_to?(:sign)) end - - private - - def indexed - @indexed ||= begin - fallback = [nil, Unsupported] - ALGOS.each_with_object(Hash.new(fallback)) do |cls, hash| - cls.const_get(:SUPPORTED).each do |alg| - hash[alg.downcase] = [alg, cls] - end - end - end - end end end end diff --git a/lib/jwt/jwa/algorithm.rb b/lib/jwt/jwa/algorithm.rb new file mode 100644 index 00000000..169fc39a --- /dev/null +++ b/lib/jwt/jwa/algorithm.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require_relative 'unsupported' + +module JWT + module JWA + module Algorithm + module ClassMethods + def register_algorithm(*algos) + ::JWT::JWA.register_algorithm(self, *algos) + end + end + + def self.included(klass) + klass.extend(ClassMethods) + end + end + + class << self + def register_algorithm(klass, *algos) + algos.each do |algo| + algorithms[algo.to_s.downcase] = [algo, klass] + end + end + + def find(algo) + algorithms.fetch(algo.downcase) { [nil, Unsupported] } + end + + private + + def algorithms + @algorithms ||= {} + end + end + end +end diff --git a/lib/jwt/jwa/ecdsa.rb b/lib/jwt/jwa/ecdsa.rb index 8d9313c1..c865e8a5 100644 --- a/lib/jwt/jwa/ecdsa.rb +++ b/lib/jwt/jwa/ecdsa.rb @@ -3,7 +3,7 @@ module JWT module JWA module Ecdsa - module_function + include JWT::JWA::Algorithm NAMED_CURVES = { 'prime256v1' => { @@ -30,46 +30,50 @@ module Ecdsa SUPPORTED = NAMED_CURVES.map { |_, c| c[:algorithm] }.uniq.freeze - def sign(algorithm, msg, key) - curve_definition = curve_by_name(key.group.curve_name) - key_algorithm = curve_definition[:algorithm] - if algorithm != key_algorithm - raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} signing key was provided" - end + register_algorithm(*SUPPORTED) - digest = OpenSSL::Digest.new(curve_definition[:digest]) - asn1_to_raw(key.dsa_sign_asn1(digest.digest(msg)), key) - end + class << self + def sign(algorithm, msg, key) + curve_definition = curve_by_name(key.group.curve_name) + key_algorithm = curve_definition[:algorithm] + if algorithm != key_algorithm + raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} signing key was provided" + end - 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 - raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} verification key was provided" + digest = OpenSSL::Digest.new(curve_definition[:digest]) + asn1_to_raw(key.dsa_sign_asn1(digest.digest(msg)), key) end - digest = OpenSSL::Digest.new(curve_definition[:digest]) - public_key.dsa_verify_asn1(digest.digest(signing_input), raw_to_asn1(signature, public_key)) - rescue OpenSSL::PKey::PKeyError - raise JWT::VerificationError, 'Signature verification raised' - end + 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 + raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} verification key was provided" + end - def curve_by_name(name) - NAMED_CURVES.fetch(name) do - raise UnsupportedEcdsaCurve, "The ECDSA curve '#{name}' is not supported" + digest = OpenSSL::Digest.new(curve_definition[:digest]) + public_key.dsa_verify_asn1(digest.digest(signing_input), raw_to_asn1(signature, public_key)) + rescue OpenSSL::PKey::PKeyError + raise JWT::VerificationError, 'Signature verification raised' end - end - def raw_to_asn1(signature, private_key) - byte_size = (private_key.group.degree + 7) / 8 - sig_bytes = signature[0..(byte_size - 1)] - sig_char = signature[byte_size..-1] || '' - OpenSSL::ASN1::Sequence.new([sig_bytes, sig_char].map { |int| OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(int, 2)) }).to_der - end + def curve_by_name(name) + NAMED_CURVES.fetch(name) do + raise UnsupportedEcdsaCurve, "The ECDSA curve '#{name}' is not supported" + end + end + + def raw_to_asn1(signature, private_key) + byte_size = (private_key.group.degree + 7) / 8 + sig_bytes = signature[0..(byte_size - 1)] + sig_char = signature[byte_size..-1] || '' + OpenSSL::ASN1::Sequence.new([sig_bytes, sig_char].map { |int| OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(int, 2)) }).to_der + end - def asn1_to_raw(signature, public_key) - byte_size = (public_key.group.degree + 7) / 8 - OpenSSL::ASN1.decode(signature).value.map { |value| value.value.to_s(2).rjust(byte_size, "\x00") }.join + def asn1_to_raw(signature, public_key) + byte_size = (public_key.group.degree + 7) / 8 + OpenSSL::ASN1.decode(signature).value.map { |value| value.value.to_s(2).rjust(byte_size, "\x00") }.join + end end end end diff --git a/lib/jwt/jwa/eddsa.rb b/lib/jwt/jwa/eddsa.rb index eef27f1b..85f19468 100644 --- a/lib/jwt/jwa/eddsa.rb +++ b/lib/jwt/jwa/eddsa.rb @@ -3,9 +3,13 @@ module JWT module JWA module Eddsa + include JWT::JWA::Algorithm + SUPPORTED = %w[ED25519 EdDSA].freeze SUPPORTED_DOWNCASED = SUPPORTED.map(&:downcase).freeze + register_algorithm(*SUPPORTED) + class << self def sign(algorithm, msg, key) unless key.is_a?(RbNaCl::Signatures::Ed25519::SigningKey) diff --git a/lib/jwt/jwa/hmac.rb b/lib/jwt/jwa/hmac.rb index da73ff64..0d96e0d7 100644 --- a/lib/jwt/jwa/hmac.rb +++ b/lib/jwt/jwa/hmac.rb @@ -3,32 +3,34 @@ module JWT module JWA module Hmac - module_function + include JWT::JWA::Algorithm - MAPPING = { + DIGEST_MAPPING = { 'HS256' => OpenSSL::Digest::SHA256, 'HS384' => OpenSSL::Digest::SHA384, 'HS512' => OpenSSL::Digest::SHA512 }.freeze - SUPPORTED = MAPPING.keys + register_algorithm(*DIGEST_MAPPING.keys) - def sign(algorithm, msg, key) - key ||= '' + class << self + def sign(algorithm, msg, key) + key ||= '' - raise JWT::DecodeError, 'HMAC key expected to be a String' unless key.is_a?(String) + raise JWT::DecodeError, 'HMAC key expected to be a String' unless key.is_a?(String) - OpenSSL::HMAC.digest(MAPPING[algorithm].new, key, msg) - rescue OpenSSL::HMACError => e - if key == '' && e.message == 'EVP_PKEY_new_mac_key: malloc failure' - raise JWT::DecodeError, 'OpenSSL 3.0 does not support nil or empty hmac_secret' - end + OpenSSL::HMAC.digest(DIGEST_MAPPING[algorithm].new, key, msg) + rescue OpenSSL::HMACError => e + if key == '' && e.message == 'EVP_PKEY_new_mac_key: malloc failure' + raise JWT::DecodeError, 'OpenSSL 3.0 does not support nil or empty hmac_secret' + end - raise e - end + raise e + end - def verify(algorithm, key, signing_input, signature) - SecurityUtils.secure_compare(signature, sign(algorithm, signing_input, key)) + def verify(algorithm, key, signing_input, signature) + SecurityUtils.secure_compare(signature, sign(algorithm, signing_input, key)) + end end # Copy of https://github.com/rails/rails/blob/v7.0.3.1/activesupport/lib/active_support/security_utils.rb diff --git a/lib/jwt/jwa/hmac_rbnacl.rb b/lib/jwt/jwa/hmac_rbnacl.rb index 67c1ac41..d710459f 100644 --- a/lib/jwt/jwa/hmac_rbnacl.rb +++ b/lib/jwt/jwa/hmac_rbnacl.rb @@ -3,8 +3,13 @@ module JWT module Algos module HmacRbNaCl + include JWT::JWA::Algorithm + MAPPING = { 'HS512256' => ::RbNaCl::HMAC::SHA512256 }.freeze SUPPORTED = MAPPING.keys + + register_algorithm(*SUPPORTED) + class << self def sign(algorithm, msg, key) Deprecations.warning("The use of the algorithm #{algorithm} is deprecated and will be removed in the next major version of ruby-jwt") diff --git a/lib/jwt/jwa/hmac_rbnacl_fixed.rb b/lib/jwt/jwa/hmac_rbnacl_fixed.rb index cece2d52..5bbfbf66 100644 --- a/lib/jwt/jwa/hmac_rbnacl_fixed.rb +++ b/lib/jwt/jwa/hmac_rbnacl_fixed.rb @@ -3,9 +3,12 @@ module JWT module Algos module HmacRbNaClFixed + include JWT::JWA::Algorithm + MAPPING = { 'HS512256' => ::RbNaCl::HMAC::SHA512256 }.freeze SUPPORTED = MAPPING.keys + register_algorithm(*SUPPORTED) class << self def sign(algorithm, msg, key) key ||= '' diff --git a/lib/jwt/jwa/none.rb b/lib/jwt/jwa/none.rb index d6fd7e62..02ca6259 100644 --- a/lib/jwt/jwa/none.rb +++ b/lib/jwt/jwa/none.rb @@ -3,16 +3,19 @@ module JWT module JWA module None - module_function - + include JWT::JWA::Algorithm SUPPORTED = %w[none].freeze - def sign(*) - '' - end + register_algorithm(*SUPPORTED) + + class << self + def sign(*) + '' + end - def verify(*) - true + def verify(*) + true + end end end end diff --git a/lib/jwt/jwa/ps.rb b/lib/jwt/jwa/ps.rb index f7488bd0..382d155e 100644 --- a/lib/jwt/jwa/ps.rb +++ b/lib/jwt/jwa/ps.rb @@ -5,25 +5,28 @@ module JWA module Ps # RSASSA-PSS signing algorithms - module_function - + include JWT::JWA::Algorithm SUPPORTED = %w[PS256 PS384 PS512].freeze - def sign(algorithm, msg, key) - unless key.is_a?(::OpenSSL::PKey::RSA) - raise EncodeError, "The given key is a #{key_class}. It has to be an OpenSSL::PKey::RSA instance." - end + register_algorithm(*SUPPORTED) - translated_algorithm = algorithm.sub('PS', 'sha') + class << self + def sign(algorithm, msg, key) + unless key.is_a?(::OpenSSL::PKey::RSA) + raise EncodeError, "The given key is a #{key_class}. It has to be an OpenSSL::PKey::RSA instance." + end - key.sign_pss(translated_algorithm, msg, salt_length: :digest, mgf1_hash: translated_algorithm) - end + translated_algorithm = algorithm.sub('PS', 'sha') - def verify(algorithm, public_key, signing_input, signature) - translated_algorithm = algorithm.sub('PS', 'sha') - public_key.verify_pss(translated_algorithm, signature, signing_input, salt_length: :auto, mgf1_hash: translated_algorithm) - rescue OpenSSL::PKey::PKeyError - raise JWT::VerificationError, 'Signature verification raised' + key.sign_pss(translated_algorithm, msg, salt_length: :digest, mgf1_hash: translated_algorithm) + end + + def verify(algorithm, public_key, signing_input, signature) + translated_algorithm = algorithm.sub('PS', 'sha') + public_key.verify_pss(translated_algorithm, signature, signing_input, salt_length: :auto, mgf1_hash: translated_algorithm) + rescue OpenSSL::PKey::PKeyError + raise JWT::VerificationError, 'Signature verification raised' + end end end end diff --git a/lib/jwt/jwa/rsa.rb b/lib/jwt/jwa/rsa.rb index f9970bda..dbe8932e 100644 --- a/lib/jwt/jwa/rsa.rb +++ b/lib/jwt/jwa/rsa.rb @@ -3,22 +3,25 @@ module JWT module JWA module Rsa - module_function - + include JWT::JWA::Algorithm SUPPORTED = %w[RS256 RS384 RS512].freeze - def sign(algorithm, msg, key) - unless key.is_a?(OpenSSL::PKey::RSA) - raise EncodeError, "The given key is a #{key.class}. It has to be an OpenSSL::PKey::RSA instance" - end + register_algorithm(*SUPPORTED) - key.sign(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), msg) - end + class << self + def sign(algorithm, msg, key) + unless key.is_a?(OpenSSL::PKey::RSA) + raise EncodeError, "The given key is a #{key.class}. It has to be an OpenSSL::PKey::RSA instance" + end - def verify(algorithm, public_key, signing_input, signature) - public_key.verify(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), signature, signing_input) - rescue OpenSSL::PKey::PKeyError - raise JWT::VerificationError, 'Signature verification raised' + key.sign(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), msg) + end + + def verify(algorithm, public_key, signing_input, signature) + public_key.verify(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), signature, signing_input) + rescue OpenSSL::PKey::PKeyError + raise JWT::VerificationError, 'Signature verification raised' + end end end end diff --git a/lib/jwt/jwa/unsupported.rb b/lib/jwt/jwa/unsupported.rb index 681a6a75..4ed21549 100644 --- a/lib/jwt/jwa/unsupported.rb +++ b/lib/jwt/jwa/unsupported.rb @@ -3,16 +3,14 @@ module JWT module JWA module Unsupported - module_function + class << self + def sign(*) + raise NotImplementedError, 'Unsupported signing method' + end - SUPPORTED = [].freeze - - def sign(*) - raise NotImplementedError, 'Unsupported signing method' - end - - def verify(*) - raise JWT::VerificationError, 'Algorithm not supported' + def verify(*) + raise JWT::VerificationError, 'Algorithm not supported' + end end end end diff --git a/lib/jwt/token.rb b/lib/jwt/token.rb new file mode 100644 index 00000000..388859e7 --- /dev/null +++ b/lib/jwt/token.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module JWT + class DecodeContext + def initialize(token) + @token = token + end + end +end