eccsi: fix universal signature forgery via r=0/s=0

wc_VerifyEccsiHash did not validate that r and s lie in [1, q-1]
after decoding them from the signature buffer.  With s=0 the scalar
multiplication [s](...) returns the point at infinity (J_x=0); with
r=0 the final mp_cmp(0,0)==MP_EQ check then accepts the forged
signature unconditionally against any message and any identity.

Add [1, q-1] range checks for r (in wc_VerifyEccsiHash, after params
are loaded) and for s (in eccsi_calc_j, after eccsi_decode_sig_s),
mirroring the checks already present in wc_ecc_check_r_s_range.
Add a defense-in-depth point-at-infinity guard on J before the final
comparison.

Reported-by: Nicholas Carlini (Anthropic) & Bronson Yen (Calif.io)
This commit is contained in:
Tobias Frauenschläger
2026-03-30 11:57:53 +02:00
parent 10953f021b
commit 13a016367f
+35
View File
@@ -2159,6 +2159,18 @@ static int eccsi_calc_j(EccsiKey* key, const mp_int* hem, const byte* sig,
if (err == 0) {
err = eccsi_decode_sig_s(key, sig, sigSz, s);
}
/* Validate s is in [1, q-1]: reject zero or out-of-range second signature
* component. With s=0, [s](...) yields the point at infinity whose
* affine x-coordinate is 0, making the final mp_cmp(0,0) accept any
* forged signature. */
if (err == 0) {
if (mp_iszero(s)) {
err = MP_ZERO_E;
}
else if (mp_cmp(s, &key->params.order) != MP_LT) {
err = ECC_OUT_OF_RANGE_E;
}
}
/* [s]( [HE]G + [r]Y ) */
if (err == 0) {
err = eccsi_mulmod_point(key, s, j, j, 1);
@@ -2238,6 +2250,19 @@ int wc_VerifyEccsiHash(EccsiKey* key, enum wc_HashType hashType,
err = mp_montgomery_setup(&params->prime, &mp);
}
/* Validate r is in [1, q-1]: reject zero or out-of-range first signature
* component before any scalar multiplication takes place.
* Without this check, r=0 causes J_x=0 and the final mp_cmp(0,0)==MP_EQ
* comparison accepts the forged signature unconditionally. */
if (err == 0) {
if (mp_iszero(r)) {
err = MP_ZERO_E;
}
else if (mp_cmp(r, &params->order) != MP_LT) {
err = ECC_OUT_OF_RANGE_E;
}
}
/* Step 1: Validate PVT is on curve */
if (err == 0) {
err = wc_ecc_is_point(pvt, &params->a, &params->b, &params->prime);
@@ -2273,6 +2298,16 @@ int wc_VerifyEccsiHash(EccsiKey* key, enum wc_HashType hashType,
key->params.haveBase = 0;
}
/* Defense-in-depth: reject J = point at infinity before the final
* comparison. Catches any future path that might reach this point
* with a neutral-element result (e.g. s = 0 mod q for a non-zero
* encoded s). */
if (err == 0) {
if (wc_ecc_point_is_at_infinity(j)) {
err = ECC_INF_E;
}
}
/* Step 6: Jx fitting, compare with r */
if (err == 0) {
jx = &key->tmp;