From 0e1bd3957b34efc3c45277ea43aaa6848acad11b Mon Sep 17 00:00:00 2001 From: Riccardo Manfrin Date: Wed, 13 May 2026 16:19:59 +0200 Subject: [PATCH] [client] Moves deterministic key gen into rosenpass --- client/internal/peer/conn.go | 23 ++------------ client/internal/rosenpass/seed.go | 42 ++++++++++++++++++++++++ client/internal/rosenpass/seed_test.go | 44 ++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 21 deletions(-) create mode 100644 client/internal/rosenpass/seed.go create mode 100644 client/internal/rosenpass/seed_test.go diff --git a/client/internal/peer/conn.go b/client/internal/peer/conn.go index 1e416bfe7..79a513956 100644 --- a/client/internal/peer/conn.go +++ b/client/internal/peer/conn.go @@ -23,6 +23,7 @@ import ( "github.com/netbirdio/netbird/client/internal/peer/id" "github.com/netbirdio/netbird/client/internal/peer/worker" "github.com/netbirdio/netbird/client/internal/portforward" + "github.com/netbirdio/netbird/client/internal/rosenpass" "github.com/netbirdio/netbird/client/internal/stdnet" "github.com/netbirdio/netbird/route" relayClient "github.com/netbirdio/netbird/shared/relay/client" @@ -899,7 +900,7 @@ func (conn *Conn) presharedKey(remoteRosenpassKey []byte) *wgtypes.Key { } // Fallback to deterministic key if no NetBird PSK is configured - determKey, err := conn.rosenpassDetermKey() + determKey, err := rosenpass.DeterministicSeedKey(conn.config.LocalKey, conn.config.Key) if err != nil { conn.Log.Errorf("failed to generate Rosenpass initial key: %v", err) return nil @@ -908,26 +909,6 @@ func (conn *Conn) presharedKey(remoteRosenpassKey []byte) *wgtypes.Key { return determKey } -// todo: move this logic into Rosenpass package -func (conn *Conn) rosenpassDetermKey() (*wgtypes.Key, error) { - lk := []byte(conn.config.LocalKey) - rk := []byte(conn.config.Key) // remote key - var keyInput []byte - if string(lk) > string(rk) { - //nolint:gocritic - keyInput = append(lk[:16], rk[:16]...) - } else { - //nolint:gocritic - keyInput = append(rk[:16], lk[:16]...) - } - - key, err := wgtypes.NewKey(keyInput) - if err != nil { - return nil, err - } - return &key, nil -} - func isController(config ConnConfig) bool { return config.LocalKey > config.Key } diff --git a/client/internal/rosenpass/seed.go b/client/internal/rosenpass/seed.go new file mode 100644 index 000000000..83aba1e0e --- /dev/null +++ b/client/internal/rosenpass/seed.go @@ -0,0 +1,42 @@ +package rosenpass + +import ( + "fmt" + + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" +) + +// DeterministicSeedKey derives a 32-byte WireGuard preshared key from a pair +// of peer public keys. Both peers, given the same key pair, produce the same +// output regardless of which side runs the function: the inputs are ordered +// lexicographically before concatenation. +// +// NetBird uses this value as the initial Rosenpass-side preshared key when no +// explicit account-level PSK is configured, so both peers converge on the same +// PSK before the first post-quantum handshake completes. +// +// The resulting key MUST NOT be treated as quantum-safe: it is deterministic +// from public keys and exists only to seed WireGuard until Rosenpass rotates +// in a real post-quantum PSK. +func DeterministicSeedKey(localKey, remoteKey string) (*wgtypes.Key, error) { + lk := []byte(localKey) + rk := []byte(remoteKey) + if len(lk) < 16 || len(rk) < 16 { + return nil, fmt.Errorf("rosenpass: peer keys must be at least 16 bytes (got local=%d, remote=%d)", len(lk), len(rk)) + } + + var keyInput []byte + if localKey > remoteKey { + keyInput = append(keyInput, lk[:16]...) + keyInput = append(keyInput, rk[:16]...) + } else { + keyInput = append(keyInput, rk[:16]...) + keyInput = append(keyInput, lk[:16]...) + } + + key, err := wgtypes.NewKey(keyInput) + if err != nil { + return nil, fmt.Errorf("rosenpass: deterministic seed key: %w", err) + } + return &key, nil +} diff --git a/client/internal/rosenpass/seed_test.go b/client/internal/rosenpass/seed_test.go new file mode 100644 index 000000000..0dfa478c7 --- /dev/null +++ b/client/internal/rosenpass/seed_test.go @@ -0,0 +1,44 @@ +package rosenpass + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestDeterministicSeedKey_SameForBothSides(t *testing.T) { + // Peer A and peer B must derive the same PSK regardless of which side + // computes it: the function orders inputs internally. + a := strings.Repeat("a", 32) + b := strings.Repeat("b", 32) + + keyAB, err := DeterministicSeedKey(a, b) + require.NoError(t, err) + keyBA, err := DeterministicSeedKey(b, a) + require.NoError(t, err) + require.Equal(t, keyAB.String(), keyBA.String(), "swapping arguments must yield identical key") +} + +func TestDeterministicSeedKey_ChangesWithKeys(t *testing.T) { + a := strings.Repeat("a", 32) + b := strings.Repeat("b", 32) + c := strings.Repeat("c", 32) + + keyAB, err := DeterministicSeedKey(a, b) + require.NoError(t, err) + keyAC, err := DeterministicSeedKey(a, c) + require.NoError(t, err) + require.NotEqual(t, keyAB.String(), keyAC.String(), "different peer pair must yield different key") +} + +func TestDeterministicSeedKey_TooShortKey_ReturnsError(t *testing.T) { + short := "short" // < 16 bytes + long := strings.Repeat("x", 32) + + _, err := DeterministicSeedKey(short, long) + require.Error(t, err) + _, err = DeterministicSeedKey(long, short) + require.Error(t, err) +} +