Skip to content

Commit

Permalink
Build list of algorithms as they are included
Browse files Browse the repository at this point in the history
  • Loading branch information
anakinj committed Jul 29, 2024
1 parent e674ca3 commit d804682
Show file tree
Hide file tree
Showing 12 changed files with 162 additions and 108 deletions.
19 changes: 1 addition & 18 deletions lib/jwt/jwa.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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)

Expand All @@ -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
37 changes: 37 additions & 0 deletions lib/jwt/jwa/algorithm.rb
Original file line number Diff line number Diff line change
@@ -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
70 changes: 37 additions & 33 deletions lib/jwt/jwa/ecdsa.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
module JWT
module JWA
module Ecdsa
module_function
include JWT::JWA::Algorithm

NAMED_CURVES = {
'prime256v1' => {
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions lib/jwt/jwa/eddsa.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
32 changes: 17 additions & 15 deletions lib/jwt/jwa/hmac.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions lib/jwt/jwa/hmac_rbnacl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
3 changes: 3 additions & 0 deletions lib/jwt/jwa/hmac_rbnacl_fixed.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 ||= ''
Expand Down
17 changes: 10 additions & 7 deletions lib/jwt/jwa/none.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 17 additions & 14 deletions lib/jwt/jwa/ps.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 15 additions & 12 deletions lib/jwt/jwa/rsa.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 7 additions & 9 deletions lib/jwt/jwa/unsupported.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit d804682

Please sign in to comment.