Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package rubygem-jwt for openSUSE:Factory checked in at 2022-09-03 23:18:48 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/rubygem-jwt (Old) and /work/SRC/openSUSE:Factory/.rubygem-jwt.new.2083 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "rubygem-jwt" Sat Sep 3 23:18:48 2022 rev:7 rq:1000911 version:2.5.0 Changes: -------- --- /work/SRC/openSUSE:Factory/rubygem-jwt/rubygem-jwt.changes 2022-08-09 15:26:52.693372463 +0200 +++ /work/SRC/openSUSE:Factory/.rubygem-jwt.new.2083/rubygem-jwt.changes 2022-09-03 23:18:58.659799673 +0200 @@ -1,0 +2,7 @@ +Mon Aug 29 06:52:15 UTC 2022 - Stephan Kulow <[email protected]> + +updated to version 2.5.0 + see installed CHANGELOG.md + + +------------------------------------------------------------------- Old: ---- jwt-2.4.1.gem New: ---- jwt-2.5.0.gem ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ rubygem-jwt.spec ++++++ --- /var/tmp/diff_new_pack.mFvqpE/_old 2022-09-03 23:19:00.287803950 +0200 +++ /var/tmp/diff_new_pack.mFvqpE/_new 2022-09-03 23:19:00.295803971 +0200 @@ -24,7 +24,7 @@ # Name: rubygem-jwt -Version: 2.4.1 +Version: 2.5.0 Release: 0 %define mod_name jwt %define mod_full_name %{mod_name}-%{version} ++++++ jwt-2.4.1.gem -> jwt-2.5.0.gem ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/.github/workflows/test.yml new/.github/workflows/test.yml --- old/.github/workflows/test.yml 2022-06-07 21:53:24.000000000 +0200 +++ new/.github/workflows/test.yml 2022-08-25 21:56:49.000000000 +0200 @@ -13,7 +13,7 @@ timeout-minutes: 30 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: @@ -22,34 +22,35 @@ - name: Run RuboCop run: bundle exec rubocop test: + name: ${{ matrix.os }} - Ruby ${{ matrix.ruby }} + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: + os: + - ubuntu-20.04 ruby: - - 2.5 - - 2.6 - - 2.7 + - "2.5" + - "2.6" + - "2.7" - "3.0" - - 3.1 + - "3.1" gemfile: - gemfiles/standalone.gemfile - gemfiles/openssl.gemfile - gemfiles/rbnacl.gemfile experimental: [false] include: - - ruby: 2.7 - gemfile: 'gemfiles/rbnacl.gemfile' - - ruby: "ruby-head" - experimental: true - - ruby: "truffleruby-head" - experimental: true - runs-on: ubuntu-20.04 + - { os: ubuntu-20.04, ruby: "2.7", gemfile: 'gemfiles/rbnacl.gemfile', experimental: false } + - { os: ubuntu-22.04, ruby: "3.1", experimental: false } + - { os: ubuntu-20.04, ruby: "truffleruby-head", experimental: true } + - { os: ubuntu-22.04, ruby: "head", experimental: true } continue-on-error: ${{ matrix.experimental }} env: BUNDLE_GEMFILE: ${{ matrix.gemfile }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install libsodium run: | diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/.rubocop.yml new/.rubocop.yml --- old/.rubocop.yml 2022-06-07 21:53:24.000000000 +0200 +++ new/.rubocop.yml 2022-08-25 21:56:49.000000000 +0200 @@ -29,7 +29,7 @@ Max: 25 Metrics/ClassLength: - Max: 105 + Max: 112 Metrics/ModuleLength: Max: 100 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/CHANGELOG.md new/CHANGELOG.md --- old/CHANGELOG.md 2022-06-07 21:53:24.000000000 +0200 +++ new/CHANGELOG.md 2022-08-25 21:56:49.000000000 +0200 @@ -1,11 +1,30 @@ # Changelog -## [v2.4.1](https://github.com/jwt/ruby-jwt/tree/v2.4.1) (2022-06-07) + + +## [v2.5.0](https://github.com/jwt/ruby-jwt/tree/v2.5.0) (NEXT) + +[Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.4.1...master) + +**Features:** + +- Support JWK thumbprints as key ids [#481](https://github.com/jwt/ruby-jwt/pull/481) ([@anakinj](https://github.com/anakinj)). +- Your contribution here **Fixes and enhancements:** -- Raise JWT::DecodeError on invalid signature [\#484](https://github.com/jwt/ruby-jwt/pull/484) ([@freakyfelt!](https://github.com/freakyfelt!)). +- Bring back the old Base64 (RFC2045) deocode mechanisms [#488](https://github.com/jwt/ruby-jwt/pull/488) ([@anakinj](https://github.com/anakinj)). +- Rescue RbNaCl exception for EdDSA wrong key [#491](https://github.com/jwt/ruby-jwt/pull/491) ([@n-studio](https://github.com/n-studio)). +- New parameter name for cases when kid is not found using JWK key loader proc [#501](https://github.com/jwt/ruby-jwt/pull/501) ([@anakinj](https://github.com/anakinj)). +- Fix NoMethodError when a 2 segment token is missing 'alg' header [#502](https://github.com/jwt/ruby-jwt/pull/502) ([@cmrd-senya](https://github.com/cmrd-senya)). +- Support OpenSSL >= 3.0 [#496](https://github.com/jwt/ruby-jwt/pull/496) ([@anakinj](https://github.com/anakinj)). +- Your contribution here + +## [v2.4.1](https://github.com/jwt/ruby-jwt/tree/v2.4.1) (2022-06-07) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.4.0...v2.4.1) +**Fixes and enhancements:** +- Raise JWT::DecodeError on invalid signature [\#484](https://github.com/jwt/ruby-jwt/pull/484) ([@freakyfelt!](https://github.com/freakyfelt!)). + ## [v2.4.0](https://github.com/jwt/ruby-jwt/tree/v2.4.0) (2022-06-06) [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.3.0...v2.4.0) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/README.md new/README.md --- old/README.md 2022-06-07 21:53:24.000000000 +0200 +++ new/README.md 2022-08-25 21:56:49.000000000 +0200 @@ -12,7 +12,7 @@ If you have further questions related to development or usage, join us: [ruby-jwt google group](https://groups.google.com/forum/#!forum/ruby-jwt). ## Announcements -* Ruby 2.4 support is going to be dropped in version 2.4.0 +* Ruby 2.4 support was dropped in version 2.4.0 * Ruby 1.9.3 support was dropped at December 31st, 2016. * Version 1.5.3 yanked. See: [#132](https://github.com/jwt/ruby-jwt/issues/132) and [#133](https://github.com/jwt/ruby-jwt/issues/133) @@ -135,17 +135,14 @@ * ES256K - ECDSA using P-256K and SHA-256 ```ruby -ecdsa_key = OpenSSL::PKey::EC.new 'prime256v1' -ecdsa_key.generate_key -ecdsa_public = OpenSSL::PKey::EC.new ecdsa_key -ecdsa_public.private_key = nil +ecdsa_key = OpenSSL::PKey::EC.generate('prime256v1') token = JWT.encode payload, ecdsa_key, 'ES256' # eyJhbGciOiJFUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.AlLW--kaF7EX1NMX9WJRuIW8NeRJbn2BLXHns7Q5TZr7Hy3lF6MOpMlp7GoxBFRLISQ6KrD0CJOrR8aogEsPeg puts token -decoded_token = JWT.decode token, ecdsa_public, true, { algorithm: 'ES256' } +decoded_token = JWT.decode token, ecdsa_key, true, { algorithm: 'ES256' } # Array # [ @@ -186,7 +183,7 @@ ### **RSASSA-PSS** -In order to use this algorithm you need to add the `openssl` gem to you `Gemfile` with a version greater or equal to `2.1`. +In order to use this algorithm you need to add the `openssl` gem to your `Gemfile` with a version greater or equal to `2.1`. ```ruby gem 'openssl', '~> 2.1' @@ -546,30 +543,41 @@ ### JSON Web Key (JWK) -JWK is a JSON structure representing a cryptographic key. Currently only supports RSA, EC and HMAC keys. +JWK is a JSON structure representing a cryptographic key. Currently only supports RSA, EC and HMAC keys. The `jwks` option can be given as a lambda that evaluates every time a kid is resolved. -```ruby -jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), "optional-kid") -payload, headers = { data: 'data' }, { kid: jwk.kid } - -token = JWT.encode(payload, jwk.keypair, 'RS512', headers) +If the kid is not found from the given set the loader will be called a second time with the `kid_not_found` option set to `true`. The application can choose to implement some kind of JWK cache invalidation or other mechanism to handle such cases. -# The jwk loader would fetch the set of JWKs from a trusted source -jwk_loader = ->(options) do - @cached_keys = nil if options[:invalidate] # need to reload the keys - @cached_keys ||= { keys: [jwk.export] } -end +```ruby + jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), 'optional-kid') + payload = { data: 'data' } + headers = { kid: jwk.kid } + + token = JWT.encode(payload, jwk.keypair, 'RS512', headers) + + # The jwk loader would fetch the set of JWKs from a trusted source, + # to avoid malicious requests triggering cache invalidations there needs to be some kind of grace time or other logic for determining the validity of the invalidation. + # This example only allows cache invalidations every 5 minutes. + jwk_loader = ->(options) do + if options[:kid_not_found] && @cache_last_update < Time.now.to_i - 300 + logger.info("Invalidating JWK cache. #{options[:kid]} not found from previous cache") + @cached_keys = nil + end + @cached_keys ||= begin + @cache_last_update = Time.now.to_i + { keys: [jwk.export] } + end + end -begin - JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwk_loader}) -rescue JWT::JWKError - # Handle problems with the provided JWKs -rescue JWT::DecodeError - # Handle other decode related issues e.g. no kid in header, no matching public key found etc. -end + begin + JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwk_loader }) + rescue JWT::JWKError + # Handle problems with the provided JWKs + rescue JWT::DecodeError + # Handle other decode related issues e.g. no kid in header, no matching public key found etc. + end ``` -or by passing JWK as a simple Hash +or by passing the JWKs as a simple Hash ``` jwks = { keys: [{ ... }] } # keys accepts both of string and symbol @@ -587,8 +595,39 @@ jwk_hash_with_private_key = jwk.export(include_private: true) ``` -## How to contribute +### Key ID (kid) and JWKs + +The key id (kid) generation in the gem is a custom algorithm and not based on any standards. To use a standardized JWK thumbprint (RFC 7638) as the kid for JWKs a generator type can be specified in the global configuration or can be given to the JWK instance on initialization. + +```ruby +JWT.configuration.jwk.kid_generator_type = :rfc7638_thumbprint +# OR +JWT.configuration.jwk.kid_generator = ::JWT::JWK::Thumbprint +# OR +jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), kid_generator: ::JWT::JWK::Thumbprint) + +jwk_hash = jwk.export + +thumbprint_as_the_kid = jwk_hash[:kid] + +``` + +# Development and Tests + +We depend on [Bundler](http://rubygems.org/gems/bundler) for defining gemspec and performing releases to rubygems.org, which can be done with + +```bash +rake release +``` +The tests are written with rspec. [Appraisal](https://github.com/thoughtbot/appraisal) is used to ensure compatibility with 3rd party dependencies providing cryptographic features. + +```bash +bundle install +bundle exec appraisal rake test +``` + +## How to contribute See [CONTRIBUTING](CONTRIBUTING.md). ## Contributors Binary files old/checksums.yaml.gz and new/checksums.yaml.gz differ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lib/jwt/algos/eddsa.rb new/lib/jwt/algos/eddsa.rb --- old/lib/jwt/algos/eddsa.rb 2022-06-07 21:53:24.000000000 +0200 +++ new/lib/jwt/algos/eddsa.rb 2022-08-25 21:56:49.000000000 +0200 @@ -27,6 +27,8 @@ raise DecodeError, "key given is a #{public_key.class} but has to be a RbNaCl::Signatures::Ed25519::VerifyKey" if public_key.class != RbNaCl::Signatures::Ed25519::VerifyKey public_key.verify(signature, signing_input) + rescue RbNaCl::CryptoError + false end end end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lib/jwt/base64.rb new/lib/jwt/base64.rb --- old/lib/jwt/base64.rb 1970-01-01 01:00:00.000000000 +0100 +++ new/lib/jwt/base64.rb 2022-08-25 21:56:49.000000000 +0200 @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'base64' + +module JWT + # Base64 helpers + class Base64 + class << self + def url_encode(str) + ::Base64.encode64(str).tr('+/', '-_').gsub(/[\n=]/, '') + end + + def url_decode(str) + str += '=' * (4 - str.length.modulo(4)) + ::Base64.decode64(str.tr('-_', '+/')) + end + end + end +end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lib/jwt/configuration/container.rb new/lib/jwt/configuration/container.rb --- old/lib/jwt/configuration/container.rb 1970-01-01 01:00:00.000000000 +0100 +++ new/lib/jwt/configuration/container.rb 2022-08-25 21:56:49.000000000 +0200 @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require_relative 'decode_configuration' +require_relative 'jwk_configuration' + +module JWT + module Configuration + class Container + attr_accessor :decode, :jwk + + def initialize + reset! + end + + def reset! + @decode = DecodeConfiguration.new + @jwk = JwkConfiguration.new + end + end + end +end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lib/jwt/configuration/decode_configuration.rb new/lib/jwt/configuration/decode_configuration.rb --- old/lib/jwt/configuration/decode_configuration.rb 1970-01-01 01:00:00.000000000 +0100 +++ new/lib/jwt/configuration/decode_configuration.rb 2022-08-25 21:56:49.000000000 +0200 @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module JWT + module Configuration + class DecodeConfiguration + attr_accessor :verify_expiration, + :verify_not_before, + :verify_iss, + :verify_iat, + :verify_jti, + :verify_aud, + :verify_sub, + :leeway, + :algorithms, + :required_claims + + def initialize + @verify_expiration = true + @verify_not_before = true + @verify_iss = false + @verify_iat = false + @verify_jti = false + @verify_aud = false + @verify_sub = false + @leeway = 0 + @algorithms = ['HS256'] + @required_claims = [] + end + + def to_h + { + verify_expiration: verify_expiration, + verify_not_before: verify_not_before, + verify_iss: verify_iss, + verify_iat: verify_iat, + verify_jti: verify_jti, + verify_aud: verify_aud, + verify_sub: verify_sub, + leeway: leeway, + algorithms: algorithms, + required_claims: required_claims + } + end + end + end +end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lib/jwt/configuration/jwk_configuration.rb new/lib/jwt/configuration/jwk_configuration.rb --- old/lib/jwt/configuration/jwk_configuration.rb 1970-01-01 01:00:00.000000000 +0100 +++ new/lib/jwt/configuration/jwk_configuration.rb 2022-08-25 21:56:49.000000000 +0200 @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require_relative '../jwk/kid_as_key_digest' +require_relative '../jwk/thumbprint' + +module JWT + module Configuration + class JwkConfiguration + def initialize + self.kid_generator_type = :key_digest + end + + def kid_generator_type=(value) + self.kid_generator = case value + when :key_digest + JWT::JWK::KidAsKeyDigest + when :rfc7638_thumbprint + JWT::JWK::Thumbprint + else + raise ArgumentError, "#{value} is not a valid kid generator type." + end + end + + attr_accessor :kid_generator + end + end +end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lib/jwt/configuration.rb new/lib/jwt/configuration.rb --- old/lib/jwt/configuration.rb 1970-01-01 01:00:00.000000000 +0100 +++ new/lib/jwt/configuration.rb 2022-08-25 21:56:49.000000000 +0200 @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require_relative 'configuration/container' + +module JWT + module Configuration + def configure + yield(configuration) + end + + def configuration + @configuration ||= ::JWT::Configuration::Container.new + end + end +end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lib/jwt/decode.rb new/lib/jwt/decode.rb --- old/lib/jwt/decode.rb 2022-06-07 21:53:24.000000000 +0200 +++ new/lib/jwt/decode.rb 2022-08-25 21:56:49.000000000 +0200 @@ -113,13 +113,11 @@ end def none_algorithm? - algorithm.casecmp('none').zero? + algorithm == 'none' end def decode_crypto - @signature = Base64.urlsafe_decode64(@segments[2] || '') - rescue ArgumentError - raise(JWT::DecodeError, 'Invalid segment encoding') + @signature = ::JWT::Base64.url_decode(@segments[2] || '') end def algorithm @@ -139,8 +137,8 @@ end def parse_and_decode(segment) - JWT::JSON.parse(Base64.urlsafe_decode64(segment)) - rescue ::JSON::ParserError, ArgumentError + JWT::JSON.parse(::JWT::Base64.url_decode(segment)) + rescue ::JSON::ParserError raise JWT::DecodeError, 'Invalid segment encoding' end end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lib/jwt/default_options.rb new/lib/jwt/default_options.rb --- old/lib/jwt/default_options.rb 2022-06-07 21:53:24.000000000 +0200 +++ new/lib/jwt/default_options.rb 1970-01-01 01:00:00.000000000 +0100 @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -module JWT - module DefaultOptions - DEFAULT_OPTIONS = { - verify_expiration: true, - verify_not_before: true, - verify_iss: false, - verify_iat: false, - verify_jti: false, - verify_aud: false, - verify_sub: false, - leeway: 0, - algorithms: ['HS256'], - required_claims: [] - }.freeze - end -end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lib/jwt/encode.rb new/lib/jwt/encode.rb --- old/lib/jwt/encode.rb 2022-06-07 21:53:24.000000000 +0200 +++ new/lib/jwt/encode.rb 2022-08-25 21:56:49.000000000 +0200 @@ -55,11 +55,11 @@ def encode_signature return '' if @algorithm == ALG_NONE - Base64.urlsafe_encode64(JWT::Signature.sign(@algorithm, encoded_header_and_payload, @key), padding: false) + ::JWT::Base64.url_encode(JWT::Signature.sign(@algorithm, encoded_header_and_payload, @key)) end def encode(data) - Base64.urlsafe_encode64(JWT::JSON.generate(data), padding: false) + ::JWT::Base64.url_encode(JWT::JSON.generate(data)) end def combine(*parts) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lib/jwt/jwk/ec.rb new/lib/jwt/jwk/ec.rb --- old/lib/jwt/jwk/ec.rb 2022-06-07 21:53:24.000000000 +0200 +++ new/lib/jwt/jwk/ec.rb 2022-08-25 21:56:49.000000000 +0200 @@ -4,39 +4,53 @@ module JWT module JWK - class EC < KeyBase + class EC < KeyBase # rubocop:disable Metrics/ClassLength extend Forwardable - def_delegators :@keypair, :public_key + def_delegators :keypair, :public_key KTY = 'EC' KTYS = [KTY, OpenSSL::PKey::EC].freeze BINARY = 2 - def initialize(keypair, kid = nil) + attr_reader :keypair + + def initialize(keypair, options = {}) raise ArgumentError, 'keypair must be of type OpenSSL::PKey::EC' unless keypair.is_a?(OpenSSL::PKey::EC) - kid ||= generate_kid(keypair) - super(keypair, kid) + @keypair = keypair + + super(options) end def private? @keypair.private_key? end - def export(options = {}) + def members crv, x_octets, y_octets = keypair_components(keypair) - exported_hash = { + { kty: KTY, crv: crv, x: encode_octets(x_octets), - y: encode_octets(y_octets), - kid: kid + y: encode_octets(y_octets) } + end + + def export(options = {}) + exported_hash = members.merge(kid: kid) + return exported_hash unless private? && options[:include_private] == true append_private_parts(exported_hash) end + def key_digest + _crv, x_octets, y_octets = keypair_components(keypair) + sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(x_octets, BINARY)), + OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(y_octets, BINARY))]) + OpenSSL::Digest::SHA256.hexdigest(sequence.to_der) + end + private def append_private_parts(the_hash) @@ -46,13 +60,6 @@ ) end - def generate_kid(ec_keypair) - _crv, x_octets, y_octets = keypair_components(ec_keypair) - sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(x_octets, BINARY)), - OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(y_octets, BINARY))]) - OpenSSL::Digest::SHA256.hexdigest(sequence.to_der) - end - def keypair_components(ec_keypair) encoded_point = ec_keypair.public_key.to_bn.to_s(BINARY) case ec_keypair.group.curve_name @@ -75,11 +82,11 @@ end def encode_octets(octets) - Base64.urlsafe_encode64(octets, padding: false) + ::JWT::Base64.url_encode(octets) end def encode_open_ssl_bn(key_part) - Base64.urlsafe_encode64(key_part.to_s(BINARY), padding: false) + ::JWT::Base64.url_encode(key_part.to_s(BINARY)) end class << self @@ -90,7 +97,7 @@ jwk_crv, jwk_x, jwk_y, jwk_d, jwk_kid = jwk_attrs(jwk_data, %i[crv x y d kid]) raise JWT::JWKError, 'Key format is invalid for EC' unless jwk_crv && jwk_x && jwk_y - new(ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d), jwk_kid) + new(ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d), kid: jwk_kid) end def to_openssl_curve(crv) @@ -114,39 +121,77 @@ end end - def ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d) - curve = to_openssl_curve(jwk_crv) + if ::JWT.openssl_3? + def ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d) # rubocop:disable Metrics/MethodLength + curve = to_openssl_curve(jwk_crv) + + x_octets = decode_octets(jwk_x) + y_octets = decode_octets(jwk_y) + + point = OpenSSL::PKey::EC::Point.new( + OpenSSL::PKey::EC::Group.new(curve), + OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2) + ) + + sequence = if jwk_d + # https://datatracker.ietf.org/doc/html/rfc5915.html + # ECPrivateKey ::= SEQUENCE { + # version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1), + # privateKey OCTET STRING, + # parameters [0] ECParameters {{ NamedCurve }} OPTIONAL, + # publicKey [1] BIT STRING OPTIONAL + # } + + OpenSSL::ASN1::Sequence([ + OpenSSL::ASN1::Integer(1), + OpenSSL::ASN1::OctetString(OpenSSL::BN.new(decode_octets(jwk_d), 2).to_s(2)), + OpenSSL::ASN1::ObjectId(curve, 0, :EXPLICIT), + OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed), 1, :EXPLICIT) + ]) + else + OpenSSL::ASN1::Sequence([ + OpenSSL::ASN1::Sequence([OpenSSL::ASN1::ObjectId('id-ecPublicKey'), OpenSSL::ASN1::ObjectId(curve)]), + OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed)) + ]) + end - x_octets = decode_octets(jwk_x) - y_octets = decode_octets(jwk_y) + OpenSSL::PKey::EC.new(sequence.to_der) + end + else + def ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d) + curve = to_openssl_curve(jwk_crv) - key = OpenSSL::PKey::EC.new(curve) + x_octets = decode_octets(jwk_x) + y_octets = decode_octets(jwk_y) - # The details of the `Point` instantiation are covered in: - # - https://docs.ruby-lang.org/en/2.4.0/OpenSSL/PKey/EC.html - # - https://www.openssl.org/docs/manmaster/man3/EC_POINT_new.html - # - https://tools.ietf.org/html/rfc5480#section-2.2 - # - https://www.secg.org/SEC1-Ver-1.0.pdf - # Section 2.3.3 of the last of these references specifies that the - # encoding of an uncompressed point consists of the byte `0x04` followed - # by the x value then the y value. - point = OpenSSL::PKey::EC::Point.new( - OpenSSL::PKey::EC::Group.new(curve), - OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2) - ) + key = OpenSSL::PKey::EC.new(curve) - key.public_key = point - key.private_key = OpenSSL::BN.new(decode_octets(jwk_d), 2) if jwk_d + # The details of the `Point` instantiation are covered in: + # - https://docs.ruby-lang.org/en/2.4.0/OpenSSL/PKey/EC.html + # - https://www.openssl.org/docs/manmaster/man3/EC_POINT_new.html + # - https://tools.ietf.org/html/rfc5480#section-2.2 + # - https://www.secg.org/SEC1-Ver-1.0.pdf + # Section 2.3.3 of the last of these references specifies that the + # encoding of an uncompressed point consists of the byte `0x04` followed + # by the x value then the y value. + point = OpenSSL::PKey::EC::Point.new( + OpenSSL::PKey::EC::Group.new(curve), + OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2) + ) - key + key.public_key = point + key.private_key = OpenSSL::BN.new(decode_octets(jwk_d), 2) if jwk_d + + key + end end def decode_octets(jwk_data) - Base64.urlsafe_decode64(jwk_data) + ::JWT::Base64.url_decode(jwk_data) end def decode_open_ssl_bn(jwk_data) - OpenSSL::BN.new(Base64.urlsafe_decode64(jwk_data), BINARY) + OpenSSL::BN.new(::JWT::Base64.url_decode(jwk_data), BINARY) end end end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lib/jwt/jwk/hmac.rb new/lib/jwt/jwk/hmac.rb --- old/lib/jwt/jwk/hmac.rb 2022-06-07 21:53:24.000000000 +0200 +++ new/lib/jwt/jwk/hmac.rb 2022-08-25 21:56:49.000000000 +0200 @@ -3,14 +3,16 @@ module JWT module JWK class HMAC < KeyBase - KTY = 'oct' + KTY = 'oct' KTYS = [KTY, String].freeze - def initialize(keypair, kid = nil) - raise ArgumentError, 'keypair must be of type String' unless keypair.is_a?(String) + attr_reader :signing_key - super - @kid = kid || generate_kid + def initialize(signing_key, options = {}) + raise ArgumentError, 'signing_key must be of type String' unless signing_key.is_a?(String) + + @signing_key = signing_key + super(options) end def private? @@ -31,14 +33,21 @@ return exported_hash unless private? && options[:include_private] == true exported_hash.merge( - k: keypair + k: signing_key ) end - private + def members + { + kty: KTY, + k: signing_key + } + end + + alias keypair signing_key # for backwards compatibility - def generate_kid - sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::UTF8String.new(keypair), + def key_digest + sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::UTF8String.new(signing_key), OpenSSL::ASN1::UTF8String.new(KTY)]) OpenSSL::Digest::SHA256.hexdigest(sequence.to_der) end @@ -50,7 +59,7 @@ raise JWT::JWKError, 'Key format is invalid for HMAC' unless jwk_k - new(jwk_k, jwk_kid) + new(jwk_k, kid: jwk_kid) end end end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lib/jwt/jwk/key_base.rb new/lib/jwt/jwk/key_base.rb --- old/lib/jwt/jwk/key_base.rb 2022-06-07 21:53:24.000000000 +0200 +++ new/lib/jwt/jwk/key_base.rb 2022-08-25 21:56:49.000000000 +0200 @@ -3,17 +3,33 @@ module JWT module JWK class KeyBase - attr_reader :keypair, :kid - - def initialize(keypair, kid = nil) - @keypair = keypair - @kid = kid - end - def self.inherited(klass) super ::JWT::JWK.classes << klass end + + def initialize(options) + options ||= {} + + if options.is_a?(String) # For backwards compatibility when kid was a String + options = { kid: options } + end + + @kid = options[:kid] + @kid_generator = options[:kid_generator] || ::JWT.configuration.jwk.kid_generator + end + + def kid + @kid ||= generate_kid + end + + private + + attr_reader :kid_generator + + def generate_kid + kid_generator.new(self).generate + end end end end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lib/jwt/jwk/key_finder.rb new/lib/jwt/jwk/key_finder.rb --- old/lib/jwt/jwk/key_finder.rb 2022-06-07 21:53:24.000000000 +0200 +++ new/lib/jwt/jwk/key_finder.rb 2022-08-25 21:56:49.000000000 +0200 @@ -28,7 +28,7 @@ return jwk if jwk if reloadable? - load_keys(invalidate: true) + load_keys(invalidate: true, kid_not_found: true, kid: kid) # invalidate for backwards compatibility return find_key(kid) end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lib/jwt/jwk/kid_as_key_digest.rb new/lib/jwt/jwk/kid_as_key_digest.rb --- old/lib/jwt/jwk/kid_as_key_digest.rb 1970-01-01 01:00:00.000000000 +0100 +++ new/lib/jwt/jwk/kid_as_key_digest.rb 2022-08-25 21:56:49.000000000 +0200 @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module JWT + module JWK + class KidAsKeyDigest + def initialize(jwk) + @jwk = jwk + end + + def generate + @jwk.key_digest + end + end + end +end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lib/jwt/jwk/rsa.rb new/lib/jwt/jwk/rsa.rb --- old/lib/jwt/jwk/rsa.rb 2022-06-07 21:53:24.000000000 +0200 +++ new/lib/jwt/jwk/rsa.rb 2022-08-25 21:56:49.000000000 +0200 @@ -8,10 +8,14 @@ KTYS = [KTY, OpenSSL::PKey::RSA].freeze RSA_KEY_ELEMENTS = %i[n e d p q dp dq qi].freeze - def initialize(keypair, kid = nil) + attr_reader :keypair + + def initialize(keypair, options = {}) raise ArgumentError, 'keypair must be of type OpenSSL::PKey::RSA' unless keypair.is_a?(OpenSSL::PKey::RSA) - super(keypair, kid || generate_kid(keypair.public_key)) + @keypair = keypair + + super(options) end def private? @@ -23,26 +27,29 @@ end def export(options = {}) - exported_hash = { - kty: KTY, - n: encode_open_ssl_bn(public_key.n), - e: encode_open_ssl_bn(public_key.e), - kid: kid - } + exported_hash = members.merge(kid: kid) return exported_hash unless private? && options[:include_private] == true append_private_parts(exported_hash) end - private + def members + { + kty: KTY, + n: encode_open_ssl_bn(public_key.n), + e: encode_open_ssl_bn(public_key.e) + } + end - def generate_kid(public_key) + def key_digest sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(public_key.n), OpenSSL::ASN1::Integer.new(public_key.e)]) OpenSSL::Digest::SHA256.hexdigest(sequence.to_der) end + private + def append_private_parts(the_hash) the_hash.merge( d: encode_open_ssl_bn(keypair.d), @@ -55,7 +62,7 @@ end def encode_open_ssl_bn(key_part) - Base64.urlsafe_encode64(key_part.to_s(BINARY), padding: false) + ::JWT::Base64.url_encode(key_part.to_s(BINARY)) end class << self @@ -63,8 +70,7 @@ pkey_params = jwk_attributes(jwk_data, *RSA_KEY_ELEMENTS) do |value| decode_open_ssl_bn(value) end - kid = jwk_attributes(jwk_data, :kid)[:kid] - new(rsa_pkey(pkey_params), kid) + new(rsa_pkey(pkey_params), kid: jwk_attributes(jwk_data, :kid)[:kid]) end private @@ -80,35 +86,51 @@ def rsa_pkey(rsa_parameters) raise JWT::JWKError, 'Key format is invalid for RSA' unless rsa_parameters[:n] && rsa_parameters[:e] - populate_key(OpenSSL::PKey::RSA.new, rsa_parameters) + create_rsa_key(rsa_parameters) end - if OpenSSL::PKey::RSA.new.respond_to?(:set_key) - def populate_key(rsa_key, rsa_parameters) - rsa_key.set_key(rsa_parameters[:n], rsa_parameters[:e], rsa_parameters[:d]) - rsa_key.set_factors(rsa_parameters[:p], rsa_parameters[:q]) if rsa_parameters[:p] && rsa_parameters[:q] - rsa_key.set_crt_params(rsa_parameters[:dp], rsa_parameters[:dq], rsa_parameters[:qi]) if rsa_parameters[:dp] && rsa_parameters[:dq] && rsa_parameters[:qi] - rsa_key + if ::JWT.openssl_3? + ASN1_SEQUENCE = %i[n e d p q dp dq qi].freeze + def create_rsa_key(rsa_parameters) + sequence = ASN1_SEQUENCE.each_with_object([]) do |key, arr| + next if rsa_parameters[key].nil? + + arr << OpenSSL::ASN1::Integer.new(rsa_parameters[key]) + end + + if sequence.size > 2 # For a private key + sequence.unshift(OpenSSL::ASN1::Integer.new(0)) + end + + OpenSSL::PKey::RSA.new(OpenSSL::ASN1::Sequence(sequence).to_der) + end + elsif OpenSSL::PKey::RSA.new.respond_to?(:set_key) + def create_rsa_key(rsa_parameters) + OpenSSL::PKey::RSA.new.tap do |rsa_key| + rsa_key.set_key(rsa_parameters[:n], rsa_parameters[:e], rsa_parameters[:d]) + rsa_key.set_factors(rsa_parameters[:p], rsa_parameters[:q]) if rsa_parameters[:p] && rsa_parameters[:q] + rsa_key.set_crt_params(rsa_parameters[:dp], rsa_parameters[:dq], rsa_parameters[:qi]) if rsa_parameters[:dp] && rsa_parameters[:dq] && rsa_parameters[:qi] + end end else - def populate_key(rsa_key, rsa_parameters) - rsa_key.n = rsa_parameters[:n] - rsa_key.e = rsa_parameters[:e] - rsa_key.d = rsa_parameters[:d] if rsa_parameters[:d] - rsa_key.p = rsa_parameters[:p] if rsa_parameters[:p] - rsa_key.q = rsa_parameters[:q] if rsa_parameters[:q] - rsa_key.dmp1 = rsa_parameters[:dp] if rsa_parameters[:dp] - rsa_key.dmq1 = rsa_parameters[:dq] if rsa_parameters[:dq] - rsa_key.iqmp = rsa_parameters[:qi] if rsa_parameters[:qi] - - rsa_key + def create_rsa_key(rsa_parameters) # rubocop:disable Metrics/AbcSize + OpenSSL::PKey::RSA.new.tap do |rsa_key| + rsa_key.n = rsa_parameters[:n] + rsa_key.e = rsa_parameters[:e] + rsa_key.d = rsa_parameters[:d] if rsa_parameters[:d] + rsa_key.p = rsa_parameters[:p] if rsa_parameters[:p] + rsa_key.q = rsa_parameters[:q] if rsa_parameters[:q] + rsa_key.dmp1 = rsa_parameters[:dp] if rsa_parameters[:dp] + rsa_key.dmq1 = rsa_parameters[:dq] if rsa_parameters[:dq] + rsa_key.iqmp = rsa_parameters[:qi] if rsa_parameters[:qi] + end end end def decode_open_ssl_bn(jwk_data) return nil unless jwk_data - OpenSSL::BN.new(Base64.urlsafe_decode64(jwk_data), BINARY) + OpenSSL::BN.new(::JWT::Base64.url_decode(jwk_data), BINARY) end end end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lib/jwt/jwk/thumbprint.rb new/lib/jwt/jwk/thumbprint.rb --- old/lib/jwt/jwk/thumbprint.rb 1970-01-01 01:00:00.000000000 +0100 +++ new/lib/jwt/jwk/thumbprint.rb 2022-08-25 21:56:49.000000000 +0200 @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module JWT + module JWK + # https://tools.ietf.org/html/rfc7638 + class Thumbprint + attr_reader :jwk + + def initialize(jwk) + @jwk = jwk + end + + def generate + ::Base64.urlsafe_encode64( + Digest::SHA256.digest( + JWT::JSON.generate( + jwk.members.sort.to_h + ) + ), padding: false + ) + end + + alias to_s generate + end + end +end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lib/jwt/version.rb new/lib/jwt/version.rb --- old/lib/jwt/version.rb 2022-06-07 21:53:24.000000000 +0200 +++ new/lib/jwt/version.rb 2022-08-25 21:56:49.000000000 +0200 @@ -11,13 +11,18 @@ # major version MAJOR = 2 # minor version - MINOR = 4 + MINOR = 5 # tiny version - TINY = 1 + TINY = 0 # alpha, beta, etc. tag PRE = nil # Build version string STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end + + def self.openssl_3? + return false if OpenSSL::OPENSSL_VERSION.include?('LibreSSL') + return true if OpenSSL::OPENSSL_VERSION_NUMBER >= 3 * 0x10000000 + end end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lib/jwt/x5c_key_finder.rb new/lib/jwt/x5c_key_finder.rb --- old/lib/jwt/x5c_key_finder.rb 2022-06-07 21:53:24.000000000 +0200 +++ new/lib/jwt/x5c_key_finder.rb 2022-08-25 21:56:49.000000000 +0200 @@ -47,7 +47,7 @@ x5c_header_or_certificates else x5c_header_or_certificates.map do |encoded| - OpenSSL::X509::Certificate.new(::Base64.strict_decode64(encoded)) + OpenSSL::X509::Certificate.new(::JWT::Base64.url_decode(encoded)) end end end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/lib/jwt.rb new/lib/jwt.rb --- old/lib/jwt.rb 2022-06-07 21:53:24.000000000 +0200 +++ new/lib/jwt.rb 2022-08-25 21:56:49.000000000 +0200 @@ -1,9 +1,10 @@ # frozen_string_literal: true -require 'base64' +require 'jwt/version' +require 'jwt/base64' require 'jwt/json' require 'jwt/decode' -require 'jwt/default_options' +require 'jwt/configuration' require 'jwt/encode' require 'jwt/error' require 'jwt/jwk' @@ -13,7 +14,7 @@ # Should be up to date with the latest spec: # https://tools.ietf.org/html/rfc7519 module JWT - include JWT::DefaultOptions + extend ::JWT::Configuration module_function @@ -25,6 +26,6 @@ end def decode(jwt, key = nil, verify = true, options = {}, &keyfinder) # rubocop:disable Style/OptionalBooleanParameter - Decode.new(jwt, key, verify, DEFAULT_OPTIONS.merge(options), &keyfinder).decode_segments + Decode.new(jwt, key, verify, configuration.decode.to_h.merge(options), &keyfinder).decode_segments end end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/metadata new/metadata --- old/metadata 2022-06-07 21:53:24.000000000 +0200 +++ new/metadata 2022-08-25 21:56:49.000000000 +0200 @@ -1,14 +1,14 @@ --- !ruby/object:Gem::Specification name: jwt version: !ruby/object:Gem::Version - version: 2.4.1 + version: 2.5.0 platform: ruby authors: - Tim Rudat autorequire: bindir: bin cert_chain: [] -date: 2022-06-07 00:00:00.000000000 Z +date: 2022-08-25 00:00:00.000000000 Z dependencies: - !ruby/object:Gem::Dependency name: appraisal @@ -127,9 +127,13 @@ - lib/jwt/algos/ps.rb - lib/jwt/algos/rsa.rb - lib/jwt/algos/unsupported.rb +- lib/jwt/base64.rb - lib/jwt/claims_validator.rb +- lib/jwt/configuration.rb +- lib/jwt/configuration/container.rb +- lib/jwt/configuration/decode_configuration.rb +- lib/jwt/configuration/jwk_configuration.rb - lib/jwt/decode.rb -- lib/jwt/default_options.rb - lib/jwt/encode.rb - lib/jwt/error.rb - lib/jwt/json.rb @@ -138,7 +142,9 @@ - lib/jwt/jwk/hmac.rb - lib/jwt/jwk/key_base.rb - lib/jwt/jwk/key_finder.rb +- lib/jwt/jwk/kid_as_key_digest.rb - lib/jwt/jwk/rsa.rb +- lib/jwt/jwk/thumbprint.rb - lib/jwt/security_utils.rb - lib/jwt/signature.rb - lib/jwt/verify.rb @@ -150,7 +156,7 @@ - MIT metadata: bug_tracker_uri: https://github.com/jwt/ruby-jwt/issues - changelog_uri: https://github.com/jwt/ruby-jwt/blob/v2.4.1/CHANGELOG.md + changelog_uri: https://github.com/jwt/ruby-jwt/blob/v2.5.0/CHANGELOG.md post_install_message: rdoc_options: [] require_paths: @@ -166,7 +172,7 @@ - !ruby/object:Gem::Version version: '0' requirements: [] -rubygems_version: 3.3.7 +rubygems_version: 3.3.21 signing_key: specification_version: 4 summary: JSON Web Token implementation in Ruby
