Skip to content

Commit

Permalink
Use RbNaCl for HMAC if available with fallback to OpenSSL
Browse files Browse the repository at this point in the history
  • Loading branch information
mwpastore committed Feb 22, 2017
1 parent a1c9e42 commit b912f7b
Show file tree
Hide file tree
Showing 5 changed files with 53 additions and 4 deletions.
6 changes: 6 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
sudo: required
cache: bundler
language: ruby
rvm:
- 2.2.0
- 2.3.0
- 2.4.0
script: "bundle exec rspec && bundle exec codeclimate-test-reporter"
before_install:
- sudo add-apt-repository ppa:chris-lea/libsodium -y
- sudo apt-get update -q
- sudo apt-get install libsodium-dev -y
- gem install bundler
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ puts decoded_token
**HMAC** (default: HS256)

* HS256 - HMAC using SHA-256 hash algorithm (default)
* HS512256 - HMAC using SHA-512/256 hash algorithm (only available with RbNaCl; see note below)
* HS384 - HMAC using SHA-384 hash algorithm
* HS512 - HMAC using SHA-512 hash algorithm

Expand All @@ -84,6 +85,8 @@ decoded_token = JWT.decode token, hmac_secret, true, { :algorithm => 'HS256' }
puts decoded_token
```

Note: If [RbNaCl](https://github.com/cryptosphere/rbnacl) is loadable, ruby-jwt will use it for HMAC-SHA256, HMAC-SHA512/256, and HMAC-SHA512. RbNaCl enforces a maximum key size of 32 bytes for these algorithms.

**RSA**

* RS256 - RSA using SHA-256 hash algorithm
Expand Down
44 changes: 41 additions & 3 deletions lib/jwt/signature.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
# frozen_string_literal: true
require 'openssl'
begin
require 'rbnacl'
rescue LoadError
end

module JWT
module Signature
extend self

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

Expand All @@ -29,7 +33,7 @@ def sign(algorithm, msg, key)

def verify(algo, key, signing_input, signature)
verified = if HMAC_ALGORITHMS.include?(algo)
secure_compare(signature, sign_hmac(algo, signing_input, key))
verify_hmac(algo, key, signing_input, signature)
elsif RSA_ALGORITHMS.include?(algo)
verify_rsa(algo, key, signing_input, signature)
elsif ECDSA_ALGORITHMS.include?(algo)
Expand Down Expand Up @@ -63,7 +67,12 @@ def sign_ecdsa(algorithm, msg, private_key)
end

def sign_hmac(algorithm, msg, key)
OpenSSL::HMAC.digest(OpenSSL::Digest.new(algorithm.sub('HS', 'sha')), key, msg)
authenticator, padded_key = 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_rsa(algorithm, public_key, signing_input, signature)
Expand All @@ -80,6 +89,19 @@ def verify_ecdsa(algorithm, public_key, signing_input, signature)
public_key.dsa_verify_asn1(digest.digest(signing_input), raw_to_asn1(signature, public_key))
end

def verify_hmac(algorithm, public_key, signing_input, signature)
authenticator, padded_key = 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
secure_compare(signature, sign_hmac(algorithm, signing_input, public_key))
end
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
Expand All @@ -92,6 +114,22 @@ def raw_to_asn1(signature, private_key)
OpenSSL::ASN1::Sequence.new([r, s].map { |int| OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(int, 2)) }).to_der
end

def rbnacl_fixup(algorithm, key)
algorithm = algorithm.sub('HS', 'SHA').to_sym

return [] unless defined?(RbNaCl) && RbNaCl::HMAC.constants(false).include?(algorithm)

authenticator = RbNaCl::HMAC.const_get(algorithm)

# Fall back to OpenSSL for keys larger than 32 bytes.
return [] if key.bytesize > authenticator.key_bytes

[
authenticator,
key.bytes.fill(0, key.bytesize...authenticator.key_bytes).pack('C*')
]
end

# From devise
# constant-time comparison algorithm to prevent timing attacks
def secure_compare(a, b)
Expand Down
1 change: 1 addition & 0 deletions ruby-jwt.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ Gem::Specification.new do |spec|
spec.add_development_dependency 'simplecov-json'
spec.add_development_dependency 'codeclimate-test-reporter'
spec.add_development_dependency 'codacy-coverage'
spec.add_development_dependency 'rbnacl'
end
3 changes: 2 additions & 1 deletion spec/jwt_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
'ES512_public' => OpenSSL::PKey.read(File.read(File.join(CERT_PATH, 'ec512-public.pem'))),
'NONE' => 'eyJhbGciOiJub25lIn0.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.',
'HS256' => 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.kWOVtIOpWcG7JnyJG0qOkTDbOy636XrrQhMm_8JrRQ8',
'HS512256' => 'eyJhbGciOiJIUzUxMjI1NiJ9.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.Ds_4ibvf7z4QOBoKntEjDfthy3WJ-3rKMspTEcHE2bA',
'HS384' => 'eyJhbGciOiJIUzM4NCJ9.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.VuV4j4A1HKhWxCNzEcwc9qVF3frrEu-BRLzvYPkbWO0LENRGy5dOiBQ34remM3XH',
'HS512' => 'eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.8zNtCBTJIZTHpZ-BkhR-6sZY1K85Nm5YCKqV3AxRdsBJDt_RR-REH2db4T3Y0uQwNknhrCnZGvhNHrvhDwV1kA',
'RS256' => 'eyJhbGciOiJSUzI1NiJ9.eyJ1c2VyX2lkIjoic29tZUB1c2VyLnRsZCJ9.eSXvWP4GViiwUALj_-qTxU68I1oM0XjgDsCZBBUri2Ghh9d75QkVDoZ_v872GaqunN5A5xcnBK0-cOq-CR6OwibgJWfOt69GNzw5RrOfQ2mz3QI3NYEq080nF69h8BeqkiaXhI24Q51joEgfa9aj5Y-oitLAmtDPYTm7vTcdGufd6AwD3_3jajKBwkh0LPSeMtbe_5EyS94nFoEF9OQuhJYjUmp7agsBVa8FFEjVw5jEgVqkvERSj5hSY4nEiCAomdVxIKBfykyi0d12cgjhI7mBFwWkPku8XIPGZ7N8vpiSLdM68BnUqIK5qR7NAhtvT7iyLFgOqhZNUQ6Ret5VpQ',
Expand Down Expand Up @@ -61,7 +62,7 @@
end
end

%w(HS256 HS384 HS512).each do |alg|
%w(HS256 HS512256 HS384 HS512).each do |alg|
context "alg: #{alg}" do
it 'should generate a valid token' do
token = JWT.encode payload, data[:secret], alg
Expand Down

0 comments on commit b912f7b

Please sign in to comment.