Skip to content

Commit b45219d

Browse files
authored
Do not automatically trust the JWA in the JWK (#695)
Do not automatically trust the alg in the JWK
1 parent 5c4fb4c commit b45219d

File tree

11 files changed

+111
-25
lines changed

11 files changed

+111
-25
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
**Fixes and enhancements:**
1212

13+
- Require the algorithm to be provided when signing and verifying tokens using JWKs [#695](https://github.com/jwt/ruby-jwt/pull/695) ([@anakinj](https://github.com/anakinj))
1314
- Your contribution here
1415

1516
## [v3.1.0](https://github.com/jwt/ruby-jwt/tree/v3.1.0) (2025-06-23)

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ token = JWT::Token.new(payload: payload, header: header)
268268
token.sign!(key: jwk)
269269

270270
encoded_token = JWT::EncodedToken.new(token.jwt)
271-
encoded_token.verify!(signature: { key: jwk})
271+
encoded_token.verify!(signature: { algorithm: ["HS256", "HS512"], key: jwk})
272272
```
273273

274274
#### Using a keyfinder

lib/jwt/encoded_token.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def valid?(signature:, claims: nil)
138138
# @return [nil]
139139
# @raise [JWT::VerificationError] if the signature verification fails.
140140
# @raise [ArgumentError] if neither key nor key_finder is provided, or if both are provided.
141-
def verify_signature!(algorithm: nil, key: nil, key_finder: nil)
141+
def verify_signature!(algorithm:, key: nil, key_finder: nil)
142142
return if valid_signature?(algorithm: algorithm, key: key, key_finder: key_finder)
143143

144144
raise JWT::VerificationError, 'Signature verification failed'

lib/jwt/jwa.rb

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ module JWT
1515
module JWA
1616
# @api private
1717
class VerifierContext
18+
attr_reader :jwa
19+
1820
def initialize(jwa:, keys:)
1921
@jwa = jwa
2022
@keys = Array(keys)
@@ -29,6 +31,8 @@ def verify(*args, **kwargs)
2931

3032
# @api private
3133
class SignerContext
34+
attr_reader :jwa
35+
3236
def initialize(jwa:, key:)
3337
@jwa = jwa
3438
@key = key
@@ -37,10 +41,6 @@ def initialize(jwa:, key:)
3741
def sign(*args, **kwargs)
3842
@jwa.sign(*args, **kwargs, signing_key: @key)
3943
end
40-
41-
def jwa_header
42-
@jwa.header
43-
end
4444
end
4545

4646
class << self
@@ -64,7 +64,11 @@ def resolve_and_sort(algorithms:, preferred_algorithm:)
6464

6565
# @api private
6666
def create_signer(algorithm:, key:)
67-
return key if key.is_a?(JWK::KeyBase)
67+
if key.is_a?(JWK::KeyBase)
68+
validate_jwk_algorithms!(key, algorithm, DecodeError)
69+
70+
return key
71+
end
6872

6973
SignerContext.new(jwa: resolve(algorithm), key: key)
7074
end
@@ -73,10 +77,27 @@ def create_signer(algorithm:, key:)
7377
def create_verifiers(algorithms:, keys:, preferred_algorithm:)
7478
jwks, other_keys = keys.partition { |key| key.is_a?(JWK::KeyBase) }
7579

80+
validate_jwk_algorithms!(jwks, algorithms, VerificationError)
81+
7682
jwks + resolve_and_sort(algorithms: algorithms,
7783
preferred_algorithm: preferred_algorithm)
7884
.map { |jwa| VerifierContext.new(jwa: jwa, keys: other_keys) }
7985
end
86+
87+
# @api private
88+
def validate_jwk_algorithms!(jwks, algorithms, error_class)
89+
algorithms = Array(algorithms)
90+
91+
return if algorithms.empty?
92+
93+
return if Array(jwks).all? do |jwk|
94+
algorithms.any? do |alg|
95+
jwk.jwa.valid_alg?(alg)
96+
end
97+
end
98+
99+
raise error_class, "Provided JWKs do not support one of the specified algorithms: #{algorithms.join(', ')}"
100+
end
80101
end
81102
end
82103
end

lib/jwt/jwk/key_base.rb

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,6 @@ def sign(**kwargs)
5050
jwa.sign(**kwargs, signing_key: signing_key)
5151
end
5252

53-
# @api private
54-
def jwa_header
55-
jwa.header
56-
end
57-
5853
alias eql? ==
5954

6055
def <=>(other)
@@ -63,12 +58,12 @@ def <=>(other)
6358
self[:kid] <=> other[:kid]
6459
end
6560

66-
private
67-
6861
def jwa
6962
raise JWT::JWKError, 'Could not resolve the JWA, the "alg" parameter is missing' unless self[:alg]
7063

71-
JWA.resolve(self[:alg])
64+
JWA.resolve(self[:alg]).tap do |jwa|
65+
raise JWT::JWKError, 'none algorithm usage not supported via JWK' if jwa.is_a?(JWA::None)
66+
end
7267
end
7368

7469
attr_reader :parameters

lib/jwt/token.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,11 @@ def detach_payload!
9191
# @param algorithm [String, Object] the algorithm to use for signing.
9292
# @return [void]
9393
# @raise [JWT::EncodeError] if the token is already signed or other problems when signing
94-
def sign!(key:, algorithm: nil)
94+
def sign!(key:, algorithm:)
9595
raise ::JWT::EncodeError, 'Token already signed' if @signature
9696

9797
JWA.create_signer(algorithm: algorithm, key: key).tap do |signer|
98-
header.merge!(signer.jwa_header) { |_key, old, _new| old }
98+
header.merge!(signer.jwa.header) { |_key, old, _new| old }
9999
@signature = signer.sign(data: signing_input)
100100
end
101101

spec/integration/readme_examples_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -472,10 +472,10 @@ def self.verify(data:, signature:, verification_key:)
472472
jwk = JWT::JWK.import(JSON.parse(jwk_json))
473473

474474
token = JWT::Token.new(payload: payload, header: header)
475-
token.sign!(key: jwk)
475+
token.sign!(key: jwk, algorithm: 'HS256')
476476

477477
encoded_token = JWT::EncodedToken.new(token.jwt)
478-
expect { encoded_token.verify!(signature: { key: jwk }) }.not_to raise_error
478+
expect { encoded_token.verify!(signature: { algorithm: %w[HS256 HS512], key: jwk }) }.not_to raise_error
479479
end
480480
end
481481
end

spec/jwt/encoded_token_spec.rb

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,15 @@
139139
end
140140
end
141141

142+
context 'when algorithm is an empty array' do
143+
it 'raises an error' do
144+
expect { token.verify_signature!(key: 'secret', algorithm: []) }.to raise_error(JWT::VerificationError, 'No algorithm provided')
145+
end
146+
end
147+
142148
context 'when algorithm is not given' do
143149
it 'raises an error' do
144-
expect { token.verify_signature!(key: 'secret') }.to raise_error(JWT::VerificationError, 'No algorithm provided')
150+
expect { token.verify_signature!(key: 'secret') }.to raise_error(ArgumentError, /missing keyword/)
145151
end
146152
end
147153

@@ -226,8 +232,22 @@
226232
.jwt
227233
end
228234

229-
it 'uses the JWK for verification' do
230-
expect(token.verify_signature!(key: jwk)).to eq(nil)
235+
context 'with empty algorithm array provided' do
236+
it 'uses the JWK for verification' do
237+
expect(token.verify_signature!(key: jwk, algorithm: [])).to eq(nil)
238+
end
239+
end
240+
241+
context 'with algorithms supported by key provided' do
242+
it 'uses the JWK for verification' do
243+
expect(token.verify_signature!(algorithm: %w[RS256 RS512], key: jwk)).to eq(nil)
244+
end
245+
end
246+
247+
context 'with algorithms not supported by key provided' do
248+
it 'raises JWT::VerificationError' do
249+
expect { token.verify_signature!(algorithm: %w[RS384 RS512], key: jwk) }.to raise_error(JWT::VerificationError, 'Provided JWKs do not support one of the specified algorithms: RS384, RS512')
250+
end
231251
end
232252
end
233253
end

spec/jwt/jwk/ec_spec.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,27 @@
139139
end
140140
end
141141
end
142+
143+
context 'when the jwk has an invalid alg header' do
144+
let(:rsa) { described_class.new(ec_key, alg: 'INVALID') }
145+
it 'raises JWT::VerificationError' do
146+
expect { rsa.verify(data: data, signature: 'signature') }.to raise_error(JWT::VerificationError, 'Algorithm not supported')
147+
end
148+
end
149+
150+
context 'when the jwk has none as the alg parameter' do
151+
let(:rsa) { described_class.new(ec_key, alg: 'none') }
152+
it 'raises JWT::JWKError' do
153+
expect { rsa.verify(data: data, signature: 'signature') }.to raise_error(JWT::JWKError, 'none algorithm usage not supported via JWK')
154+
end
155+
end
156+
157+
context 'when the jwk has HS256 as the alg parameter' do
158+
let(:rsa) { described_class.new(ec_key, alg: 'HS256') }
159+
it 'raises JWT::DecodeError' do
160+
expect { rsa.verify(data: data, signature: 'signature') }.to raise_error(JWT::DecodeError, 'HMAC key expected to be a String')
161+
end
162+
end
142163
end
143164

144165
describe '.to_openssl_curve' do

spec/jwt/jwk/rsa_spec.rb

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,24 @@
119119

120120
context 'when the jwk has an invalid alg header' do
121121
let(:rsa) { described_class.new(rsa_key, alg: 'INVALID') }
122-
it 'raises JWT::JWKError' do
122+
it 'raises JWT::VerificationError' do
123123
expect { rsa.verify(data: data, signature: 'signature') }.to raise_error(JWT::VerificationError, 'Algorithm not supported')
124124
end
125125
end
126+
127+
context 'when the jwk has none as the alg parameter' do
128+
let(:rsa) { described_class.new(rsa_key, alg: 'none') }
129+
it 'raises JWT::JWKError' do
130+
expect { rsa.verify(data: data, signature: 'signature') }.to raise_error(JWT::JWKError, 'none algorithm usage not supported via JWK')
131+
end
132+
end
133+
134+
context 'when the jwk has HS256 as the alg parameter' do
135+
let(:rsa) { described_class.new(rsa_key, alg: 'HS256') }
136+
it 'raises JWT::DecodeError' do
137+
expect { rsa.verify(data: data, signature: 'signature') }.to raise_error(JWT::DecodeError, 'HMAC key expected to be a String')
138+
end
139+
end
126140
end
127141

128142
describe '.common_parameters' do

0 commit comments

Comments
 (0)