[client] pqkem: make signalling bootstrap idempotent through awaitingRekey

The controller sends its KEM offer both on its own guard event and in reply
to the responder's offer. SignalOffer was idempotent only while awaiting the
answer; once the answer arrived (awaitingRekey) a repeat call started a fresh
exchange with a different PSK, desyncing the two peers (one on the old PSK,
one on the new) so WireGuard derived misaligned transport keys and dropped all
data. Treat awaitingRekey as in-flight too and return the same offer.
This commit is contained in:
riccardom
2026-08-06 18:04:07 +02:00
parent c7e888180a
commit 1c47243da8

View File

@@ -285,7 +285,13 @@ func (m *Manager) SignalOffer(remoteID RemoteID) ([]byte, error) {
m.mu.Unlock()
return nil, nil // peer does not run the KEM; do not offer (avoids a failure/reoffer loop)
}
if ex := m.exchanges[remoteID]; ex != nil && ex.viaSignal && ex.state == stateAwaitingAnswer {
// Idempotent while a signalling bootstrap is in flight OR already derived a PSK but
// not yet chained a rotation (awaitingRekey): return the SAME offer instead of
// starting a new exchange. This matters when the controller both offers on its own
// guard AND re-offers in response to the responder's offer — without this, the
// second call would start a fresh exchange (a different PSK) and desync the peers.
if ex := m.exchanges[remoteID]; ex != nil && ex.viaSignal &&
(ex.state == stateAwaitingAnswer || ex.state == stateAwaitingRekey) {
last := ex.lastSent
m.mu.Unlock()
return last, nil