Files
netbird/client/internal/pqkem/datapath_auth.go
T
riccardom 13451079c3 [client] pqkem: authenticate data-path messages under the shared PSK
The data-path socket identified a sender only by its source IP and port and
trusted that anything reaching the overlay address came through the tunnel.
On a host-bound socket that does not hold: Linux delivers a datagram to a
local overlay IP from a physical interface too (rp_filter is loose, the
firewall's input rules match the tunnel interface), so a LAN-adjacent host
spoofing a peer's overlay source could inject a KEM offer. A forged
bootstrap offer was adopted outright, desynchronising the two PSKs until a
re-bootstrap, and repeating it sustains the outage (security review of
#7098, finding F-003). No traffic is disclosed, but the exchange must not
accept unauthenticated input.

Seal every data-path message with an HMAC keyed from the PSK the two peers
share; only a peer that holds it can rotate. A reply is sealed under the key
that verified the request, because the responder has already committed the
new PSK when it answers. The previous PSK stays valid for verification so an
offer retransmitted after that commit is still answered. Bootstraps are
refused on the data path entirely: they start without a shared PSK and only
signalling can carry them.

A rotation between peers whose configured secrets differ is now refused at
authentication, so the configured-secret test checks that instead.
2026-10-19 17:02:15 +02:00

49 lines
1.4 KiB
Go

package pqkem
import (
"crypto/hkdf"
"crypto/hmac"
"crypto/sha256"
"fmt"
)
// dataPathMACLabel separates the data-path MAC key from the PSK the consumer programs
// into its channel, so the same secret never serves two purposes directly.
const dataPathMACLabel = "netbird-pq-datapath-mac-v1"
// dataPathTagSize is the length of the HMAC-SHA256 tag appended to every data-path frame.
const dataPathTagSize = sha256.Size
// dataPathMACKey derives the key that authenticates data-path messages from a PSK both
// peers hold.
func dataPathMACKey(psk PSK) ([]byte, error) {
key, err := hkdf.Key(sha256.New, psk[:], nil, dataPathMACLabel, sha256.Size)
if err != nil {
return nil, fmt.Errorf("derive data-path mac key: %w", err)
}
return key, nil
}
// sealDataPath appends the frame's HMAC-SHA256 tag under key.
func sealDataPath(key, frame []byte) []byte {
mac := hmac.New(sha256.New, key)
mac.Write(frame)
sealed := make([]byte, 0, len(frame)+dataPathTagSize)
sealed = append(sealed, frame...)
return mac.Sum(sealed)
}
// openDataPath verifies a sealed frame under key and returns the frame without its tag.
func openDataPath(key, sealed []byte) ([]byte, bool) {
if len(sealed) < dataPathTagSize {
return nil, false
}
frame, tag := sealed[:len(sealed)-dataPathTagSize], sealed[len(sealed)-dataPathTagSize:]
mac := hmac.New(sha256.New, key)
mac.Write(frame)
if !hmac.Equal(tag, mac.Sum(nil)) {
return nil, false
}
return frame, true
}