Skip to content

Commit db560b7

Browse files
authored
Merge commit from fork
* Reject nil and empty HMAC keys when signing and verifying * Version bump
1 parent ffef4f2 commit db560b7

6 files changed

Lines changed: 47 additions & 109 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
# Changelog
22

3-
## [v3.1.3](https://github.com/jwt/ruby-jwt/tree/v3.1.3) (NEXT)
3+
## [v3.2.0](https://github.com/jwt/ruby-jwt/tree/v3.2.0) (2026-05-13)
44

5-
[Full Changelog](https://github.com/jwt/ruby-jwt/compare/v3.1.2...v3.1.3)
5+
[Full Changelog](https://github.com/jwt/ruby-jwt/compare/v3.1.2...v3.2.0)
66

77
**Features:**
88

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

1211
**Fixes and enhancements:**
1312

13+
- Reject `nil` and empty HMAC keys when signing and verifying ([CVE-2026-45363](https://www.cve.org/CVERecord?id=CVE-2026-45363) / [GHSA-c32j-vqhx-rx3x](https://github.com/jwt/ruby-jwt/security/advisories/GHSA-c32j-vqhx-rx3x))
1414
- Fix compatibility with the openssl 4.0 gem [#706](https://github.com/jwt/ruby-jwt/pull/706)
1515
- Test against Ruby 4.0 on CI [#707](https://github.com/jwt/ruby-jwt/pull/707)
1616
- Fix type error when header is not a JSON object [#715](https://github.com/jwt/ruby-jwt/pull/715) - ([@304](https://github.com/304))
17-
- Your contribution here
1817

1918
## [v3.1.2](https://github.com/jwt/ruby-jwt/tree/v3.1.2) (2025-06-28)
2019

lib/jwt/jwa/hmac.rb

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,25 +21,17 @@ def initialize(alg, digest)
2121
end
2222

2323
def sign(data:, signing_key:)
24-
signing_key ||= ''
25-
raise_verify_error!('HMAC key expected to be a String') unless signing_key.is_a?(String)
26-
24+
ensure_valid_key!(signing_key)
2725
validate_key_length!(signing_key)
2826

2927
OpenSSL::HMAC.digest(digest.new, signing_key, data)
30-
rescue OpenSSL::HMACError => e
31-
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'
32-
33-
raise e
3428
end
3529

3630
def verify(data:, signature:, verification_key:)
37-
validation_key = verification_key || ''
38-
raise_verify_error!('HMAC key expected to be a String') unless validation_key.is_a?(String)
39-
40-
validate_key_length!(validation_key)
31+
ensure_valid_key!(verification_key)
32+
validate_key_length!(verification_key)
4133

42-
SecurityUtils.secure_compare(signature, sign(data: data, signing_key: verification_key))
34+
SecurityUtils.secure_compare(signature, OpenSSL::HMAC.digest(digest.new, verification_key, data))
4335
end
4436

4537
register_algorithm(new('HS256', OpenSSL::Digest::SHA256))
@@ -50,6 +42,11 @@ def verify(data:, signature:, verification_key:)
5042

5143
attr_reader :digest
5244

45+
def ensure_valid_key!(key)
46+
raise_verify_error!('HMAC key expected to be a String') unless key.is_a?(String)
47+
raise_verify_error!('HMAC key cannot be empty') if key.empty?
48+
end
49+
5350
def validate_key_length!(key)
5451
return unless JWT.configuration.decode.enforce_hmac_key_length
5552

lib/jwt/version.rb

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ def self.gem_version
1515
# Version constants
1616
module VERSION
1717
MAJOR = 3
18-
MINOR = 1
19-
TINY = 3
18+
MINOR = 2
19+
TINY = 0
2020
PRE = nil
2121

2222
STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
@@ -32,14 +32,6 @@ def self.openssl_3?
3232
true if 3 * 0x10000000 <= OpenSSL::OPENSSL_VERSION_NUMBER
3333
end
3434

35-
# Checks if there is an OpenSSL 3 HMAC empty key regression.
36-
#
37-
# @return [Boolean] true if there is an OpenSSL 3 HMAC empty key regression, false otherwise.
38-
# @api private
39-
def self.openssl_3_hmac_empty_key_regression?
40-
openssl_3? && openssl_version <= ::Gem::Version.new('3.0.0')
41-
end
42-
4335
# Returns the OpenSSL version.
4436
#
4537
# @return [Gem::Version] the OpenSSL version.

spec/integration/readme_examples_spec.rb

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,6 @@
2828
]
2929
end
3030

31-
it 'decodes with HMAC algorithm without secret key' do
32-
pending 'Different behaviour on OpenSSL 3.0 (https://github.com/openssl/openssl/issues/13089)' if JWT.openssl_3_hmac_empty_key_regression?
33-
token = JWT.encode payload, nil, 'HS256'
34-
decoded_token = JWT.decode token, nil, false
35-
36-
expect(token).to eq 'eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.pVzcY2dX8JNM3LzIYeP2B1e1Wcpt1K3TWVvIYSF4x-o'
37-
expect(decoded_token).to eq [
38-
{ 'data' => 'test' },
39-
{ 'alg' => 'HS256' }
40-
]
41-
end
42-
4331
it 'RSA' do
4432
rsa_private = OpenSSL::PKey::RSA.generate 2048
4533
rsa_public = rsa_private.public_key

spec/jwt/jwa/hmac_spec.rb

Lines changed: 33 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -12,68 +12,31 @@
1212
it { is_expected.to eq(valid_signature) }
1313
end
1414

15-
# Address OpenSSL 3.0 errors with empty hmac_secret - https://github.com/jwt/ruby-jwt/issues/526
15+
# GHSA-c32j-vqhx-rx3x: empty/nil keys must be rejected before reaching OpenSSL,
16+
# so a forged token signed with "" cannot verify.
1617
context 'when nil hmac_secret is passed' do
1718
let(:hmac_secret) { nil }
18-
context 'when OpenSSL 3.0 raises a malloc failure' do
19-
before do
20-
allow(OpenSSL::HMAC).to receive(:digest).and_raise(OpenSSL::HMACError.new('EVP_PKEY_new_mac_key: malloc failure'))
21-
end
22-
23-
it 'raises JWT::DecodeError' do
24-
expect { subject }.to raise_error(JWT::DecodeError, 'OpenSSL 3.0 does not support nil or empty hmac_secret')
25-
end
26-
end
2719

28-
context 'when OpenSSL raises any other error' do
29-
before do
30-
allow(OpenSSL::HMAC).to receive(:digest).and_raise(OpenSSL::HMACError.new('Another Random Error'))
31-
end
32-
33-
it 'raises the original error' do
34-
expect { subject }.to raise_error(OpenSSL::HMACError, 'Another Random Error')
35-
end
20+
it 'raises JWT::DecodeError' do
21+
expect { subject }.to raise_error(JWT::DecodeError, 'HMAC key expected to be a String')
3622
end
3723

38-
context 'when other versions of openssl do not raise an exception' do
39-
let(:response) { Base64.decode64("Q7DO+ZJl+eNMEOqdNQGSbSezn1fG1nRWHYuiNueoGfs=\n") }
40-
before do
41-
allow(OpenSSL::HMAC).to receive(:digest).and_return(response)
42-
end
43-
44-
it { is_expected.to eql(response) }
24+
it 'does not call OpenSSL::HMAC.digest' do
25+
expect(OpenSSL::HMAC).not_to receive(:digest)
26+
expect { subject }.to raise_error(JWT::DecodeError)
4527
end
4628
end
4729

4830
context 'when blank hmac_secret is passed' do
4931
let(:hmac_secret) { '' }
50-
context 'when OpenSSL 3.0 raises a malloc failure' do
51-
before do
52-
allow(OpenSSL::HMAC).to receive(:digest).and_raise(OpenSSL::HMACError.new('EVP_PKEY_new_mac_key: malloc failure'))
53-
end
54-
55-
it 'raises JWT::DecodeError' do
56-
expect { subject }.to raise_error(JWT::DecodeError, 'OpenSSL 3.0 does not support nil or empty hmac_secret')
57-
end
58-
end
5932

60-
context 'when OpenSSL raises any other error' do
61-
before do
62-
allow(OpenSSL::HMAC).to receive(:digest).and_raise(OpenSSL::HMACError.new('Another Random Error'))
63-
end
64-
65-
it 'raises the original error' do
66-
expect { subject }.to raise_error(OpenSSL::HMACError, 'Another Random Error')
67-
end
33+
it 'raises JWT::DecodeError' do
34+
expect { subject }.to raise_error(JWT::DecodeError, 'HMAC key cannot be empty')
6835
end
6936

70-
context 'when other versions of openssl do not raise an exception' do
71-
let(:response) { Base64.decode64("Q7DO+ZJl+eNMEOqdNQGSbSezn1fG1nRWHYuiNueoGfs=\n") }
72-
before do
73-
allow(OpenSSL::HMAC).to receive(:digest).and_return(response)
74-
end
75-
76-
it { is_expected.to eql(response) }
37+
it 'does not call OpenSSL::HMAC.digest' do
38+
expect(OpenSSL::HMAC).not_to receive(:digest)
39+
expect { subject }.to raise_error(JWT::DecodeError)
7740
end
7841
end
7942

@@ -160,6 +123,27 @@
160123
end
161124
end
162125

126+
# GHSA-c32j-vqhx-rx3x
127+
context 'when verification_key is nil' do
128+
let(:signature) { valid_signature }
129+
let(:hmac_secret) { nil }
130+
131+
it 'raises error and does not call OpenSSL::HMAC.digest' do
132+
expect(OpenSSL::HMAC).not_to receive(:digest)
133+
expect { subject }.to raise_error(JWT::DecodeError, 'HMAC key expected to be a String')
134+
end
135+
end
136+
137+
context 'when verification_key is empty' do
138+
let(:signature) { valid_signature }
139+
let(:hmac_secret) { '' }
140+
141+
it 'raises error and does not call OpenSSL::HMAC.digest' do
142+
expect(OpenSSL::HMAC).not_to receive(:digest)
143+
expect { subject }.to raise_error(JWT::DecodeError, 'HMAC key cannot be empty')
144+
end
145+
end
146+
163147
context 'when enforce_hmac_key_length is enabled' do
164148
before do
165149
JWT.configuration.decode.enforce_hmac_key_length = true

spec/jwt/jwt_spec.rb

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -602,20 +602,6 @@
602602
end
603603
end
604604

605-
context 'when hmac algorithm is used without secret key' do
606-
it 'encodes payload' do
607-
pending 'Different behaviour on OpenSSL 3.0 (https://github.com/openssl/openssl/issues/13089)' if JWT.openssl_3_hmac_empty_key_regression?
608-
payload = { a: 1, b: 'b' }
609-
610-
token = JWT.encode(payload, '', 'HS256')
611-
612-
expect do
613-
token_without_secret = JWT.encode(payload, nil, 'HS256')
614-
expect(token).to eq(token_without_secret)
615-
end.not_to raise_error
616-
end
617-
end
618-
619605
context 'algorithm case insensitivity' do
620606
let(:payload) { { 'a' => 1, 'b' => 'b' } }
621607

@@ -732,14 +718,6 @@
732718
end
733719
end
734720

735-
describe 'when token signed with nil and decoded with nil' do
736-
let(:no_key_token) { JWT.encode(payload, nil, 'HS512') }
737-
it 'raises JWT::DecodeError' do
738-
pending 'Different behaviour on OpenSSL 3.0 (https://github.com/openssl/openssl/issues/13089)' if JWT.openssl_3_hmac_empty_key_regression?
739-
expect { JWT.decode(no_key_token, nil, true, algorithms: 'HS512') }.to raise_error(JWT::DecodeError, 'No verification key available')
740-
end
741-
end
742-
743721
context 'when token ends with a newline char' do
744722
let(:token) { "#{JWT.encode(payload, 'secret', 'HS256')}\n" }
745723
it 'raises an error' do

0 commit comments

Comments
 (0)