From e0b5339eecb9411f525935ead0e7b9fccb6d4a45 Mon Sep 17 00:00:00 2001 From: George Date: Wed, 6 May 2026 21:40:18 +0300 Subject: [PATCH] test(verify): de-flake SignatureVerifier SV2 and SV7 invariants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same root cause as #3802: handlers with a single coverage edge get deprioritized by the corpus-guided fuzzer. With ~50k CI runs the deprioritization eventually starves a run of any SV2 (or SV7) calls and trips the coverage assertion. Changes: - Split each SV2 high-s handler (secp/p256/webauthn) into a 'flipped' variant (real-sig-derived high s) and a 'boundary' variant (s = N - 1 or N/2 + 1) — 6 handlers instead of 3. - Split each SV7 keychain handler into a 'validSig' and 'garbageSig' variant — 4 handlers instead of 2. - Extract _sv2SecpCheck/_sv2P256Check/_sv2WebAuthnCheck/_sv7Check helpers so per-handler bodies stay small. Verified with 10 runs at depth 1000 (fuzz-seed 0xdeadbeef): SV2 min: 32 -> 58, SV7 min: 20 -> 35. SV1/SV3/SV6 still have >=28 per-run minimum after the dilution. --- .../test/invariants/SignatureVerifier.t.sol | 213 ++++++++++++++---- 1 file changed, 164 insertions(+), 49 deletions(-) diff --git a/tips/verify/test/invariants/SignatureVerifier.t.sol b/tips/verify/test/invariants/SignatureVerifier.t.sol index 8f18e9be38..c53ec7f416 100644 --- a/tips/verify/test/invariants/SignatureVerifier.t.sol +++ b/tips/verify/test/invariants/SignatureVerifier.t.sol @@ -218,50 +218,116 @@ contract SignatureVerifierInvariantTest is TempoTest { SV2: MALLEABILITY RESISTANCE //////////////////////////////////////////////////////////////*/ - /// @notice SV2 (secp256k1): high-s must be rejected - function handler_sv2_secpHighS(uint256 actorSeed, bytes32 hash) external { + /// @dev Common SV2 dispatch for secp256k1 — exercised by the variants below. + function _sv2SecpCheck( + bytes32 hash, + bytes32 r, + uint256 highS, + uint8 v, + address signer + ) + internal + { + bytes memory sig = abi.encodePacked(r, bytes32(highS), v); + if (_callBothRevert(hash, sig, signer)) { + ghost_sv2_highSAllowed++; + } else { + ghost_sv2_secpHighSRejected++; + } + } + + /// @dev Common SV2 dispatch for P256 — exercised by the variants below. + function _sv2P256Check(bytes32 hash, uint256 idx, bytes32 r, uint256 highS) internal { + bytes memory sig = abi.encodePacked( + TYPE_P256, r, bytes32(highS), _p256PubX[idx], _p256PubY[idx], uint8(0) + ); + if (_callBothRevert(hash, sig, _p256Addrs[idx])) { + ghost_sv2_highSAllowed++; + } else { + ghost_sv2_p256HighSRejected++; + } + } + + /// @dev Common SV2 dispatch for WebAuthn — exercised by the variants below. + function _sv2WebAuthnCheck( + bytes32 hash, + uint256 idx, + bytes memory webauthnData, + bytes32 r, + uint256 highS + ) + internal + { + bytes memory sig = abi.encodePacked( + TYPE_WEBAUTHN, webauthnData, r, bytes32(highS), _p256PubX[idx], _p256PubY[idx] + ); + if (_callBothRevert(hash, sig, _p256Addrs[idx])) { + ghost_sv2_highSAllowed++; + } else { + ghost_sv2_webauthnHighSRejected++; + } + } + + // -------- secp256k1 high-s variants -------- + + /// @notice SV2 (secp256k1): flipped high-s from a real signature must be rejected. + function handler_sv2_secpHighS_flipped(uint256 actorSeed, bytes32 hash) external { uint256 idx = actorSeed % _secpKeys.length; (uint8 v, bytes32 r, bytes32 s) = vm.sign(_secpKeys[idx], hash); - uint256 sVal = uint256(s); uint8 highV = v; if (sVal <= _SECP256K1_N_HALF) { sVal = _SECP256K1_N - sVal; highV = v == 27 ? uint8(28) : uint8(27); } - bytes memory highSSig = abi.encodePacked(r, bytes32(sVal), highV); + _sv2SecpCheck(hash, r, sVal, highV, _secpAddrs[idx]); + } - if (_callBothRevert(hash, highSSig, _secpAddrs[idx])) { - ghost_sv2_highSAllowed++; - } else { - ghost_sv2_secpHighSRejected++; - } + /// @notice SV2 (secp256k1): boundary high-s (s = N - 1 or s = N/2 + 1) must be rejected. + function handler_sv2_secpHighS_boundary( + uint256 actorSeed, + bytes32 hash, + bytes32 r, + uint8 sel + ) + external + { + uint256 idx = actorSeed % _secpKeys.length; + uint8 v = (sel % 2 == 0) ? 27 : 28; + uint256 highS = (sel & 0x02) == 0 ? _SECP256K1_N - 1 : _SECP256K1_N_HALF + 1; + _sv2SecpCheck(hash, r, highS, v, _secpAddrs[idx]); } - /// @notice SV2 (P256): high-s must be rejected - function handler_sv2_p256HighS(uint256 actorSeed, bytes32 hash) external { + // -------- P256 high-s variants -------- + + /// @notice SV2 (P256): flipped high-s from a real signature must be rejected. + function handler_sv2_p256HighS_flipped(uint256 actorSeed, bytes32 hash) external { uint256 idx = actorSeed % _p256Keys.length; (bytes32 r, bytes32 s) = vm.signP256(_p256Keys[idx], hash); - uint256 sVal = uint256(s); if (sVal > P256N_HALF) sVal = P256_ORDER - sVal; - uint256 highS = P256_ORDER - sVal; - - bytes memory highSSig = abi.encodePacked( - TYPE_P256, r, bytes32(highS), _p256PubX[idx], _p256PubY[idx], uint8(0) - ); - - if (_callBothRevert(hash, highSSig, _p256Addrs[idx])) { - ghost_sv2_highSAllowed++; - } else { - ghost_sv2_p256HighSRejected++; - } + _sv2P256Check(hash, idx, r, P256_ORDER - sVal); } - /// @notice SV2 (WebAuthn): high-s on inner P256 sig must be rejected - function handler_sv2_webauthnHighS(uint256 actorSeed, bytes32 hash) external { + /// @notice SV2 (P256): boundary high-s (s = N - 1 or s = N/2 + 1) must be rejected. + function handler_sv2_p256HighS_boundary( + uint256 actorSeed, + bytes32 hash, + bytes32 r, + uint8 sel + ) + external + { uint256 idx = actorSeed % _p256Keys.length; + uint256 highS = (sel & 0x01) == 0 ? P256_ORDER - 1 : P256N_HALF + 1; + _sv2P256Check(hash, idx, r, highS); + } + // -------- WebAuthn high-s variants -------- + + /// @notice SV2 (WebAuthn): flipped high-s on inner P256 sig must be rejected. + function handler_sv2_webauthnHighS_flipped(uint256 actorSeed, bytes32 hash) external { + uint256 idx = actorSeed % _p256Keys.length; bytes memory webauthnData = _buildWebAuthnData(hash); bytes memory authData = _slice(webauthnData, 0, 37); bytes memory clientDataJSON = _slice(webauthnData, 37, webauthnData.length - 37); @@ -270,17 +336,23 @@ contract SignatureVerifierInvariantTest is TempoTest { (bytes32 r, bytes32 s) = vm.signP256(_p256Keys[idx], messageHash); uint256 sVal = uint256(s); if (sVal > P256N_HALF) sVal = P256_ORDER - sVal; - uint256 highS = P256_ORDER - sVal; - - bytes memory highSSig = abi.encodePacked( - TYPE_WEBAUTHN, webauthnData, r, bytes32(highS), _p256PubX[idx], _p256PubY[idx] - ); + _sv2WebAuthnCheck(hash, idx, webauthnData, r, P256_ORDER - sVal); + } - if (_callBothRevert(hash, highSSig, _p256Addrs[idx])) { - ghost_sv2_highSAllowed++; - } else { - ghost_sv2_webauthnHighSRejected++; - } + /// @notice SV2 (WebAuthn): boundary high-s (s = N - 1 or s = N/2 + 1) on inner P256 sig must + /// be rejected. + function handler_sv2_webauthnHighS_boundary( + uint256 actorSeed, + bytes32 hash, + bytes32 r, + uint8 sel + ) + external + { + uint256 idx = actorSeed % _p256Keys.length; + bytes memory webauthnData = _buildWebAuthnData(hash); + uint256 highS = (sel & 0x01) == 0 ? P256_ORDER - 1 : P256N_HALF + 1; + _sv2WebAuthnCheck(hash, idx, webauthnData, r, highS); } /*////////////////////////////////////////////////////////////// @@ -515,22 +587,47 @@ contract SignatureVerifierInvariantTest is TempoTest { SV7: KEYCHAIN REJECTION //////////////////////////////////////////////////////////////*/ - /// @notice SV7: 0x03 prefix (Keychain secp256k1) rejected with valid-looking envelope - function handler_sv7_keychainSecp(uint256 actorSeed, bytes32 hash) external { + /// @dev Common SV7 dispatch — exercised by the variants below. + function _sv7Check(bytes32 hash, bytes memory sig, address signer) internal { + if (_callBothRevert(hash, sig, signer)) { + ghost_sv7_keychainAllowed++; + } else { + ghost_sv7_keychainRejected++; + } + } + + // -------- 0x03 (Keychain secp256k1) variants -------- + + /// @notice SV7: 0x03 prefix with valid signature must be rejected. + function handler_sv7_keychainSecp_validSig(uint256 actorSeed, bytes32 hash) external { uint256 idx = actorSeed % _secpKeys.length; address user = _secpAddrs[idx]; (uint8 v, bytes32 r, bytes32 s) = vm.sign(_secpKeys[idx], hash); bytes memory sig = abi.encodePacked(TYPE_KEYCHAIN_SECP, user, r, s, v); + _sv7Check(hash, sig, user); + } - if (_callBothRevert(hash, sig, user)) { - ghost_sv7_keychainAllowed++; - } else { - ghost_sv7_keychainRejected++; - } + /// @notice SV7: 0x03 prefix with garbage r/s must be rejected. + function handler_sv7_keychainSecp_garbageSig( + uint256 actorSeed, + bytes32 hash, + bytes32 r, + bytes32 s, + uint8 vSeed + ) + external + { + uint256 idx = actorSeed % _secpKeys.length; + address user = _secpAddrs[idx]; + uint8 v = (vSeed % 2 == 0) ? 27 : 28; + bytes memory sig = abi.encodePacked(TYPE_KEYCHAIN_SECP, user, r, s, v); + _sv7Check(hash, sig, user); } - /// @notice SV7: 0x04 prefix (Keychain P256) rejected with valid-looking envelope - function handler_sv7_keychainP256(uint256 actorSeed, bytes32 hash) external { + // -------- 0x04 (Keychain P256) variants -------- + + /// @notice SV7: 0x04 prefix with valid signature must be rejected. + function handler_sv7_keychainP256_validSig(uint256 actorSeed, bytes32 hash) external { uint256 idx = actorSeed % _p256Keys.length; (bytes32 r, bytes32 s) = vm.signP256(_p256Keys[idx], hash); s = _normalizeP256S(s); @@ -544,12 +641,30 @@ contract SignatureVerifierInvariantTest is TempoTest { _p256PubY[idx], uint8(0) ); + _sv7Check(hash, sig, _p256Addrs[idx]); + } - if (_callBothRevert(hash, sig, _p256Addrs[idx])) { - ghost_sv7_keychainAllowed++; - } else { - ghost_sv7_keychainRejected++; - } + /// @notice SV7: 0x04 prefix with garbage r/s must be rejected. + function handler_sv7_keychainP256_garbageSig( + uint256 actorSeed, + bytes32 hash, + bytes32 r, + bytes32 s + ) + external + { + uint256 idx = actorSeed % _p256Keys.length; + bytes memory sig = abi.encodePacked( + TYPE_KEYCHAIN_P256, + _p256Addrs[idx], + TYPE_P256, + r, + s, + _p256PubX[idx], + _p256PubY[idx], + uint8(0) + ); + _sv7Check(hash, sig, _p256Addrs[idx]); } /*//////////////////////////////////////////////////////////////