Hi team
Finding: the rotate signature carries no chain id, so a signed rotate captured on one chain executes on any other chain sharing the key set, while recall and govern both refuse to cross chains.
Location. Gatekeeper dot sol line 182 rotate: no packed payload and no chain check anywhere in the function. Line 153 recall, line 161 the WrongChainId check. Line 200 govern, line 208 the same check. Verifier dot sol line 113 computeChallenge, line 116 the hash over tag, tag, rx, px and call: no chain id and no contract address in any of the three message types.
Executed in forge, both directions:
- The repo session 0 rotate vector, signed once, executes under chain id 42161 set by vm dot chainId, and isTransactionExecuted for session 0 becomes true on the foreign chain
- A recall payload packed for chain 31337, self-called on chain 42161, reverts with the exact error WrongChainId
- Govern on the same construction reverts identically
- The session 1 recall vector passes signature verification on chain 42161 (the challenge carries no chain id, so the signature still verifies) and then dies inside recall, surfaced as ExecutionReverted
- The session 2 govern vector behaves identically: ExecutionReverted wrapping the inner WrongChainId
The asymmetry is proven by execution, not inferred: rotate crosses chains and lands, recall and govern cross chains and revert.
Open premise, stated honestly: multi-chain key-set sameness. An attacker cannot forge a rotate signature; they must capture a legitimately signed rotate message and control only replay timing and target chain. If every chain runs an independent key set, this drops to a design note.
Suggested fix: bind the chain id into the rotate signature the way recall and govern already do.
Shahmeer