Skip to content

Commit 50bb298

Browse files
committed
Raise EncodeError for every signing failure
Signing with an ECDSA key on a mismatched or unsupported curve raised IncorrectAlgorithm or UnsupportedEcdsaCurve, and an invalid HMAC key raised through the verify helper. Under the new hierarchy those are SignatureError < TokenError, so `JWT.encode` could fail with an error that is not an EncodeError, unlike its other signing failures and the Token#sign! contract. The ECDSA sign path now resolves the curve through a signing-side helper that raises EncodeError, and the HMAC key validation yields its message so sign and verify each raise their own error class. Verify paths are unchanged. Specs: rename the ECDSA sign examples that claimed VerificationError while asserting EncodeError, cover the curve mismatch on sign, and split the curve_name spec into an encode and a decode case. The JWK "ES384 key pointed to as ES512 key" spec was loading the P-384 fixture for both keys and only passed because encoding failed first; it now uses the P-521 fixture and exercises verification.
1 parent a694655 commit 50bb298

7 files changed

Lines changed: 50 additions & 37 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
**Features:**
88

9-
- Revamp error hierarchy: introduce `JWT::Error`, `JWT::TokenError`, `JWT::MalformedTokenError`, `JWT::SignatureError`, and `JWT::ClaimValidationError` grouping classes. `JWT::DecodeError` is now a deprecated alias for `JWT::Error`; because of this, `rescue JWT::DecodeError` now also catches `JWT::EncodeError` [#722](https://github.com/jwt/ruby-jwt/pull/722) ([@anakinj](https://github.com/anakinj))
9+
- Revamp error hierarchy: introduce `JWT::Error`, `JWT::TokenError`, `JWT::MalformedTokenError`, `JWT::SignatureError`, and `JWT::ClaimValidationError` grouping classes. `JWT::DecodeError` is now a deprecated alias for `JWT::Error`; because of this, `rescue JWT::DecodeError` now also catches `JWT::EncodeError`. Signing failures now consistently raise `JWT::EncodeError`: an ECDSA signing key with a mismatched or unsupported curve and an invalid HMAC signing key previously surfaced as `JWT::IncorrectAlgorithm`, `JWT::UnsupportedEcdsaCurve` or `JWT::DecodeError` from `JWT.encode` [#722](https://github.com/jwt/ruby-jwt/pull/722) ([@anakinj](https://github.com/anakinj))
1010
- Your contribution here
1111

1212
**Fixes and enhancements:**

lib/jwt/jwa/ecdsa.rb

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,8 @@ def sign(data:, signing_key:)
1515
raise_sign_error!("The given key is a #{signing_key.class}. It has to be an OpenSSL::PKey::EC instance") unless signing_key.is_a?(::OpenSSL::PKey::EC)
1616
raise_sign_error!('The given key is not a private key') unless signing_key.private?
1717

18-
curve_definition = curve_by_name(signing_key.group.curve_name)
19-
key_algorithm = curve_definition[:algorithm]
20-
21-
raise IncorrectAlgorithm, "payload algorithm is #{alg} but #{key_algorithm} signing key was provided" if alg != key_algorithm
18+
key_algorithm = signing_key_algorithm(signing_key)
19+
raise_sign_error!("payload algorithm is #{alg} but #{key_algorithm} signing key was provided") if alg != key_algorithm
2220

2321
asn1_to_raw(signing_key.dsa_sign_asn1(OpenSSL::Digest.new(digest).digest(data)), signing_key)
2422
end
@@ -95,6 +93,13 @@ def curve_by_name(name)
9593
self.class.curve_by_name(name)
9694
end
9795

96+
# Signing-side counterpart of {.curve_by_name}. An unsupported curve on the
97+
# signing key is an encoding problem, so it raises a JWT::EncodeError.
98+
def signing_key_algorithm(signing_key)
99+
curve_name = signing_key.group.curve_name
100+
NAMED_CURVES.fetch(curve_name) { raise_sign_error!("The ECDSA curve '#{curve_name}' is not supported") }[:algorithm]
101+
end
102+
98103
def raw_to_asn1(signature, private_key)
99104
byte_size = (private_key.group.degree + 7) / 8
100105
sig_bytes = signature[0..(byte_size - 1)]

lib/jwt/jwa/hmac.rb

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,13 @@ def initialize(alg, digest)
2121
end
2222

2323
def sign(data:, signing_key:)
24-
ensure_valid_key!(signing_key)
25-
validate_key_length!(signing_key)
24+
validate_key!(signing_key) { |message| raise_sign_error!(message) }
2625

2726
OpenSSL::HMAC.digest(digest.new, signing_key, data)
2827
end
2928

3029
def verify(data:, signature:, verification_key:)
31-
ensure_valid_key!(verification_key)
32-
validate_key_length!(verification_key)
30+
validate_key!(verification_key) { |message| raise_verify_error!(message) }
3331

3432
SecurityUtils.secure_compare(signature, OpenSSL::HMAC.digest(digest.new, verification_key, data))
3533
end
@@ -42,18 +40,16 @@ def verify(data:, signature:, verification_key:)
4240

4341
attr_reader :digest
4442

45-
def ensure_valid_key!(key)
46-
raise_verify_error!('HMAC key expected to be a String') unless key.is_a?(String)
47-
raise_verify_error!('HMAC key cannot be empty') if key.empty?
48-
end
43+
# Yields a message for the first problem found with the key. The caller
44+
# raises it, so signing and verification failures keep their own error class.
45+
def validate_key!(key)
46+
yield 'HMAC key expected to be a String' unless key.is_a?(String)
47+
yield 'HMAC key cannot be empty' if key.empty?
4948

50-
def validate_key_length!(key)
5149
return unless JWT.configuration.decode.enforce_hmac_key_length
5250

5351
min_length = MIN_KEY_LENGTHS[alg]
54-
return if key.bytesize >= min_length
55-
56-
raise_verify_error!("HMAC key must be at least #{min_length} bytes for #{alg} algorithm")
52+
yield "HMAC key must be at least #{min_length} bytes for #{alg} algorithm" if key.bytesize < min_length
5753
end
5854

5955
# Copy of https://github.com/rails/rails/blob/v7.0.3.1/activesupport/lib/active_support/security_utils.rb

spec/jwt/jwa/ecdsa_spec.rb

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@
8282
end
8383

8484
context 'when the signing key is a public key' do
85-
it 'raises a JWT::VerificationError' do
85+
it 'raises a JWT::EncodeError' do
8686
public_key = test_pkey('ec256-public.pem')
8787
expect do
8888
instance.sign(data: data, signing_key: public_key)
@@ -91,19 +91,28 @@
9191
end
9292

9393
context 'when the signing key is not an OpenSSL::PKey::EC instance' do
94-
it 'raises a JWT::VerificationError' do
94+
it 'raises a JWT::EncodeError' do
9595
expect do
9696
instance.sign(data: data, signing_key: 'not_a_key')
9797
end.to raise_error(JWT::EncodeError, 'The given key is a String. It has to be an OpenSSL::PKey::EC instance')
9898
end
9999
end
100100

101-
context 'when the signing key is invalid' do
102-
it 'raises a JWT::UnsupportedEcdsaCurve' do
101+
context 'when the signing key uses an unsupported curve' do
102+
it 'raises a JWT::EncodeError' do
103103
invalid_key = OpenSSL::PKey::EC.generate('sect571r1')
104104
expect do
105105
instance.sign(data: data, signing_key: invalid_key)
106-
end.to raise_error(JWT::UnsupportedEcdsaCurve, "The ECDSA curve 'sect571r1' is not supported")
106+
end.to raise_error(JWT::EncodeError, "The ECDSA curve 'sect571r1' is not supported")
107+
end
108+
end
109+
110+
context 'when the signing key is for another curve' do
111+
it 'raises a JWT::EncodeError' do
112+
other_curve_key = OpenSSL::PKey::EC.generate('secp384r1')
113+
expect do
114+
instance.sign(data: data, signing_key: other_curve_key)
115+
end.to raise_error(JWT::EncodeError, 'payload algorithm is ES256 but ES384 signing key was provided')
107116
end
108117
end
109118
end

spec/jwt/jwa/hmac_spec.rb

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,26 @@
1717
context 'when nil hmac_secret is passed' do
1818
let(:hmac_secret) { nil }
1919

20-
it 'raises JWT::VerificationError' do
21-
expect { subject }.to raise_error(JWT::VerificationError, 'HMAC key expected to be a String')
20+
it 'raises JWT::EncodeError' do
21+
expect { subject }.to raise_error(JWT::EncodeError, 'HMAC key expected to be a String')
2222
end
2323

2424
it 'does not call OpenSSL::HMAC.digest' do
2525
expect(OpenSSL::HMAC).not_to receive(:digest)
26-
expect { subject }.to raise_error(JWT::VerificationError)
26+
expect { subject }.to raise_error(JWT::EncodeError)
2727
end
2828
end
2929

3030
context 'when blank hmac_secret is passed' do
3131
let(:hmac_secret) { '' }
3232

33-
it 'raises JWT::VerificationError' do
34-
expect { subject }.to raise_error(JWT::VerificationError, 'HMAC key cannot be empty')
33+
it 'raises JWT::EncodeError' do
34+
expect { subject }.to raise_error(JWT::EncodeError, 'HMAC key cannot be empty')
3535
end
3636

3737
it 'does not call OpenSSL::HMAC.digest' do
3838
expect(OpenSSL::HMAC).not_to receive(:digest)
39-
expect { subject }.to raise_error(JWT::VerificationError)
39+
expect { subject }.to raise_error(JWT::EncodeError)
4040
end
4141
end
4242

@@ -85,7 +85,7 @@
8585
let(:hmac_secret) { 'short' }
8686

8787
it 'raises error' do
88-
expect { subject }.to raise_error(JWT::VerificationError, 'HMAC key must be at least 32 bytes for HS256 algorithm')
88+
expect { subject }.to raise_error(JWT::EncodeError, 'HMAC key must be at least 32 bytes for HS256 algorithm')
8989
end
9090
end
9191

spec/jwt/jwk/decode_with_jwk_spec.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@
125125
let(:hmac_jwk) { JWT::JWK.new('secret') }
126126
let(:rsa_jwk) { JWT::JWK.new(test_pkey('rsa-2048-private.pem')) }
127127
let(:ec_jwk_secp384r1) { JWT::JWK.new(test_pkey('ec384-private.pem')) }
128-
let(:ec_jwk_secp521r1) { JWT::JWK.new(test_pkey('ec384-private.pem')) }
128+
let(:ec_jwk_secp521r1) { JWT::JWK.new(test_pkey('ec512-private.pem')) }
129129
let(:jwks) { { keys: [hmac_jwk.export(include_private: true), rsa_jwk.export, ec_jwk_secp384r1.export, ec_jwk_secp521r1.export] } }
130130

131131
context 'when RSA key is pointed to as HMAC secret' do
@@ -175,11 +175,11 @@
175175
end
176176

177177
context 'when ES384 key is pointed to as ES512 key' do
178-
let(:signed_token) { described_class.encode({ 'foo' => 'bar' }, ec_jwk_secp384r1.signing_key, 'ES512', { kid: ec_jwk_secp521r1.kid }) }
178+
let(:signed_token) { described_class.encode({ 'foo' => 'bar' }, ec_jwk_secp384r1.signing_key, 'ES384', { kid: ec_jwk_secp521r1.kid }) }
179179

180180
it 'fails in some way' do
181-
expect { described_class.decode(signed_token, nil, true, algorithms: ['ES512'], jwks: jwks) }.to(
182-
raise_error(JWT::IncorrectAlgorithm, 'payload algorithm is ES512 but ES384 signing key was provided')
181+
expect { described_class.decode(signed_token, nil, true, algorithms: ['ES384'], jwks: jwks) }.to(
182+
raise_error(JWT::IncorrectAlgorithm, 'payload algorithm is ES384 but ES512 verification key was provided')
183183
)
184184
end
185185
end

spec/jwt/jwt_spec.rb

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -260,18 +260,21 @@
260260
expect { JWT.decode(token, nil, true) }.to raise_error(JWT::SignatureError, 'No verification key available')
261261
end
262262

263-
it 'ECDSA curve_name should raise JWT::IncorrectAlgorithm' do
263+
it 'ECDSA curve_name mismatch should raise JWT::EncodeError when encoding' do
264264
key = OpenSSL::PKey::EC.generate('secp256k1')
265265

266266
expect do
267267
JWT.encode payload, key, 'ES256'
268-
end.to raise_error JWT::IncorrectAlgorithm
268+
end.to raise_error JWT::EncodeError, 'payload algorithm is ES256 but ES256K signing key was provided'
269+
end
269270

271+
it 'ECDSA curve_name mismatch should raise JWT::IncorrectAlgorithm when decoding' do
272+
key = OpenSSL::PKey::EC.generate('secp256k1')
270273
token = JWT.encode payload, data['ES256_private'], 'ES256'
271274

272275
expect do
273-
JWT.decode token, key
274-
end.to raise_error JWT::IncorrectAlgorithm
276+
JWT.decode token, key, true, algorithm: 'ES256'
277+
end.to raise_error JWT::IncorrectAlgorithm, 'payload algorithm is ES256 but ES256K verification key was provided'
275278
end
276279
end
277280

0 commit comments

Comments
 (0)