From b912f7bdd2a939c5c54b467580473d87b9dc5b13 Mon Sep 17 00:00:00 2001 From: Mike Pastore Date: Tue, 21 Feb 2017 17:12:59 -0600 Subject: [PATCH] Use RbNaCl for HMAC if available with fallback to OpenSSL --- .travis.yml | 6 ++++++ README.md | 3 +++ lib/jwt/signature.rb | 44 +++++++++++++++++++++++++++++++++++++++++--- ruby-jwt.gemspec | 1 + spec/jwt_spec.rb | 3 ++- 5 files changed, 53 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 888474e1..653f3697 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ +sudo: required cache: bundler language: ruby rvm: @@ -5,3 +6,8 @@ rvm: - 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 diff --git a/README.md b/README.md index dbb3fec3..e2478653 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/lib/jwt/signature.rb b/lib/jwt/signature.rb index 4c479025..395b83f2 100644 --- a/lib/jwt/signature.rb +++ b/lib/jwt/signature.rb @@ -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 @@ -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) @@ -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) @@ -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 @@ -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) diff --git a/ruby-jwt.gemspec b/ruby-jwt.gemspec index dcd4fa27..efcc4b76 100644 --- a/ruby-jwt.gemspec +++ b/ruby-jwt.gemspec @@ -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 diff --git a/spec/jwt_spec.rb b/spec/jwt_spec.rb index b379ea01..2db731da 100644 --- a/spec/jwt_spec.rb +++ b/spec/jwt_spec.rb @@ -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', @@ -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