Introduces a forced WG handshake on initial MLKEM bootstrap.

To ensure two peers agree on a key, we need asymmetry. one peer is
the controller ("initiator") the other is the "responder".

Otherwise imagine two offers in parallel driving two answers at the same time

   A                   B
   | <----B-OFFER----- |
   | -----A-OFFER----> |
   |                   |
   |                   |
   ---------------------------------
  |****** ICE + WG Handshake ****** |
   ---------------------------------
   |                   |
   | <----B-ANSWER---- |
   | -----A-ANSWER---> |

PSK is derived on receive of offer, so A and B derive different PSKs.
When WG handshake takes place it picks misaligned PSKs.

So we impair the two nodes and only the offer of one of the two (the controller/initiator)
carries the KEM material.

This means that if the responder OFFER/ANSWER comes first, when the controller/initiator's one
completes (and the genuine PSK is shared between A and B, we need to force a new WG handshake with
the proper keys.
This commit is contained in:
riccardom
2026-08-06 14:42:07 +02:00
parent 7699697231
commit b5a72eca65
9 changed files with 103 additions and 15 deletions
+29
View File
@@ -736,6 +736,35 @@ func (conn *Conn) RequestReoffer() {
}
}
// ForcePQRehandshake makes WireGuard adopt a freshly bootstrapped post-quantum PSK on
// an already-up session. The KEM exchange can complete after the WG endpoint was
// configured (a race between the KEM and the relay/ICE connection coming up), so the
// live session may be keyed with a pre-PQ key (the strict sentinel, or the ordinary
// key in permissive mode); SetPresharedKey only updates config, not the running
// session. Removing and re-adding the peer forces a fresh handshake that uses the real
// PSK. No-op if not connected yet (the upcoming endpoint config will pull the PSK) or
// if no PSK exists (a non-PQ peer stays fail-closed).
func (conn *Conn) ForcePQRehandshake() {
conn.mu.Lock()
defer conn.mu.Unlock()
if conn.ctx.Err() != nil || conn.config.PQ == nil {
return
}
psk, ok := conn.config.PQ.PSK(conn.config.Key)
if !ok {
return
}
if conn.currentConnPriority == conntype.None {
return
}
conn.Log.Debugf("pqkem: bootstrap PSK ready, forcing WireGuard re-handshake to adopt it")
wgPsk := wgtypes.Key(psk)
if err := conn.endpointUpdater.ForceRehandshake(&wgPsk); err != nil {
conn.Log.Warnf("pqkem: force re-handshake failed: %v", err)
}
}
func (conn *Conn) onWGDisconnected(watcherCtx context.Context) {
conn.mu.Lock()
defer conn.mu.Unlock()