Skip to content
35 changes: 35 additions & 0 deletions lib/jwt/algos/ecdsa.rb
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions lib/jwt/algos/eddsa.rb
Original file line number Diff line number Diff line change
@@ -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
33 changes: 33 additions & 0 deletions lib/jwt/algos/hmac.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module JWT
module Algos
module Hmac
module_function
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep a blank line after 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
19 changes: 19 additions & 0 deletions lib/jwt/algos/rsa.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module JWT
module Algos
module Rsa
module_function
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep a blank line after 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
16 changes: 16 additions & 0 deletions lib/jwt/algos/unsupported.rb
Original file line number Diff line number Diff line change
@@ -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
114 changes: 21 additions & 93 deletions lib/jwt/signature.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 = [
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Freeze mutable objects assigned to constants.

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