mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-14 09:29:07 +02:00
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:
@@ -9,7 +9,13 @@ type CallbackHandler interface {
|
||||
// and must be programmed into the consumer's secure channel. It is invoked at
|
||||
// the commit point of each side: the initiator on receiving the answer, the
|
||||
// responder on receiving the confirm.
|
||||
OnNewPSKReady(remoteID RemoteID, psk PSK) error
|
||||
//
|
||||
// bootstrap is true when the PSK came from a signalling exchange (initial setup or
|
||||
// a wake re-negotiation) rather than a data-path rotation. On bootstrap the consumer
|
||||
// must ensure the live channel actually adopts the PSK now (e.g. force a WireGuard
|
||||
// re-handshake) — the channel may already be up on a pre-PQ key. A rotation
|
||||
// (bootstrap=false) is adopted at the channel's next natural rekey.
|
||||
OnNewPSKReady(remoteID RemoteID, psk PSK, bootstrap bool) error
|
||||
|
||||
// OnRekeyFailed fires when an exchange fails to converge within the allotted
|
||||
// time. The host should tear the peer connection down so it re-establishes, and
|
||||
|
||||
@@ -67,7 +67,7 @@ func (m *Manager) startExchange(remoteID RemoteID, viaSignal bool, ackID Exchang
|
||||
// (that offer riding the data path under the freshly adopted key proves it worked),
|
||||
// then derives the PSK for the new offer, commits it optimistically, and returns the
|
||||
// framed answer. A duplicate offer returns the cached answer without re-deriving.
|
||||
func (m *Manager) processOffer(remoteID RemoteID, o *OfferMsg) ([]byte, error) {
|
||||
func (m *Manager) processOffer(remoteID RemoteID, o *OfferMsg, viaSignal bool) ([]byte, error) {
|
||||
m.trace("pqkem: offer received", "peer", remoteID, "exchange", idHex(o.ExchangeID), "acks", idHex(o.AckID))
|
||||
|
||||
if o.AckID != (ExchangeID{}) {
|
||||
@@ -114,7 +114,7 @@ func (m *Manager) processOffer(remoteID RemoteID, o *OfferMsg) ([]byte, error) {
|
||||
m.trace("pqkem: new PSK derived", "peer", remoteID, "exchange", idHex(o.ExchangeID), "role", "responder", "psk_fp", pskFingerprint(psk))
|
||||
|
||||
// Commit optimistically so our data path can rekey to the new PSK.
|
||||
if err := m.cbHandler.OnNewPSKReady(remoteID, psk); err != nil {
|
||||
if err := m.cbHandler.OnNewPSKReady(remoteID, psk, viaSignal); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.trace("pqkem: answer sent", "peer", remoteID, "exchange", idHex(o.ExchangeID))
|
||||
@@ -125,7 +125,7 @@ func (m *Manager) processOffer(remoteID RemoteID, o *OfferMsg) ([]byte, error) {
|
||||
// stateAwaitingRekey; the next offer (chained from OnDataPathRekeyed) will acknowledge
|
||||
// this exchange. Only valid in stateAwaitingAnswer; advancing the state under the
|
||||
// lock makes a concurrent/duplicate answer bail.
|
||||
func (m *Manager) processAnswer(remoteID RemoteID, a *AnswerMsg) error {
|
||||
func (m *Manager) processAnswer(remoteID RemoteID, a *AnswerMsg, viaSignal bool) error {
|
||||
m.mu.Lock()
|
||||
ex := m.exchanges[remoteID]
|
||||
if ex == nil || ex.id != a.ExchangeID || ex.state != stateAwaitingAnswer {
|
||||
@@ -159,7 +159,7 @@ func (m *Manager) processAnswer(remoteID RemoteID, a *AnswerMsg) error {
|
||||
|
||||
m.trace("pqkem: new PSK derived", "peer", remoteID, "exchange", idHex(a.ExchangeID), "role", "initiator", "psk_fp", pskFingerprint(psk))
|
||||
|
||||
return m.cbHandler.OnNewPSKReady(remoteID, psk)
|
||||
return m.cbHandler.OnNewPSKReady(remoteID, psk, viaSignal)
|
||||
}
|
||||
|
||||
// ackConverged (responder) records convergence of the exchange named by ackID: a
|
||||
|
||||
@@ -305,7 +305,7 @@ func (m *Manager) SignalOnOffer(remoteID RemoteID, offer []byte) ([]byte, error)
|
||||
if typ != MsgOffer {
|
||||
return nil, fmt.Errorf("expected offer from %s, got type %d", remoteID, typ)
|
||||
}
|
||||
return m.processOffer(remoteID, msg.(*OfferMsg))
|
||||
return m.processOffer(remoteID, msg.(*OfferMsg), true)
|
||||
}
|
||||
|
||||
// SignalOnAnswer processes a KEM answer the host extracted from an incoming answer.
|
||||
@@ -318,7 +318,7 @@ func (m *Manager) SignalOnAnswer(remoteID RemoteID, answer []byte) error {
|
||||
if typ != MsgAnswer {
|
||||
return fmt.Errorf("expected answer from %s, got type %d", remoteID, typ)
|
||||
}
|
||||
return m.processAnswer(remoteID, msg.(*AnswerMsg))
|
||||
return m.processAnswer(remoteID, msg.(*AnswerMsg), true)
|
||||
}
|
||||
|
||||
// ---- Data path ----
|
||||
@@ -346,7 +346,7 @@ func (m *Manager) OnDataPathMessage(remoteID RemoteID, raw []byte) error {
|
||||
}
|
||||
switch typ {
|
||||
case MsgOffer:
|
||||
answer, err := m.processOffer(remoteID, msg.(*OfferMsg))
|
||||
answer, err := m.processOffer(remoteID, msg.(*OfferMsg), false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -355,7 +355,7 @@ func (m *Manager) OnDataPathMessage(remoteID RemoteID, raw []byte) error {
|
||||
}
|
||||
return m.pushDataPath(remoteID, answer)
|
||||
case MsgAnswer:
|
||||
return m.processAnswer(remoteID, msg.(*AnswerMsg))
|
||||
return m.processAnswer(remoteID, msg.(*AnswerMsg), false)
|
||||
default:
|
||||
return fmt.Errorf("unhandled data-path message type %d from %s", typ, remoteID)
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ type fakeWG struct {
|
||||
|
||||
func newFakeWG() *fakeWG { return &fakeWG{psks: map[RemoteID]PSK{}} }
|
||||
|
||||
func (f *fakeWG) OnNewPSKReady(remoteID RemoteID, psk PSK) error {
|
||||
func (f *fakeWG) OnNewPSKReady(remoteID RemoteID, psk PSK, bootstrap bool) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.psks[remoteID] = psk
|
||||
|
||||
Reference in New Issue
Block a user