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

Move signature logic to its own module #195

Merged
merged 4 commits into from
Feb 19, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
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
91 changes: 2 additions & 89 deletions lib/jwt.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# frozen_string_literal: true
require 'base64'
require 'openssl'
require 'jwt/decode'
require 'jwt/default_options'
require 'jwt/encode'
require 'jwt/error'
require 'jwt/json'
require 'jwt/signature'

# JSON Web Token implementation
#
Expand All @@ -17,51 +17,6 @@ module JWT

module_function

def sign(algorithm, msg, key)
if %w(HS256 HS384 HS512).include?(algorithm)
sign_hmac(algorithm, msg, key)
elsif %w(RS256 RS384 RS512).include?(algorithm)
sign_rsa(algorithm, msg, key)
elsif %w(ES256 ES384 ES512).include?(algorithm)
sign_ecdsa(algorithm, msg, key)
else
raise NotImplementedError, 'Unsupported signing method'
end
end

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'))
asn1_to_raw(private_key.dsa_sign_asn1(digest.digest(msg)), private_key)
end

def verify_rsa(algorithm, public_key, signing_input, signature)
public_key.verify(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), 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), raw_to_asn1(signature, public_key))
end

def sign_hmac(algorithm, msg, key)
OpenSSL::HMAC.digest(OpenSSL::Digest.new(algorithm.sub('HS', 'sha')), key, msg)
end

def decoded_segments(jwt, key = nil, verify = true, custom_options = {}, &keyfinder)
raise(JWT::DecodeError, 'Nil JSON web token') unless jwt

Expand Down Expand Up @@ -96,7 +51,7 @@ def decode_verify_signature(key, header, payload, signature, signing_input, opti
raise(JWT::IncorrectAlgorithm, 'An algorithm must be specified') unless options[:algorithm]
raise(JWT::IncorrectAlgorithm, 'Expected a different algorithm') unless algo == options[:algorithm]

verify_signature(algo, key, signing_input, signature)
Signature.verify(algo, key, signing_input, signature)
end

def signature_algorithm_and_key(header, payload, key, &keyfinder)
Expand All @@ -111,48 +66,6 @@ def signature_algorithm_and_key(header, payload, key, &keyfinder)
[header['alg'], key]
end

def verify_signature(algo, key, signing_input, signature)
verify_signature_algo(algo, key, signing_input, signature)
rescue OpenSSL::PKey::PKeyError
raise JWT::VerificationError, 'Signature verification raised'
ensure
OpenSSL.errors.clear
end

def verify_signature_algo(algo, key, signing_input, signature)
if %w(HS256 HS384 HS512).include?(algo)
raise(JWT::VerificationError, 'Signature verification raised') unless secure_compare(signature, sign_hmac(algo, signing_input, key))
elsif %w(RS256 RS384 RS512).include?(algo)
raise(JWT::VerificationError, 'Signature verification raised') unless verify_rsa(algo, key, signing_input, signature)
elsif %w(ES256 ES384 ES512).include?(algo)
raise(JWT::VerificationError, 'Signature verification raised') unless verify_ecdsa(algo, key, signing_input, signature)
else
raise JWT::VerificationError, 'Algorithm not supported'
end
end

# From devise
# constant-time comparison algorithm to prevent timing attacks
def secure_compare(a, b)
return false if a.nil? || b.nil? || a.empty? || b.empty? || a.bytesize != b.bytesize
l = a.unpack "C#{a.bytesize}"
res = 0
b.each_byte { |byte| res |= byte ^ l.shift }
res.zero?
end

def raw_to_asn1(signature, private_key)
byte_size = (private_key.group.degree + 7) / 8
r = signature[0..(byte_size - 1)]
s = signature[byte_size..-1] || ''
OpenSSL::ASN1::Sequence.new([r, s].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
end

def base64url_decode(str)
Decode.base64url_decode(str)
end
Expand Down
2 changes: 0 additions & 2 deletions lib/jwt/default_options.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
module JWT
module DefaultOptions
NAMED_CURVES = { 'prime256v1' => 'ES256', 'secp384r1' => 'ES384', 'secp521r1' => 'ES512' }.freeze

DEFAULT_OPTIONS = {
verify_expiration: true,
verify_not_before: true,
Expand Down
2 changes: 1 addition & 1 deletion lib/jwt/encode.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def encoded_signature(signing_input, key, algorithm)
if algorithm == 'none'
''
else
signature = JWT.sign(algorithm, signing_input, key)
signature = JWT::Signature.sign(algorithm, signing_input, key)
JWT.base64url_encode(signature)
end
end
Expand Down
105 changes: 105 additions & 0 deletions lib/jwt/signature.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# frozen_string_literal: true
require 'openssl'

module JWT
module Signature
extend self
Copy link
Member

Choose a reason for hiding this comment

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

Please use module_function instead of extend self

Copy link
Contributor Author

Choose a reason for hiding this comment

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

what about private functions? I don't think it's possible to define private function with module_functions.
What are the benefits of module_functions?

Copy link
Member

@excpt excpt Feb 13, 2017

Choose a reason for hiding this comment

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

Using module_function is the preferred coding style.

You're alway able to define a function as private by using this syntax:

def my_fun(my_param) do
  puts my_param
end
private :my_fun

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think module_function respects the private visibility.

module A
  module_function

  def my_fun(my_param)
    puts my_param
  end
  private :my_fun
end

A.my_fun("calling private method")   #=>  "calling private method"


HMAC_ALGORITHMS = %w(HS256 HS384 HS512).freeze
RSA_ALGORITHMS = %w(RS256 RS384 RS512).freeze
ECDSA_ALGORITHMS = %w(ES256 ES384 ES512).freeze

NAMED_CURVES = {
'prime256v1' => 'ES256',
'secp384r1' => 'ES384',
'secp521r1' => 'ES512'
}.freeze

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)
else
raise NotImplementedError, 'Unsupported signing method'
end
end

def verify(algo, key, signing_input, signature)
verified = if HMAC_ALGORITHMS.include?(algo)
secure_compare(signature, sign_hmac(algo, signing_input, key))
elsif RSA_ALGORITHMS.include?(algo)
verify_rsa(algo, key, signing_input, signature)
elsif ECDSA_ALGORITHMS.include?(algo)
verify_ecdsa(algo, key, signing_input, signature)
else
raise JWT::VerificationError, 'Algorithm not supported'
end

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'))
asn1_to_raw(private_key.dsa_sign_asn1(digest.digest(msg)), private_key)
end

def sign_hmac(algorithm, msg, key)
OpenSSL::HMAC.digest(OpenSSL::Digest.new(algorithm.sub('HS', 'sha')), key, msg)
end

def verify_rsa(algorithm, public_key, signing_input, signature)
public_key.verify(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), 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), raw_to_asn1(signature, public_key))
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
end

