From 014d4b35ffc6a1074db5f8e1cedd005645826aa6 Mon Sep 17 00:00:00 2001 From: Joakim Antman Date: Sat, 28 Dec 2024 23:37:16 +0200 Subject: [PATCH] Encode and decode the k value for hmac keys --- CHANGELOG.md | 1 + UPGRADING.md | 6 +++++- lib/jwt/jwk/hmac.rb | 4 ++-- spec/jwt/jwk/hmac_spec.rb | 9 +++++++++ spec/jwt/jwk/set_spec.rb | 22 +++++++++++----------- 5 files changed, 28 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5816c184..de4c0b66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Support only stricter base64 decoding (RFC 4648) [#658](https://github.com/jwt/ruby-jwt/pull/658) ([@anakinj](https://github.com/anakinj)) - Custom algorithms are required to include `JWT::JWA::SigningAlgorithm` [#660](https://github.com/jwt/ruby-jwt/pull/660) ([@anakinj](https://github.com/anakinj)) - Require RSA keys to be at least 2048 bits [#661](https://github.com/jwt/ruby-jwt/pull/661) ([@anakinj](https://github.com/anakinj)) +- Base64 encode and decode the k value for HMAC JWKs [#662](https://github.com/jwt/ruby-jwt/pull/662) ([@anakinj](https://github.com/anakinj)) Take a look at the [upgrade guide](UPGRADING.md) for more details. diff --git a/UPGRADING.md b/UPGRADING.md index 9da62fc8..1953ddf9 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -39,4 +39,8 @@ Claim verification has been [split into separate classes](https://github.com/jwt The internal algorithms were [restructured](https://github.com/jwt/ruby-jwt/pull/607) to support extensions from separate libraries. The changes led to a few deprecations and new requirements: - The `sign` and `verify` static methods on all the algorithms (`::JWT::JWA`) will be removed. -- Custom algorithms are expected to include the `JWT::JWA::SigningAlgorithm` module. \ No newline at end of file +- Custom algorithms are expected to include the `JWT::JWA::SigningAlgorithm` module. + +## Base64 the `k´ value for HMAC JWKs + +The gem was missing the Base64 encoding and decoding when representing and parsing a HMAC key as a JWK. This issue is now addressed. The added encoding will break compatibility with JWKs produced by older versions of the gem. diff --git a/lib/jwt/jwk/hmac.rb b/lib/jwt/jwk/hmac.rb index 2eb88fca..7bff9948 100644 --- a/lib/jwt/jwk/hmac.rb +++ b/lib/jwt/jwk/hmac.rb @@ -70,7 +70,7 @@ def []=(key, value) private def secret - self[:k] + @secret ||= ::JWT::Base64.url_decode(self[:k]) end def extract_key_params(key) @@ -78,7 +78,7 @@ def extract_key_params(key) when JWT::JWK::HMAC key.export(include_private: true) when String # Accept String key as input - { kty: KTY, k: key } + { kty: KTY, k: ::JWT::Base64.url_encode(key) } when Hash key.transform_keys(&:to_sym) else diff --git a/spec/jwt/jwk/hmac_spec.rb b/spec/jwt/jwk/hmac_spec.rb index b64e5c3a..12f4832a 100644 --- a/spec/jwt/jwk/hmac_spec.rb +++ b/spec/jwt/jwk/hmac_spec.rb @@ -73,6 +73,15 @@ end end end + + context 'when example from RFC' do + let(:params) { { kty: 'oct', k: 'AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow' } } + + it 'decodes the k' do + expected_key = "\x03#5K+\x0F\xA5\xBC\x83~\x06ew{\xA6\x8FZ\xB3(\xE6\xF0T\xC9(\xA9\x0F\x84\xB2\xD2P.\xBF\xD3\xFBZ\x92\xD2\x06G\xEF\x96\x8A\xB4\xC3wb=\"=.!r\x05.O\b\xC0\xCD\x9A\xF5g\xD0\x80\xA3".dup.force_encoding('ASCII-8BIT') + expect(subject.verify_key).to eq(expected_key) + end + end end describe '#[]=' do diff --git a/spec/jwt/jwk/set_spec.rb b/spec/jwt/jwk/set_spec.rb index 08c8372c..999622ac 100644 --- a/spec/jwt/jwk/set_spec.rb +++ b/spec/jwt/jwk/set_spec.rb @@ -13,14 +13,14 @@ end it 'from a JWKS hash with symbol keys' do - jwks = { keys: [{ kty: 'oct', k: 'testkey' }] } - jwk = JWT::JWK.new({ kty: 'oct', k: 'testkey' }) + jwks = { keys: [{ kty: 'oct', k: Base64.strict_encode64('testkey') }] } + jwk = JWT::JWK.new({ kty: 'oct', k: Base64.strict_encode64('testkey') }) expect(described_class.new(jwks).keys).to eql([jwk]) end it 'from a JWKS hash with string keys' do - jwks = { 'keys' => [{ 'kty' => 'oct', 'k' => 'testkey' }] } - jwk = JWT::JWK.new({ kty: 'oct', k: 'testkey' }) + jwks = { 'keys' => [{ 'kty' => 'oct', 'k' => Base64.strict_encode64('testkey') }] } + jwk = JWT::JWK.new({ kty: 'oct', k: Base64.strict_encode64('testkey') }) expect(described_class.new(jwks).keys).to eql([jwk]) end @@ -30,7 +30,7 @@ end it 'from an existing JWT::JWK::Set' do - jwk = JWT::JWK.new({ kty: 'oct', k: 'testkey' }) + jwk = JWT::JWK.new({ kty: 'oct', k: Base64.strict_encode64('testkey') }) jwks = described_class.new(jwk) expect(described_class.new(jwks)).to eql(jwks) end @@ -43,7 +43,7 @@ describe '.export' do it 'exports the JWKS to Hash' do - jwk = JWT::JWK.new({ kty: 'oct', k: 'testkey' }) + jwk = JWT::JWK.new({ kty: 'oct', k: Base64.strict_encode64('testkey') }) jwks = described_class.new(jwk) exported = jwks.export expect(exported[:keys].size).to eql(1) @@ -53,15 +53,15 @@ describe '.eql?' do it 'correctly classifies equal sets' do - jwk = JWT::JWK.new({ kty: 'oct', k: 'testkey' }) + jwk = JWT::JWK.new({ kty: 'oct', k: Base64.strict_encode64('testkey') }) jwks1 = described_class.new(jwk) jwks2 = described_class.new(jwk) expect(jwks1).to eql(jwks2) end it 'correctly classifies different sets' do - jwk1 = JWT::JWK.new({ kty: 'oct', k: 'testkey' }) - jwk2 = JWT::JWK.new({ kty: 'oct', k: 'testkex' }) + jwk1 = JWT::JWK.new({ kty: 'oct', k: Base64.strict_encode64('testkey') }) + jwk2 = JWT::JWK.new({ kty: 'oct', k: Base64.strict_encode64('testkex') }) jwks1 = described_class.new(jwk1) jwks2 = described_class.new(jwk2) expect(jwks1).not_to eql(jwks2) @@ -72,8 +72,8 @@ # but Array#uniq! doesn't recognize this, despite the documentation saying otherwise describe '.uniq!' do it 'filters out equal keys' do - jwk = JWT::JWK.new({ kty: 'oct', k: 'testkey' }) - jwk2 = JWT::JWK.new({ kty: 'oct', k: 'testkey' }) + jwk = JWT::JWK.new({ kty: 'oct', k: Base64.strict_encode64('testkey') }) + jwk2 = JWT::JWK.new({ kty: 'oct', k: Base64.strict_encode64('testkey') }) jwks = described_class.new([jwk, jwk2]) jwks.uniq! expect(jwks.keys.size).to eql(1)