From 7ff5f070ce696ed31d361238fda221d429786187 Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Sat, 28 Dec 2024 17:45:44 +0200 Subject: [PATCH 1/4] Fix deprecation messages --- lib/jwt/jwa/eddsa.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/jwt/jwa/eddsa.rb b/lib/jwt/jwa/eddsa.rb index a1d21fbf0..441286c84 100644 --- a/lib/jwt/jwa/eddsa.rb +++ b/lib/jwt/jwa/eddsa.rb @@ -13,7 +13,7 @@ def initialize(alg) def sign(data:, signing_key:) raise_sign_error!("Key given is a #{signing_key.class} but has to be an RbNaCl::Signatures::Ed25519::SigningKey") unless signing_key.is_a?(RbNaCl::Signatures::Ed25519::SigningKey) - Deprecations.warning('Using Ed25519 keys is deprecated and will be removed in a future version of ruby-jwt. Please use the ruby-eddsa gem instead.') + Deprecations.warning('Using the EdDSA algorithm is deprecated and will be removed in a future version of ruby-jwt. In the future the algorithm will be provided by the jwt-eddsa gem.') signing_key.sign(data) end @@ -21,7 +21,7 @@ def sign(data:, signing_key:) def verify(data:, signature:, verification_key:) raise_verify_error!("key given is a #{verification_key.class} but has to be a RbNaCl::Signatures::Ed25519::VerifyKey") unless verification_key.is_a?(RbNaCl::Signatures::Ed25519::VerifyKey) - Deprecations.warning('Using Ed25519 keys is deprecated and will be removed in a future version of ruby-jwt. Please use the ruby-eddsa gem instead.') + Deprecations.warning('Using the EdDSA algorithm is deprecated and will be removed in a future version of ruby-jwt. In the future the algorithm will be provided by the jwt-eddsa gem.') verification_key.verify(signature, data) rescue RbNaCl::CryptoError From c73c286901b88bd7c73ec72c5154da74b8533ba1 Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Sun, 29 Jun 2025 12:01:18 +0300 Subject: [PATCH 2/4] Simplify CI on 2.10 branch (#702) * Remove coverage steps * Use ubuntu latest * Older openssl on selected Ruby versions * Logger was needed --- .github/workflows/test.yml | 48 +++++++++++--------------------------- ruby-jwt.gemspec | 1 + 2 files changed, 14 insertions(+), 35 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dac652402..be3742509 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,7 +28,7 @@ jobs: fail-fast: false matrix: os: - - ubuntu-20.04 + - ubuntu-latest ruby: - "2.5" - "2.6" @@ -37,24 +37,28 @@ jobs: - "3.1" - "3.2" - "3.3" + - "3.4" gemfile: - gemfiles/standalone.gemfile - - gemfiles/openssl.gemfile - gemfiles/rbnacl.gemfile - gemfiles/rbnacl_pre_6.gemfile experimental: [false] include: - - os: ubuntu-22.04 - ruby: "3.1" - gemfile: 'gemfiles/standalone.gemfile' + - os: ubuntu-latest + ruby: "2.5" + gemfile: "gemfiles/openssl.gemfile" experimental: false - - os: ubuntu-20.04 + - os: ubuntu-latest + ruby: "3.0" + gemfile: "gemfiles/openssl.gemfile" + experimental: false + - os: ubuntu-latest ruby: "truffleruby-head" - gemfile: 'gemfiles/standalone.gemfile' + gemfile: "gemfiles/standalone.gemfile" experimental: true - - os: ubuntu-22.04 + - os: ubuntu-latest ruby: "head" - gemfile: 'gemfiles/standalone.gemfile' + gemfile: "gemfiles/standalone.gemfile" experimental: true continue-on-error: ${{ matrix.experimental }} env: @@ -77,32 +81,6 @@ jobs: - name: Run tests run: bundle exec rspec - - name: Upload test coverage folder for later reporting - uses: actions/upload-artifact@v3 - with: - name: coverage-reports - path: ${{github.workspace}}/coverage-*/coverage.json - retention-days: 1 - - coverage: - name: Report coverage to Code Climate - runs-on: ubuntu-20.04 - needs: test - if: success() && github.ref == 'refs/heads/main' - env: - CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} - steps: - - uses: actions/checkout@v3 - - - name: Download coverage reports from the test job - uses: actions/download-artifact@v3 - with: - name: coverage-reports - - - uses: paambaati/codeclimate-action@v3.2.0 - with: - coverageLocations: "coverage-*/coverage.json:simplecov" - smoke: name: Built GEM smoke test timeout-minutes: 30 diff --git a/ruby-jwt.gemspec b/ruby-jwt.gemspec index 67ff39116..7a4963ed1 100644 --- a/ruby-jwt.gemspec +++ b/ruby-jwt.gemspec @@ -35,6 +35,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'appraisal' spec.add_development_dependency 'bundler' + spec.add_development_dependency 'logger' spec.add_development_dependency 'rake' spec.add_development_dependency 'rspec' spec.add_development_dependency 'rubocop' From 67dc9d344ece2c18ff1f25621e10f5a692503191 Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Sun, 29 Jun 2025 12:06:41 +0300 Subject: [PATCH 3/4] 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 jwt/ruby-jwt#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 --- lib/jwt/jwa/ecdsa.rb | 6 ++--- lib/jwt/jwa/rsa.rb | 6 ++--- spec/jwt/concurrent_encode_decode_spec.rb | 29 +++++++++++++++++++++++ spec/jwt/jwa/rsa_spec.rb | 2 +- 4 files changed, 36 insertions(+), 7 deletions(-) create mode 100644 spec/jwt/concurrent_encode_decode_spec.rb diff --git a/lib/jwt/jwa/ecdsa.rb b/lib/jwt/jwa/ecdsa.rb index 9876ecca6..8f5dc4349 100644 --- a/lib/jwt/jwa/ecdsa.rb +++ b/lib/jwt/jwa/ecdsa.rb @@ -8,7 +8,7 @@ class Ecdsa def initialize(alg, digest) @alg = alg - @digest = OpenSSL::Digest.new(digest) + @digest = digest end def sign(data:, signing_key:) @@ -16,7 +16,7 @@ def sign(data:, signing_key:) key_algorithm = curve_definition[:algorithm] raise IncorrectAlgorithm, "payload algorithm is #{alg} but #{key_algorithm} signing key was provided" if alg != key_algorithm - asn1_to_raw(signing_key.dsa_sign_asn1(digest.digest(data)), signing_key) + asn1_to_raw(signing_key.dsa_sign_asn1(OpenSSL::Digest.new(digest).digest(data)), signing_key) end def verify(data:, signature:, verification_key:) @@ -24,7 +24,7 @@ def verify(data:, signature:, verification_key:) key_algorithm = curve_definition[:algorithm] raise IncorrectAlgorithm, "payload algorithm is #{alg} but #{key_algorithm} verification key was provided" if alg != key_algorithm - verification_key.dsa_verify_asn1(digest.digest(data), raw_to_asn1(signature, verification_key)) + verification_key.dsa_verify_asn1(OpenSSL::Digest.new(digest).digest(data), raw_to_asn1(signature, verification_key)) rescue OpenSSL::PKey::PKeyError raise JWT::VerificationError, 'Signature verification raised' end diff --git a/lib/jwt/jwa/rsa.rb b/lib/jwt/jwa/rsa.rb index d9ab925f0..7aedd91b5 100644 --- a/lib/jwt/jwa/rsa.rb +++ b/lib/jwt/jwa/rsa.rb @@ -8,17 +8,17 @@ class Rsa def initialize(alg) @alg = alg - @digest = OpenSSL::Digest.new(alg.sub('RS', 'SHA')) + @digest = alg.sub('RS', 'SHA') end def sign(data:, signing_key:) 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) - signing_key.sign(digest, data) + signing_key.sign(OpenSSL::Digest.new(digest), data) end def verify(data:, signature:, verification_key:) - verification_key.verify(digest, signature, data) + verification_key.verify(OpenSSL::Digest.new(digest), signature, data) rescue OpenSSL::PKey::PKeyError raise JWT::VerificationError, 'Signature verification raised' end diff --git a/spec/jwt/concurrent_encode_decode_spec.rb b/spec/jwt/concurrent_encode_decode_spec.rb new file mode 100644 index 000000000..07809aa1e --- /dev/null +++ b/spec/jwt/concurrent_encode_decode_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +RSpec.describe JWT::JWA::Ecdsa do + context 'used across threads for encoding and decoding' do + it 'successfully encodes, decodes, and verifies' do + threads = 10.times.map do + Thread.new do + 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) + + 10.times 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 }) + expect(output_payload).to eq input_payload + expect(output_header).to eq input_header + end + end + end + + threads.each(&:join) + end + end +end diff --git a/spec/jwt/jwa/rsa_spec.rb b/spec/jwt/jwa/rsa_spec.rb index 758bd5d50..332fa08cf 100644 --- a/spec/jwt/jwa/rsa_spec.rb +++ b/spec/jwt/jwa/rsa_spec.rb @@ -8,7 +8,7 @@ describe '#initialize' do it 'initializes with the correct algorithm and digest' do expect(rsa_instance.instance_variable_get(:@alg)).to eq('RS256') - expect(rsa_instance.send(:digest).name).to eq('SHA256') + expect(rsa_instance.send(:digest)).to eq('SHA256') end end From 658275c3f20156df0656cf25d3e2129fa0fd2322 Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Sun, 29 Jun 2025 12:13:33 +0300 Subject: [PATCH 4/4] Version 2.10.2 (#703) --- CHANGELOG.md | 10 ++++++++++ lib/jwt/version.rb | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc8dbcb0b..8318e9e1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,17 @@ # Changelog +## [v2.10.2](https://github.com/jwt/ruby-jwt/tree/v2.10.2) (2025-06-29) + +[Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.10.1...v2.10.2) + +**Fixes and enhancements:** + +- Avoid using the same digest across calls in JWT::JWA::Ecdsa and JWT::JWA::Rsa [#697](https://github.com/jwt/ruby-jwt/pull/697) + ## [v2.10.1](https://github.com/jwt/ruby-jwt/tree/v2.10.1) (2024-12-26) +[Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.10.0...v2.10.1) + **Fixes and enhancements:** - Make version constants public again [#646](https://github.com/jwt/ruby-jwt/pull/646) ([@anakinj] diff --git a/lib/jwt/version.rb b/lib/jwt/version.rb index 37309a972..6222b9680 100644 --- a/lib/jwt/version.rb +++ b/lib/jwt/version.rb @@ -16,7 +16,7 @@ def self.gem_version module VERSION MAJOR = 2 MINOR = 10 - TINY = 1 + TINY = 2 PRE = nil STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')