Skip to content
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

**Features:**

- Add `enforce_hmac_key_length` configuration option [#716](https://github.com/jwt/ruby-jwt/pull/716) - ([@304](https://github.com/304))
- Your contribution here

**Fixes and enhancements:**
Expand Down
9 changes: 7 additions & 2 deletions lib/jwt/configuration/decode_configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ class DecodeConfiguration
# @return [Array<String>] the list of acceptable algorithms.
# @!attribute [rw] required_claims
# @return [Array<String>] the list of required claims.
# @!attribute [rw] enforce_hmac_key_length
# @return [Boolean] whether to enforce minimum HMAC key lengths. false disables validation (default).

attr_accessor :verify_expiration,
:verify_not_before,
Expand All @@ -34,7 +36,8 @@ class DecodeConfiguration
:verify_sub,
:leeway,
:algorithms,
:required_claims
:required_claims,
:enforce_hmac_key_length

# Initializes a new DecodeConfiguration instance with default settings.
def initialize
Expand All @@ -48,6 +51,7 @@ def initialize
@leeway = 0
@algorithms = ['HS256']
@required_claims = []
@enforce_hmac_key_length = false
end

# @api private
Expand All @@ -62,7 +66,8 @@ def to_h
verify_sub: verify_sub,
leeway: leeway,
algorithms: algorithms,
required_claims: required_claims
required_claims: required_claims,
enforce_hmac_key_length: enforce_hmac_key_length
}
end
end
Expand Down
25 changes: 25 additions & 0 deletions lib/jwt/jwa/hmac.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ module JWA
class Hmac
include JWT::JWA::SigningAlgorithm

# Minimum key lengths for HMAC algorithms based on RFC 7518 Section 3.2.
# Keys must be at least the size of the hash output to ensure sufficient
# entropy for the algorithm's security level.
MIN_KEY_LENGTHS = {
Copy link
Member

Choose a reason for hiding this comment

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

Future us could wonder where are these values coming from, a comment pointing to the RFC would be useful at this point.

'HS256' => 32,
'HS384' => 48,
'HS512' => 64
}.freeze

def initialize(alg, digest)
@alg = alg
@digest = digest
Expand All @@ -15,6 +24,8 @@ def sign(data:, signing_key:)
signing_key ||= ''
raise_verify_error!('HMAC key expected to be a String') unless signing_key.is_a?(String)

validate_key_length!(signing_key)

OpenSSL::HMAC.digest(digest.new, signing_key, data)
rescue OpenSSL::HMACError => e
raise_verify_error!('OpenSSL 3.0 does not support nil or empty hmac_secret') if signing_key == '' && e.message == 'EVP_PKEY_new_mac_key: malloc failure'
Expand All @@ -23,6 +34,11 @@ def sign(data:, signing_key:)
end

def verify(data:, signature:, verification_key:)
validation_key = verification_key || ''
raise_verify_error!('HMAC key expected to be a String') unless validation_key.is_a?(String)

validate_key_length!(validation_key)

SecurityUtils.secure_compare(signature, sign(data: data, signing_key: verification_key))
end

Expand All @@ -34,6 +50,15 @@ def verify(data:, signature:, verification_key:)

attr_reader :digest

def validate_key_length!(key)
return unless JWT.configuration.decode.enforce_hmac_key_length

min_length = MIN_KEY_LENGTHS[alg]
return if key.bytesize >= min_length

raise_verify_error!("HMAC key must be at least #{min_length} bytes for #{alg} algorithm")
end

# Copy of https://github.com/rails/rails/blob/v7.0.3.1/activesupport/lib/active_support/security_utils.rb
# rubocop:disable Naming/MethodParameterName, Style/StringLiterals, Style/NumericPredicate
module SecurityUtils
Expand Down
63 changes: 63 additions & 0 deletions spec/jwt/jwa/hmac_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,32 @@
it { is_expected.to eql(response) }
end
end

context 'when enforce_hmac_key_length is enabled' do
before do
JWT.configuration.decode.enforce_hmac_key_length = true
end

after do
JWT.configuration.decode.enforce_hmac_key_length = false
end

context 'when key shorter than algorithm minimum' do
let(:hmac_secret) { 'short' }

it 'raises error' do
expect { subject }.to raise_error(JWT::DecodeError, 'HMAC key must be at least 32 bytes for HS256 algorithm')
end
end

context 'when key meets minimum length' do
let(:hmac_secret) { 'a' * 32 }

it 'does not raise error' do
expect { subject }.not_to raise_error
end
end
end
end

describe '#verify' do
Expand All @@ -124,5 +150,42 @@

it { is_expected.to be(false) }
end

context 'when verification_key is not a String' do
let(:signature) { valid_signature }
let(:hmac_secret) { 123 }

it 'raises error' do
expect { subject }.to raise_error(JWT::DecodeError, 'HMAC key expected to be a String')
end
end

context 'when enforce_hmac_key_length is enabled' do
before do
JWT.configuration.decode.enforce_hmac_key_length = true
end

after do
JWT.configuration.decode.enforce_hmac_key_length = false
end

let(:signature) { valid_signature }

context 'when key shorter than algorithm minimum' do
let(:hmac_secret) { 'short' }

it 'raises error' do
expect { subject }.to raise_error(JWT::DecodeError, 'HMAC key must be at least 32 bytes for HS256 algorithm')
end
end

context 'when key meets minimum length' do
let(:hmac_secret) { 'a' * 32 }

it 'does not raise error' do
expect { subject }.not_to raise_error
end
end
end
end
end
Loading