Skip to content

Commit 67dc9d3

Browse files
anakinjheadius
andauthored
Backport: Avoid using the same digest across calls (#697) (#701)
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 Co-authored-by: Charles Oliver Nutter <headius@headius.com>
1 parent c73c286 commit 67dc9d3

File tree

4 files changed

+36
-7
lines changed

4 files changed

+36
-7
lines changed

lib/jwt/jwa/ecdsa.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,23 @@ 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:)
1515
curve_definition = curve_by_name(signing_key.group.curve_name)
1616
key_algorithm = curve_definition[:algorithm]
1717
raise IncorrectAlgorithm, "payload algorithm is #{alg} but #{key_algorithm} signing key was provided" if alg != key_algorithm
1818

19-
asn1_to_raw(signing_key.dsa_sign_asn1(digest.digest(data)), signing_key)
19+
asn1_to_raw(signing_key.dsa_sign_asn1(OpenSSL::Digest.new(digest).digest(data)), signing_key)
2020
end
2121

2222
def verify(data:, signature:, verification_key:)
2323
curve_definition = curve_by_name(verification_key.group.curve_name)
2424
key_algorithm = curve_definition[:algorithm]
2525
raise IncorrectAlgorithm, "payload algorithm is #{alg} but #{key_algorithm} verification key was provided" if alg != key_algorithm
2626

27-
verification_key.dsa_verify_asn1(digest.digest(data), raw_to_asn1(signature, verification_key))
27+
verification_key.dsa_verify_asn1(OpenSSL::Digest.new(digest).digest(data), raw_to_asn1(signature, verification_key))
2828
rescue OpenSSL::PKey::PKeyError
2929
raise JWT::VerificationError, 'Signature verification raised'
3030
end

lib/jwt/jwa/rsa.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,17 @@ 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

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

2020
def verify(data:, signature:, verification_key:)
21-
verification_key.verify(digest, signature, data)
21+
verification_key.verify(OpenSSL::Digest.new(digest), signature, data)
2222
rescue OpenSSL::PKey::PKeyError
2323
raise JWT::VerificationError, 'Signature verification raised'
2424
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)