Skip to content

Concurrency issue when encoding/decoding across threads #696

Description

@headius

There is a concurrency issue when encoding and decoding across many threads.

The report below was against JRuby, but I confirmed it also happens on TruffleRuby, another parallel-threaded Ruby implementation. The fact that it fails on both of those indicated to me that it was not an issue in our respective openssl library implementations, but instead an issue in JWT.

It does not appear to affect CRuby.

I have a fix in progress.

Originally reported by @mohamedhafez to JRuby at jruby/jruby#8504.

The following script works just fine with jwt version 2.8.2 on CRuby 3.1.6 and JRuby 9.4.9.0 with jruby-openssl version 0.15.2. However if I upgrade jwt to 2.9.3, it will fail with JWT::VerificationError: Signature verification failed message in JRuby, while it continues to run just fine on C-Ruby.

require 'jwt'
require 'openssl'

puts "JWT version: #{JWT::VERSION::STRING}"
puts "OpenSSL version: #{OpenSSL::OPENSSL_VERSION}"

threads = []

10.times do
  thread = Thread.new {
    #generated on C-Ruby 3.1.6 with the following code. some of the methods not available in JRuby so had to generate offline
    #curve = OpenSSL::PKey::EC.generate('prime256v1')
    #curve.public_to_pem
    #curve.private_to_pem
    public_key_pem = "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcKuFOqoNEN+TXylz4MVAWREa9yA8\npOF9QgGchnAy6Ad4P7yCpk+R3wCGTDLfNboYqUmbK5Hd9uHszf+EMTi22g==\n-----END PUBLIC KEY-----\n"
    private_key_pem = "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgiF/iNuQem/yQyd16\nc9shf2Y9vMycOU7g6W6LTmkyj1ehRANCAARwq4U6qg0Q35NfKXPgxUBZERr3IDyk\n4X1CAZyGcDLoB3g/vIKmT5HfAIZMMt81uhipSZsrkd324ezN/4QxOLba\n-----END PRIVATE KEY-----\n"
    full_pem = private_key_pem + public_key_pem
    curve = OpenSSL::PKey.read(full_pem)
    public_key = OpenSSL::PKey::EC.new(public_key_pem)

    loop do
      input_payload = {"aud" => "https://fcm.googleapis.com", "exp" => (Time.now.to_i + 600), "sub" => "mailto:example@example.com"}
      input_header = { "typ" => "JWT", "alg" => "ES256" }
      token = JWT.encode(input_payload, curve, 'ES256', input_header)

      output_payload, output_header = JWT.decode(token, public_key, true, { algorithm: 'ES256', verify_expiration: true })
      raise "output doesnt match input!" unless output_payload == input_payload && output_header == input_header
    end
  }
  thread.abort_on_exception = true
  threads << thread
end

loop do
  sleep 10
  puts "all threads still alive" if threads.all?(&:alive?)
end

The stack trace:

JWT::VerificationError: Signature verification failed
  verify_signature at /Users/mohamed/.rbenv/versions/jruby-9.4.9.0/lib/ruby/gems/shared/gems/jwt-2.9.3/lib/jwt/decode.rb:47
   decode_segments at /Users/mohamed/.rbenv/versions/jruby-9.4.9.0/lib/ruby/gems/shared/gems/jwt-2.9.3/lib/jwt/decode.rb:28
            decode at /Users/mohamed/.rbenv/versions/jruby-9.4.9.0/lib/ruby/gems/shared/gems/jwt-2.9.3/lib/jwt.rb:35
           context at /Users/mohamed/.rbenv/versions/jruby-9.4.9.0/lib/ruby/gems/shared/gems/jwt-2.9.3/lib/jwt/deprecations.rb:8
            decode at /Users/mohamed/.rbenv/versions/jruby-9.4.9.0/lib/ruby/gems/shared/gems/jwt-2.9.3/lib/jwt.rb:34
            <main> at /Users/mohamed/Library/Application Support/JetBrains/RubyMine2024.3/scratches/scratch.rb:26
              loop at org/jruby/RubyKernel.java:1725
            <main> at /Users/mohamed/Library/Application Support/JetBrains/RubyMine2024.3/scratches/scratch.rb:21

This only happens when running multiple threads all calling JWT.encode, if you edit the script above to only have 1 thread, then it'll run for millions of iterations without issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions