mirror of
https://github.com/netbirdio/netbird.git
synced 2026-08-04 19:45:14 -04:00
pqkem: strict (fail-closed) mode + wire status Quantum resistance
Strict mode (NB_PQ_MLKEM_STRICT, default off) closes the initial PQ-vulnerable window (NET-1408): when enabled, conn.presharedKey programs a per-conn random sentinel PSK until the ML-KEM exchange derives the real one, so no session can form on a non-PQ key (the real PSK is pushed via SetPresharedKey once it converges). Default stays opportunistic. Also surface PQ status: the peer 'Quantum resistance' flag (RosenpassEnabled) is now true when an ML-KEM PSK has been derived for the peer, not only for Rosenpass.
This commit is contained in:
@@ -1917,6 +1917,7 @@ func (e *Engine) createPeerConn(pubKey string, allowedIPs []netip.Prefix, agentV
|
||||
}
|
||||
if e.pqkemManager != nil {
|
||||
config.PQ = pqHandshaker{mgr: e.pqkemManager}
|
||||
config.PQStrict = pqkem.Strict()
|
||||
}
|
||||
|
||||
serviceDependencies := peer.ServiceDependencies{
|
||||
|
||||
@@ -123,6 +123,9 @@ type ConnConfig struct {
|
||||
|
||||
// PQ carries post-quantum ML-KEM material on offers/answers; nil when disabled.
|
||||
PQ PQHandshaker
|
||||
// PQStrict fails closed: block peer traffic until the ML-KEM PSK is established,
|
||||
// instead of letting the tunnel come up classically and upgrading to PQ later.
|
||||
PQStrict bool
|
||||
|
||||
// ICEConfig ICE protocol configuration
|
||||
ICEConfig icemaker.Config
|
||||
@@ -182,6 +185,11 @@ type Conn struct {
|
||||
// pendingFirstPacket is the lazyconn-captured handshake init, replayed once the real
|
||||
// transport is up.
|
||||
pendingFirstPacket []byte
|
||||
|
||||
// pqBlockingKey is a per-conn random sentinel PSK used in PQ strict mode to fail
|
||||
// closed: it is programmed until the real ML-KEM PSK is derived, so no session can
|
||||
// form on a non-PQ key. Per-conn random so two strict peers never match by chance.
|
||||
pqBlockingKey *wgtypes.Key
|
||||
}
|
||||
|
||||
// injectPendingFirstPacket replays the captured handshake through the proxy if present, else
|
||||
@@ -239,6 +247,14 @@ func NewConn(config ConnConfig, services ServiceDependencies) (*Conn, error) {
|
||||
metricsRecorder: services.MetricsRecorder,
|
||||
}
|
||||
|
||||
if config.PQ != nil && config.PQStrict {
|
||||
if k, err := wgtypes.GenerateKey(); err != nil {
|
||||
connLog.Errorf("pqkem: failed to generate strict-mode sentinel key, strict fail-closed disabled for this peer: %v", err)
|
||||
} else {
|
||||
conn.pqBlockingKey = &k
|
||||
}
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
@@ -760,7 +776,7 @@ func (conn *Conn) updateRelayStatus(relayServerAddr string, rosenpassPubKey []by
|
||||
ConnStatus: conn.evalStatus(),
|
||||
Relayed: conn.isRelayed(),
|
||||
RelayServerAddress: relayServerAddr,
|
||||
RosenpassEnabled: isRosenpassEnabled(rosenpassPubKey),
|
||||
RosenpassEnabled: conn.quantumResistant(rosenpassPubKey),
|
||||
}
|
||||
|
||||
err := conn.statusRecorder.UpdatePeerRelayedState(peerState)
|
||||
@@ -779,7 +795,7 @@ func (conn *Conn) updateIceState(iceConnInfo ICEConnInfo, updateTime time.Time)
|
||||
RemoteIceCandidateType: iceConnInfo.RemoteIceCandidateType,
|
||||
LocalIceCandidateEndpoint: iceConnInfo.LocalIceCandidateEndpoint,
|
||||
RemoteIceCandidateEndpoint: iceConnInfo.RemoteIceCandidateEndpoint,
|
||||
RosenpassEnabled: isRosenpassEnabled(iceConnInfo.RosenpassPubKey),
|
||||
RosenpassEnabled: conn.quantumResistant(iceConnInfo.RosenpassPubKey),
|
||||
}
|
||||
|
||||
err := conn.statusRecorder.UpdatePeerICEState(peerState)
|
||||
@@ -1060,6 +1076,14 @@ func (conn *Conn) presharedKey(remoteRosenpassKey []byte) *wgtypes.Key {
|
||||
if psk, ok := conn.config.PQ.PSK(conn.config.Key); ok {
|
||||
return &psk
|
||||
}
|
||||
if conn.config.PQStrict && conn.pqBlockingKey != nil {
|
||||
// Fail closed: program a non-matching sentinel so no session forms on a
|
||||
// non-PQ key until the ML-KEM exchange derives the real PSK (pushed via
|
||||
// SetPresharedKey once it converges). "pending" — turns into a "stuck"
|
||||
// warning from the manager if the exchange keeps failing (see raiseFailure).
|
||||
conn.Log.Debugf("pqkem: strict mode — no PQ PSK yet, blocking peer traffic until the ML-KEM exchange converges")
|
||||
return conn.pqBlockingKey
|
||||
}
|
||||
}
|
||||
|
||||
if conn.config.RosenpassConfig.PubKey == nil {
|
||||
@@ -1101,6 +1125,21 @@ func isRosenpassEnabled(remoteRosenpassPubKey []byte) bool {
|
||||
return remoteRosenpassPubKey != nil
|
||||
}
|
||||
|
||||
// quantumResistant reports whether the peer's tunnel is post-quantum protected, for
|
||||
// the status "Quantum resistance" field: either Rosenpass (the remote advertised a
|
||||
// Rosenpass key) or the ML-KEM exchange (a PQ PSK has been derived for this peer).
|
||||
func (conn *Conn) quantumResistant(remoteRosenpassPubKey []byte) bool {
|
||||
if isRosenpassEnabled(remoteRosenpassPubKey) {
|
||||
return true
|
||||
}
|
||||
if conn.config.PQ != nil {
|
||||
if _, ok := conn.config.PQ.PSK(conn.config.Key); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func evalConnStatus(in connStatusInputs) guard.ConnStatus {
|
||||
// "Relay up and needed" — the peer uses relay and the transport is connected.
|
||||
relayUsedAndUp := in.peerUsesRelay && in.relayConnected
|
||||
|
||||
@@ -35,6 +35,28 @@ func Enabled() bool {
|
||||
return enabled
|
||||
}
|
||||
|
||||
// EnvStrict enables strict (fail-closed) mode: block peer traffic until the ML-KEM
|
||||
// PSK has been established, instead of the default opportunistic behaviour that lets
|
||||
// the tunnel come up classically and upgrades to PQ once the exchange converges.
|
||||
const EnvStrict = "NB_PQ_MLKEM_STRICT"
|
||||
|
||||
// Strict reports whether strict (fail-closed) mode is enabled via the environment.
|
||||
// An empty or unrecognized value is treated as disabled (opportunistic).
|
||||
func Strict() bool {
|
||||
switch strings.ToLower(strings.TrimSpace(os.Getenv(EnvStrict))) {
|
||||
case "on":
|
||||
return true
|
||||
case "", "off":
|
||||
return false
|
||||
}
|
||||
enabled, err := strconv.ParseBool(strings.TrimSpace(os.Getenv(EnvStrict)))
|
||||
if err != nil {
|
||||
log.Warnf("failed to parse %s value %q: %v", EnvStrict, os.Getenv(EnvStrict), err)
|
||||
return false
|
||||
}
|
||||
return enabled
|
||||
}
|
||||
|
||||
// EnvLogLevel overrides the ML-KEM manager's slog level (trace/debug/info/warn/error).
|
||||
// Defaults to info. The verbose per-exchange lifecycle logs are emitted at trace.
|
||||
const EnvLogLevel = "NB_PQ_MLKEM_LOG_LEVEL"
|
||||
|
||||
Reference in New Issue
Block a user