From dae4ec06d06604587ed56d9cd5d5a87491e70d69 Mon Sep 17 00:00:00 2001 From: Oskar Pearson Date: Tue, 28 Jan 2025 17:34:50 +0000 Subject: [PATCH] Add support for ruby-jwt v2.6.0 and above, bump development deps, new Ruby vers - Remove reference to 'secure_compare' method in ruby-jwt that this code shouldn't be using, so as to support ruby-jwt v2.6.0 and above - Add support for ruby-jwt up to and including version 3.* (currently in beta) so that we don't have to bump support again later - Add recent Ruby versions to the test matrix (I left version 3.0 in place, although it's no longer officially supported, so as to avoid inconveniencing existing users that are stuck on version 3.0) Prior versions of this code depended on the method `JWT::SecurityUtils.secure_compare` Release v2.6.0 of ruby-jwt moved the `JWT::SecurityUtils.secure_compare` method to a different namespace as part of https://github.com/jwt/ruby-jwt/pull/521 While we could simply change this code to instead refer to that new namespace, I believe that the intention of ruby-jwt was that `secure_compare` should not have been used outside of the internals of ruby-jwt. This is supported by the [The ruby-jwt README examples](https://github.com/jwt/ruby-jwt/blob/v3.0.0.beta1/README.md#custom-algorithms) where the ruby-jwt team recommend using `OpenSSL.fixed_length_secure_compare` instead of `JWT::SecurityUtils.secure_compare` The code in this change is based on the logic in https://github.com/rails/rails/blob/cf6ff17e9a3c6c1139040b519a341f55f0be16cf/activesupport/lib/active_support/security_utils.rb#L33 so as to avoid adding a dependency on ActiveSupport for this single method. Unlike the code in the activesupport url above we don't fall back to a custom implementation of `fixed_length_secure_compare`, since `OpenSSL.fixed_length_secure_compare` [is present in OpenSSL 2.2](https://github.com/ruby/openssl/blob/master/History.md#version-220) and this gem already depends on Ruby 3.0 and above, which already [includes that version of OpenSSL](https://stdgems.org/openssl/) This code also doesn't need to handle nil/empty cases, unlike the [original implementation of JWT::SecurityUtils.secure_compare](https://github.com/jwt/ruby-jwt/pull/15/files#diff-2961689829b1ae20b5e8222673335c323355389f753c50b8a5b98380086f63b9R98) because these are already handled in the call to `validate_url` in one case, and `validate_payload` itself in the other. --- .github/workflows/test.yml | 4 ++-- lib/messagebird/request_validator.rb | 14 ++++++++++++-- messagebird-rest.gemspec | 5 ++++- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ad79fa1..75cdd09 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,8 +11,8 @@ jobs: strategy: matrix: - ruby-version: [ "3.0.3", "3.1.1" ] - + ruby-version: [ "3.0.7", "3.1.6", "3.2.6", "3.3.7", "3.4.1" ] + steps: - uses: actions/checkout@v2 diff --git a/lib/messagebird/request_validator.rb b/lib/messagebird/request_validator.rb index e1955a2..06ab2aa 100644 --- a/lib/messagebird/request_validator.rb +++ b/lib/messagebird/request_validator.rb @@ -71,14 +71,14 @@ def decode_signature(signature) def validate_url(url, url_hash) expected_url_hash = Digest::SHA256.hexdigest url - unless JWT::SecurityUtils.secure_compare(expected_url_hash, url_hash) + unless secure_compare(expected_url_hash, url_hash) raise ValidationError, 'invalid jwt: claim url_hash is invalid' end end def validate_payload(body, payload_hash) if !body.to_s.empty? && !payload_hash.to_s.empty? - unless JWT::SecurityUtils.secure_compare(Digest::SHA256.hexdigest(body), payload_hash) + unless secure_compare(Digest::SHA256.hexdigest(body), payload_hash) raise ValidationError, 'invalid jwt: claim payload_hash is invalid' end elsif !body.to_s.empty? @@ -87,5 +87,15 @@ def validate_payload(body, payload_hash) raise ValidationError, 'invalid jwt: claim payload_hash is set but actual payload is missing' end end + + # Adaption of https://github.com/rails/rails/blob/cf6ff17e9a3c6c1139040b519a341f55f0be16cf/activesupport/lib/active_support/security_utils.rb#L33 + # Copied here so as to avoid adding a dependency on ActiveSupport to this gem + # + # Note that unlike `fixed_length_secure_compare` in the above url we don't fall back to a custom implementation + # of fixed_length_secure_compare, since OpenSSL.fixed_length_secure_compare is present in OpenSSL 2.2 + # https://github.com/ruby/openssl/blob/master/History.md#version-220 which is included in Ruby 3.0 and above + def secure_compare(first, second) + first.bytesize == second.bytesize && OpenSSL.fixed_length_secure_compare(first, second) + end end end diff --git a/messagebird-rest.gemspec b/messagebird-rest.gemspec index 2d8ea89..3df280d 100644 --- a/messagebird-rest.gemspec +++ b/messagebird-rest.gemspec @@ -23,7 +23,10 @@ Gem::Specification.new do |s| s.files = Dir.glob('lib/**/*') + %w(LICENSE README.md) s.require_path = 'lib' - s.add_dependency "jwt", "~> 2.3" + # This code works with at least version 3.0.0.beta1 of jwt, + # so we are supporting up to version 4 to help reduce + # the necessity for future version bumps + s.add_dependency "jwt", "< 4" s.add_development_dependency "rspec", "~> 3.11.0" s.add_development_dependency "rubocop", "~> 1.26.1"