def raw_to_asn1(signature, private_key)
byte_size = (private_key.group.degree + 7) / 8
r = signature[0..(byte_size - 1)]
s = signature[byte_size..-1] || ''
OpenSSL::ASN1::Sequence.new([r, s].map { |int| OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(int, 2)) }).to_der
end

# From devise
# constant-time comparison algorithm to prevent timing attacks
def secure_compare(a, b)
return false if a.nil? || b.nil? || a.empty? || b.empty? || a.bytesize != b.bytesize
l = a.unpack "C#{a.bytesize}"
res = 0
b.each_byte { |byte| res |= byte ^ l.shift }
res.zero?
end
end
end
19 changes: 1 addition & 18 deletions spec/jwt_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
it 'wrong secret should raise JWT::DecodeError' do
expect do
JWT.decode data[alg], 'wrong_secret', true, algorithm: alg
end.to raise_error JWT::DecodeError
end.to raise_error JWT::VerificationError
end

it 'wrong secret and verify = false should not raise JWT::DecodeError' do
Expand Down Expand Up @@ -228,21 +228,4 @@
expect(JWT.base64url_encode('foo')).to eq('string-with_non-url-safe_characters_')
end
end

describe 'secure comparison' do
it 'returns true if strings are equal' do
expect(JWT.secure_compare('Foo', 'Foo')).to eq true
end

it 'returns false if either input is nil or empty' do
[nil, ''].each do |bad|
expect(JWT.secure_compare(bad, 'Foo')).to eq false
expect(JWT.secure_compare('Foo', bad)).to eq false
end
end

it 'retuns false if the strings are different' do
expect(JWT.secure_compare('Foo', 'Bar')).to eq false
end
end
end