Skip to content

Commit 7231ef5

Browse files
authored
Avoid using the same digest across calls (#697)
* Avoid using the same digest across calls JWT appears to reuse these JWA instances across threads, which can lead to them stepping on each other via the shared OpenSSL::Digest instance. This causes decoding to fail verification, likely because the digest contains an amalgam of data from the different threads. This patch creates a new OpenSSL::Digest for each use, avoiding the threading issue. Note that the HMAC JWA already calls OpenSSL::HMAC.digest, avoiding the shared state, and the others do not use digest. The original code does not fail on CRuby most likely because only one thread at a time can be calculating a digest against a given OpenSSL::Digest instance, due to the VM lock. Fixes #696 Addresses the issue reported in jruby/jruby#8504 by @mohamedhafez * Add #697 to changelog * Modify Rsa digest name test for new structure The @digest instance variable now contains the name to the digest to be used. See #697 * Add test for concurrent encode/decode using ECDSA This is adapted from the script in #696 and provides a test for the ECDSA part of the fix in #697. * Fixes for Rubocop
1 parent 9524614 commit 7231ef5

File tree

5 files changed

+37
-8
lines changed

5 files changed

+37
-8
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
**Fixes and enhancements:**
1212

13-
- Your contribution here
13+
- Avoid using the same digest across calls in JWT::JWA::Ecdsa and JWT::JWA::Rsa [#697](https://github.com/jwt/ruby-jwt/pull/697)
1414

1515
## [v3.1.1](https://github.com/jwt/ruby-jwt/tree/v3.1.1) (2025-06-24)
1616

lib/jwt/jwa/ecdsa.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ class Ecdsa
88

99
def initialize(alg, digest)
1010
@alg = alg
11-
@digest = OpenSSL::Digest.new(digest)
11+
@digest = digest
1212
end
1313

1414
def sign(data:, signing_key:)
@@ -20,7 +20,7 @@ def sign(data:, signing_key:)
2020

2121
raise IncorrectAlgorithm, "payload algorithm is #{alg} but #{key_algorithm} signing key was provided" if alg != key_algorithm
2222

23-
asn1_to_raw(signing_key.dsa_sign_asn1(digest.digest(data)), signing_key)
23+
asn1_to_raw(signing_key.dsa_sign_asn1(OpenSSL::Digest.new(digest).digest(data)), signing_key)
2424
end
2525

2626
def verify(data:, signature:, verification_key:)
@@ -32,7 +32,7 @@ def verify(data:, signature:, verification_key:)
3232
key_algorithm = curve_definition[:algorithm]
3333
raise IncorrectAlgorithm, "payload algorithm is #{alg} but #{key_algorithm} verification key was provided" if alg != key_algorithm
3434

35-
verification_key.dsa_verify_asn1(digest.digest(data), raw_to_asn1(signature, verification_key))
35+
verification_key.dsa_verify_asn1(OpenSSL::Digest.new(digest).digest(data), raw_to_asn1(signature, verification_key))
3636
rescue OpenSSL::PKey::PKeyError
3737
raise JWT::VerificationError, 'Signature verification raised'
3838
end

lib/jwt/jwa/rsa.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,18 @@ class Rsa
88

99
def initialize(alg)
1010
@alg = alg
11-
@digest = OpenSSL::Digest.new(alg.sub('RS', 'SHA'))
11+
@digest = alg.sub('RS', 'SHA')
1212
end
1313

1414
def sign(data:, signing_key:)
1515
raise_sign_error!("The given key is a #{signing_key.class}. It has to be an OpenSSL::PKey::RSA instance") unless signing_key.is_a?(OpenSSL::PKey::RSA)
1616
raise_sign_error!('The key length must be greater than or equal to 2048 bits') if signing_key.n.num_bits < 2048
1717

18-
signing_key.sign(digest, data)
18+
signing_key.sign(OpenSSL::Digest.new(digest), data)
1919
end
2020

2121
def verify(data:, signature:, verification_key:)
22-
verification_key.verify(digest, signature, data)
22+
verification_key.verify(OpenSSL::Digest.new(digest), signature, data)
2323
rescue OpenSSL::PKey::PKeyError
2424
raise JWT::VerificationError, 'Signature verification raised'
2525
end
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe JWT::JWA::Ecdsa do
4+
context 'used across threads for encoding and decoding' do
5+
it 'successfully encodes, decodes, and verifies' do
6+
threads = 10.times.map do
7+
Thread.new do
8+
public_key_pem = "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcKuFOqoNEN+TXylz4MVAWREa9yA8\npOF9QgGchnAy6Ad4P7yCpk+R3wCGTDLfNboYqUmbK5Hd9uHszf+EMTi22g==\n-----END PUBLIC KEY-----\n"
9+
private_key_pem = "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgiF/iNuQem/yQyd16\nc9shf2Y9vMycOU7g6W6LTmkyj1ehRANCAARwq4U6qg0Q35NfKXPgxUBZERr3IDyk\n4X1CAZyGcDLoB3g/vIKmT5HfAIZMMt81uhipSZsrkd324ezN/4QxOLba\n-----END PRIVATE KEY-----\n"
10+
full_pem = private_key_pem + public_key_pem
11+
curve = OpenSSL::PKey.read(full_pem)
12+
public_key = OpenSSL::PKey::EC.new(public_key_pem)
13+
14+
10.times do
15+
input_payload = { 'aud' => 'https://fcm.googleapis.com', 'exp' => (Time.now.to_i + 600), 'sub' => 'mailto:example@example.com' }
16+
input_header = { 'typ' => 'JWT', 'alg' => 'ES256' }
17+
token = JWT.encode(input_payload, curve, 'ES256', input_header)
18+
19+
output_payload, output_header = JWT.decode(token, public_key, true, { algorithm: 'ES256', verify_expiration: true })
20+
expect(output_payload).to eq input_payload
21+
expect(output_header).to eq input_header
22+
end
23+
end
24+
end
25+
26+
threads.each(&:join)
27+
end
28+
end
29+
end

spec/jwt/jwa/rsa_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
describe '#initialize' do
99
it 'initializes with the correct algorithm and digest' do
1010
expect(rsa_instance.instance_variable_get(:@alg)).to eq('RS256')
11-
expect(rsa_instance.send(:digest).name).to eq('SHA256')
11+
expect(rsa_instance.send(:digest)).to eq('SHA256')
1212
end
1313
end
1414

0 commit comments

Comments
 (0)