Epurate wg refs

This commit is contained in:
riccardom
2026-07-23 11:03:40 +02:00
parent 37ce7b4f6f
commit 6cef0d8bee
8 changed files with 124 additions and 124 deletions

View File

@@ -1,19 +1,18 @@
package pqkem
// WGCallbackHandler is implemented by the host (the NetBird wiring layer) and
// invoked by the library. The library only reports events; the host owns the
// WireGuard reaction. Keeping this an interface — rather than calling wgctrl
// directly — is what lets the KEM code be extracted as a standalone library.
type WGCallbackHandler interface {
// OnNewPSKReady fires when a fresh post-quantum PSK has been derived for a
// peer and must be programmed on the WireGuard interface. It is invoked at the
// commit point of each side: the initiator on receiving the answer, the
// CallbackHandler is implemented by the host and invoked by the library. The
// library only reports events; the host owns the reaction. Keeping this an
// interface — rather than touching the transport or keying directly — is what lets
// the KEM code be extracted as a standalone library.
type CallbackHandler interface {
// OnNewPSKReady fires when a fresh post-quantum PSK has been derived for a peer
// 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(remoteWgKey string, psk PSK) error
OnNewPSKReady(remoteID string, psk PSK) error
// OnRekeyFailed fires when an exchange fails to converge within the allotted
// time. The host should tear the peer connection down so ICE re-establishes,
// and log a WARN. The library reports the event; it does not dictate the
// reaction.
OnRekeyFailed(remoteWgKey string) error
// time. The host should tear the peer connection down so it re-establishes, and
// log a WARN. The library reports the event; it does not dictate the reaction.
OnRekeyFailed(remoteID string) error
}

View File

@@ -8,9 +8,9 @@ import (
// handleOffer (responder) derives and sends the answer for a new exchange, or
// resends the cached answer for a duplicate offer (same exchangeID) without
// re-deriving. The responder is purely reactive: no retransmit loop, no deadline.
func (m *Manager) handleOffer(remoteWgKey string, o *OfferMsg) error {
func (m *Manager) handleOffer(remoteID string, o *OfferMsg) error {
m.mu.Lock()
if ex := m.exchanges[remoteWgKey]; ex != nil && ex.id == o.ExchangeID {
if ex := m.exchanges[remoteID]; ex != nil && ex.id == o.ExchangeID {
state, last := ex.state, ex.lastSent
m.mu.Unlock()
if state == stateReserved {
@@ -18,13 +18,13 @@ func (m *Manager) handleOffer(remoteWgKey string, o *OfferMsg) error {
// dropping avoids a second (randomized -> divergent) derivation.
return nil
}
return m.transport.Send(remoteWgKey, last)
return m.transport.Send(remoteID, last)
}
// Reserve the slot so a concurrent duplicate offer bails.
m.exchanges[remoteWgKey] = &exchangeCtl{id: o.ExchangeID, state: stateReserved, startedAt: time.Now()}
m.exchanges[remoteID] = &exchangeCtl{id: o.ExchangeID, state: stateReserved, startedAt: time.Now()}
m.mu.Unlock()
answerBytes, psk, err := Respond(o.KEMOffer, m.binding(remoteWgKey))
answerBytes, psk, err := Respond(o.KEMOffer, m.binding(remoteID))
if err != nil {
return err
}
@@ -34,22 +34,22 @@ func (m *Manager) handleOffer(remoteWgKey string, o *OfferMsg) error {
}
m.mu.Lock()
if ex := m.exchanges[remoteWgKey]; ex != nil && ex.id == o.ExchangeID {
if ex := m.exchanges[remoteID]; ex != nil && ex.id == o.ExchangeID {
ex.state = stateAwaitingConfirm
ex.lastSent = raw
ex.pendingPSK = psk
}
m.mu.Unlock()
return m.transport.Send(remoteWgKey, raw)
return m.transport.Send(remoteID, raw)
}
// handleAnswer (initiator) derives the PSK, surfaces it, and sends the confirm, then
// switches the retransmit payload to the confirm. Only valid in stateAwaitingAnswer;
// advancing the state under the lock makes a concurrent/duplicate answer bail.
func (m *Manager) handleAnswer(remoteWgKey string, a *AnswerMsg) error {
func (m *Manager) handleAnswer(remoteID string, a *AnswerMsg) error {
m.mu.Lock()
ex := m.exchanges[remoteWgKey]
ex := m.exchanges[remoteID]
if ex == nil || ex.id != a.ExchangeID || ex.state != stateAwaitingAnswer {
m.mu.Unlock()
return nil
@@ -59,11 +59,11 @@ func (m *Manager) handleAnswer(remoteWgKey string, a *AnswerMsg) error {
ex.initiator = nil
m.mu.Unlock()
psk, err := init.Finish(a.KEMAnswer, m.binding(remoteWgKey))
psk, err := init.Finish(a.KEMAnswer, m.binding(remoteID))
if err != nil {
return err
}
if err := m.wg.OnNewPSKReady(remoteWgKey, psk); err != nil {
if err := m.cbHandler.OnNewPSKReady(remoteID, psk); err != nil {
return err
}
raw, err := (&ConfirmMsg{ExchangeID: a.ExchangeID}).Encode()
@@ -72,42 +72,42 @@ func (m *Manager) handleAnswer(remoteWgKey string, a *AnswerMsg) error {
}
m.mu.Lock()
if ex := m.exchanges[remoteWgKey]; ex != nil && ex.id == a.ExchangeID {
if ex := m.exchanges[remoteID]; ex != nil && ex.id == a.ExchangeID {
ex.lastSent = raw // loop now retransmits the confirm
m.established[remoteWgKey] = true
m.failures[remoteWgKey] = 0
m.established[remoteID] = true
m.failures[remoteID] = 0
_ = time.Since(ex.startedAt) // convergence latency (metrics hook, later step)
}
m.mu.Unlock()
return m.transport.Send(remoteWgKey, raw)
return m.transport.Send(remoteID, raw)
}
// handleConfirm (responder) commits the pending PSK. Only valid in
// stateAwaitingConfirm; duplicate confirms (the initiator best-effort resends it)
// find the exchange gone and are ignored.
func (m *Manager) handleConfirm(remoteWgKey string, c *ConfirmMsg) error {
func (m *Manager) handleConfirm(remoteID string, c *ConfirmMsg) error {
m.mu.Lock()
ex := m.exchanges[remoteWgKey]
ex := m.exchanges[remoteID]
if ex == nil || ex.id != c.ExchangeID || ex.state != stateAwaitingConfirm {
m.mu.Unlock()
return nil
}
psk := ex.pendingPSK
delete(m.exchanges, remoteWgKey)
m.established[remoteWgKey] = true
m.failures[remoteWgKey] = 0
delete(m.exchanges, remoteID)
m.established[remoteID] = true
m.failures[remoteID] = 0
_ = time.Since(ex.startedAt) // convergence latency (metrics hook, later step)
m.mu.Unlock()
return m.wg.OnNewPSKReady(remoteWgKey, psk)
return m.cbHandler.OnNewPSKReady(remoteID, psk)
}
// initiatorLoop retransmits the initiator's outstanding message, keyed off the
// exchange state: the offer while awaiting the answer (bounded by maxRetries ->
// failure), then the confirm a few best-effort times before stopping. The
// convergence deadline is thus derived from maxRetries * retryInterval.
func (m *Manager) initiatorLoop(ctx context.Context, remoteWgKey string, id ExchangeID) {
func (m *Manager) initiatorLoop(ctx context.Context, remoteID string, id ExchangeID) {
defer m.wait.Done()
t := time.NewTicker(m.retryInterval)
defer t.Stop()
@@ -119,7 +119,7 @@ func (m *Manager) initiatorLoop(ctx context.Context, remoteWgKey string, id Exch
return
case <-t.C:
m.mu.Lock()
ex := m.exchanges[remoteWgKey]
ex := m.exchanges[remoteID]
if ex == nil || ex.id != id {
m.mu.Unlock()
return
@@ -128,27 +128,27 @@ func (m *Manager) initiatorLoop(ctx context.Context, remoteWgKey string, id Exch
switch ex.state {
case stateAwaitingAnswer:
if offerAttempts >= m.maxRetries {
delete(m.exchanges, remoteWgKey)
fail := m.registerFailureLocked(remoteWgKey)
delete(m.exchanges, remoteID)
fail := m.registerFailureLocked(remoteID)
m.mu.Unlock()
m.raiseFailure(remoteWgKey, fail)
m.raiseFailure(remoteID, fail)
return
}
msg := ex.lastSent
offerAttempts++
m.mu.Unlock()
m.retransmit(remoteWgKey, msg)
m.retransmit(remoteID, msg)
case stateConfirming:
if confirmsSent >= confirmRetransmits {
delete(m.exchanges, remoteWgKey)
delete(m.exchanges, remoteID)
m.mu.Unlock()
return
}
msg := ex.lastSent
confirmsSent++
m.mu.Unlock()
m.retransmit(remoteWgKey, msg)
m.retransmit(remoteID, msg)
default:
m.mu.Unlock()
@@ -162,30 +162,30 @@ func (m *Manager) initiatorLoop(ctx context.Context, remoteWgKey string, id Exch
// an initial exchange (peer never established) fails immediately; a rekey tolerates
// up to maxRekeyFailures consecutive misses (we stay on the still-valid previous
// PSK) before failing. Assumes m.mu is held.
func (m *Manager) registerFailureLocked(remoteWgKey string) bool {
if !m.established[remoteWgKey] {
func (m *Manager) registerFailureLocked(remoteID string) bool {
if !m.established[remoteID] {
return true
}
m.failures[remoteWgKey]++
if m.failures[remoteWgKey] >= m.maxRekeyFailures {
m.failures[remoteWgKey] = 0
m.failures[remoteID]++
if m.failures[remoteID] >= m.maxRekeyFailures {
m.failures[remoteID] = 0
return true
}
return false
}
func (m *Manager) raiseFailure(remoteWgKey string, fail bool) {
func (m *Manager) raiseFailure(remoteID string, fail bool) {
if !fail {
m.logger.Warn("pqkem rekey attempt timed out, will retry next cycle", "peer", remoteWgKey)
m.logger.Warn("pqkem rekey attempt timed out, will retry next cycle", "peer", remoteID)
return
}
if err := m.wg.OnRekeyFailed(remoteWgKey); err != nil {
m.logger.Error("pqkem OnRekeyFailed handler error", "peer", remoteWgKey, "err", err)
if err := m.cbHandler.OnRekeyFailed(remoteID); err != nil {
m.logger.Error("pqkem OnRekeyFailed handler error", "peer", remoteID, "err", err)
}
}
func (m *Manager) retransmit(remoteWgKey string, msg []byte) {
if err := m.transport.Send(remoteWgKey, msg); err != nil {
m.logger.Warn("pqkem retransmit failed", "peer", remoteWgKey, "err", err)
func (m *Manager) retransmit(remoteID string, msg []byte) {
if err := m.transport.Send(remoteID, msg); err != nil {
m.logger.Warn("pqkem retransmit failed", "peer", remoteID, "err", err)
}
}

View File

@@ -1,6 +1,6 @@
// Package pqkem is a spike (NET-1406) for a post-quantum pre-shared-key exchange
// that could replace Rosenpass. It performs an X25519MLKEM768 hybrid key
// encapsulation and derives a 32-byte WireGuard PSK.
// encapsulation and derives a 32-byte pre-shared key (PSK).
//
// The exchange is a single round trip designed to ride the (already
// authenticated) Signal offer/answer channel:
@@ -9,7 +9,7 @@
// initiator <--Answer(1120B)-- responder
//
// Both sides then hold the same PSK, which is bound to the two peers' identities
// (their WireGuard static public keys) so the derived key cannot be transplanted
// (their peer identity keys) so the derived key cannot be transplanted
// to a different peer pair even if the transport authentication were bypassed.
//
// Combiner note: this follows the IETF hybrid layout (X25519 ‖ ML-KEM on the
@@ -35,14 +35,14 @@ const (
pskLabel = "netbird-pq-psk-v1"
)
// PSK is the 32-byte pre-shared key handed to WireGuard.
// PSK is the 32-byte derived pre-shared key handed to the consumer to key its channel.
type PSK [32]byte
// Binding identifies the peer pair the PSK is derived for. Callers set both
// WireGuard static public keys; the order does not matter (it is canonicalised).
// peer identity keys; the order does not matter (it is canonicalised).
type Binding struct {
LocalWgPub []byte
RemoteWgPub []byte
LocalID []byte
RemoteID []byte
}
// Initiator holds the ephemeral secrets between Offer and Finish.
@@ -141,7 +141,7 @@ func Respond(offer []byte, b Binding) (answer []byte, psk PSK, err error) {
// TODO(NET-1406): replace the SHA-256 concat with the RFC HKDF combiner
// (crypto/hkdf, Go 1.24+) and proper labels before this leaves spike status.
func derivePSK(ssMLKEM, ssX, offer, answer []byte, b Binding) PSK {
lo, hi := canonicalPair(b.LocalWgPub, b.RemoteWgPub)
lo, hi := canonicalPair(b.LocalID, b.RemoteID)
h := sha256.New()
h.Write([]byte(pskLabel))

View File

@@ -18,11 +18,11 @@ func TestExchange_DerivesMatchingPSK(t *testing.T) {
require.Len(t, init.Offer(), OfferSize)
answer, pskB, err := Respond(init.Offer(), Binding{LocalWgPub: wgB, RemoteWgPub: wgA})
answer, pskB, err := Respond(init.Offer(), Binding{LocalID: wgB, RemoteID: wgA})
require.NoError(t, err)
require.Len(t, answer, AnswerSize)
pskA, err := init.Finish(answer, Binding{LocalWgPub: wgA, RemoteWgPub: wgB})
pskA, err := init.Finish(answer, Binding{LocalID: wgA, RemoteID: wgB})
require.NoError(t, err)
require.Equal(t, pskB, pskA, "both sides must derive the same PSK")
@@ -34,13 +34,13 @@ func TestExchange_PSKBoundToPeerIdentities(t *testing.T) {
require.NoError(t, err)
// responder computes with the honest pair...
_, pskHonest, err := Respond(init.Offer(), Binding{LocalWgPub: wgB, RemoteWgPub: wgA})
_, pskHonest, err := Respond(init.Offer(), Binding{LocalID: wgB, RemoteID: wgA})
require.NoError(t, err)
// ...a second responder run with a different peer identity yields a different PSK,
// even though the KEM material would otherwise combine identically.
wgC := []byte("peer-C-wireguard-pubkey-32bytes!")
_, pskWrong, err := Respond(init.Offer(), Binding{LocalWgPub: wgC, RemoteWgPub: wgA})
_, pskWrong, err := Respond(init.Offer(), Binding{LocalID: wgC, RemoteID: wgA})
require.NoError(t, err)
require.NotEqual(t, pskHonest, pskWrong, "PSK must be bound to the peer pair")
@@ -70,12 +70,12 @@ func TestExchange_ReportSizesAndTiming(t *testing.T) {
tInit += time.Since(s0)
s1 := time.Now()
answer, _, err := Respond(init.Offer(), Binding{LocalWgPub: wgB, RemoteWgPub: wgA})
answer, _, err := Respond(init.Offer(), Binding{LocalID: wgB, RemoteID: wgA})
require.NoError(t, err)
tResp += time.Since(s1)
s2 := time.Now()
_, err = init.Finish(answer, Binding{LocalWgPub: wgA, RemoteWgPub: wgB})
_, err = init.Finish(answer, Binding{LocalID: wgA, RemoteID: wgB})
require.NoError(t, err)
tFinish += time.Since(s2)
}

View File

@@ -10,8 +10,9 @@ import (
)
const (
// DefaultRekeyInterval matches WireGuard's own REKEY_AFTER_TIME so the freshly
// rotated PSK is naturally adopted by WG's next handshake without forcing one.
// DefaultRekeyInterval is the default PSK rotation cadence (~2 min), chosen so a
// rotated PSK is adopted by the consumer's next transport handshake without
// forcing one.
DefaultRekeyInterval = 2 * time.Minute
// DefaultRetryInterval is how often the initiator retransmits its outstanding
// message (offer, then confirm) while an exchange is in flight.
@@ -29,11 +30,11 @@ const (
)
// Transport hands an already-encoded exchange message to the peer. The host routes
// it over the appropriate channel — Signal before the tunnel is up, the WireGuard
// tunnel for rekeys — so the Manager never needs to know which is in use. It is the
// analogue of go-rosenpass's Conn seam.
// it over the appropriate channel — a signalling channel before the tunnel is up,
// the data tunnel for rekeys — so the Manager never needs to know which is in use.
// It is the analogue of go-rosenpass's Conn seam.
type Transport interface {
Send(remoteWgKey string, msg []byte) error
Send(remoteID string, msg []byte) error
}
// exchangeState is the single source of truth for an exchange's role and phase.
@@ -66,14 +67,14 @@ type exchangeCtl struct {
// Manager is the stateful orchestrator — the analogue of go-rosenpass's Server. It
// runs the per-peer rekey timer, drives the X25519MLKEM768 exchange over a pluggable
// Transport, and surfaces the derived PSK to the host via WGCallbackHandler. The
// Transport, and surfaces the derived PSK to the host via CallbackHandler. The
// cryptography is the pure kem.go primitives; all per-exchange and per-peer state
// lives here under one lock.
type Manager struct {
localWgKey string
transport Transport
wg WGCallbackHandler
logger *slog.Logger
localID string
transport Transport
cbHandler CallbackHandler
logger *slog.Logger
rekeyInterval time.Duration
retryInterval time.Duration
@@ -91,11 +92,11 @@ type Manager struct {
wait sync.WaitGroup
}
// NewManager builds a manager for the local peer identified by its WireGuard public
// NewManager builds a manager for the local peer identified by its peer identity
// key (used for the deterministic initiator role and the identity binding). A zero
// interval falls back to DefaultRekeyInterval; a nil logger to slog.Default().
// Retry/retries/K use their defaults and can be overridden before use.
func NewManager(localWgKey string, t Transport, h WGCallbackHandler, interval time.Duration, logger *slog.Logger) *Manager {
func NewManager(localID string, t Transport, h CallbackHandler, interval time.Duration, logger *slog.Logger) *Manager {
if interval <= 0 {
interval = DefaultRekeyInterval
}
@@ -104,9 +105,9 @@ func NewManager(localWgKey string, t Transport, h WGCallbackHandler, interval ti
}
ctx, cancel := context.WithCancel(context.Background())
return &Manager{
localWgKey: localWgKey,
localID: localID,
transport: t,
wg: h,
cbHandler: h,
logger: logger,
rekeyInterval: interval,
retryInterval: DefaultRetryInterval,
@@ -122,40 +123,40 @@ func NewManager(localWgKey string, t Transport, h WGCallbackHandler, interval ti
}
// IsInitiator reports whether the local peer drives the exchange for this remote
// peer. Roles are deterministic (lexicographic WG-key compare) so exactly one side
// initiates, mirroring how Rosenpass picks its handshake initiator.
func (m *Manager) IsInitiator(remoteWgKey string) bool {
return m.localWgKey > remoteWgKey
// peer. Roles are deterministic (lexicographic identity-key compare) so exactly one
// side initiates, mirroring how Rosenpass picks its handshake initiator.
func (m *Manager) IsInitiator(remoteID string) bool {
return m.localID > remoteID
}
// AddPeer registers a remote peer and starts its rekey timer. Re-adding is a no-op.
func (m *Manager) AddPeer(remoteWgKey string) {
func (m *Manager) AddPeer(remoteID string) {
m.mu.Lock()
defer m.mu.Unlock()
if _, ok := m.peers[remoteWgKey]; ok {
if _, ok := m.peers[remoteID]; ok {
return
}
ctx, cancel := context.WithCancel(m.rootCtx)
m.peers[remoteWgKey] = cancel
m.peers[remoteID] = cancel
m.wait.Add(1)
go m.rekeyLoop(ctx, remoteWgKey)
go m.rekeyLoop(ctx, remoteID)
}
// RemovePeer stops a peer's rekey timer and any in-flight exchange, and drops state.
func (m *Manager) RemovePeer(remoteWgKey string) {
func (m *Manager) RemovePeer(remoteID string) {
m.mu.Lock()
if cancel, ok := m.peers[remoteWgKey]; ok {
if cancel, ok := m.peers[remoteID]; ok {
cancel()
delete(m.peers, remoteWgKey)
delete(m.peers, remoteID)
}
if ex, ok := m.exchanges[remoteWgKey]; ok {
if ex, ok := m.exchanges[remoteID]; ok {
if ex.cancel != nil {
ex.cancel()
}
delete(m.exchanges, remoteWgKey)
delete(m.exchanges, remoteID)
}
delete(m.established, remoteWgKey)
delete(m.failures, remoteWgKey)
delete(m.established, remoteID)
delete(m.failures, remoteID)
m.mu.Unlock()
}
@@ -171,28 +172,28 @@ func (m *Manager) Stop() {
// HandleInbound decodes an incoming message and drives the exchange, sending any
// response via the transport and surfacing derived PSKs / convergence to the host.
func (m *Manager) HandleInbound(remoteWgKey string, raw []byte) error {
func (m *Manager) HandleInbound(remoteID string, raw []byte) error {
typ, msg, err := Decode(raw)
if err != nil {
return fmt.Errorf("decode from %s: %w", remoteWgKey, err)
return fmt.Errorf("decode from %s: %w", remoteID, err)
}
switch typ {
case MsgOffer:
return m.handleOffer(remoteWgKey, msg.(*OfferMsg))
return m.handleOffer(remoteID, msg.(*OfferMsg))
case MsgAnswer:
return m.handleAnswer(remoteWgKey, msg.(*AnswerMsg))
return m.handleAnswer(remoteID, msg.(*AnswerMsg))
case MsgConfirm:
return m.handleConfirm(remoteWgKey, msg.(*ConfirmMsg))
return m.handleConfirm(remoteID, msg.(*ConfirmMsg))
default:
return fmt.Errorf("unhandled message type %d from %s", typ, remoteWgKey)
return fmt.Errorf("unhandled message type %d from %s", typ, remoteID)
}
}
// initiateRekey starts a fresh exchange when the local peer is the initiator for
// this remote peer; the responder waits for the offer instead. Exposed (unexported
// but directly callable) so tests can drive a rekey without waiting on the ticker.
func (m *Manager) initiateRekey(remoteWgKey string) error {
if !m.IsInitiator(remoteWgKey) {
func (m *Manager) initiateRekey(remoteID string) error {
if !m.IsInitiator(remoteID) {
return nil
}
init, err := NewInitiator()
@@ -210,10 +211,10 @@ func (m *Manager) initiateRekey(remoteWgKey string) error {
ctx, cancel := context.WithCancel(m.rootCtx)
m.mu.Lock()
if old := m.exchanges[remoteWgKey]; old != nil && old.cancel != nil {
if old := m.exchanges[remoteID]; old != nil && old.cancel != nil {
old.cancel()
}
m.exchanges[remoteWgKey] = &exchangeCtl{
m.exchanges[remoteID] = &exchangeCtl{
id: id,
state: stateAwaitingAnswer,
startedAt: time.Now(),
@@ -224,12 +225,12 @@ func (m *Manager) initiateRekey(remoteWgKey string) error {
m.mu.Unlock()
m.wait.Add(1)
go m.initiatorLoop(ctx, remoteWgKey, id)
go m.initiatorLoop(ctx, remoteID, id)
return m.transport.Send(remoteWgKey, raw)
return m.transport.Send(remoteID, raw)
}
func (m *Manager) rekeyLoop(ctx context.Context, remoteWgKey string) {
func (m *Manager) rekeyLoop(ctx context.Context, remoteID string) {
defer m.wait.Done()
t := time.NewTicker(m.rekeyInterval)
defer t.Stop()
@@ -238,15 +239,15 @@ func (m *Manager) rekeyLoop(ctx context.Context, remoteWgKey string) {
case <-ctx.Done():
return
case <-t.C:
if err := m.initiateRekey(remoteWgKey); err != nil {
m.logger.Error("pqkem rekey failed to start", "peer", remoteWgKey, "err", err)
if err := m.initiateRekey(remoteID); err != nil {
m.logger.Error("pqkem rekey failed to start", "peer", remoteID, "err", err)
}
}
}
}
func (m *Manager) binding(remoteWgKey string) Binding {
return Binding{LocalWgPub: []byte(m.localWgKey), RemoteWgPub: []byte(remoteWgKey)}
func (m *Manager) binding(remoteID string) Binding {
return Binding{LocalID: []byte(m.localID), RemoteID: []byte(remoteID)}
}
func newExchangeID() (ExchangeID, error) {

View File

@@ -15,7 +15,7 @@ type loopback struct {
peer *Manager
}
func (l *loopback) Send(remoteWgKey string, msg []byte) error {
func (l *loopback) Send(remoteID string, msg []byte) error {
cp := append([]byte(nil), msg...)
return l.peer.HandleInbound(l.localKey, cp)
}
@@ -28,17 +28,17 @@ type fakeWG struct {
func newFakeWG() *fakeWG { return &fakeWG{psks: map[string]PSK{}} }
func (f *fakeWG) OnNewPSKReady(remoteWgKey string, psk PSK) error {
func (f *fakeWG) OnNewPSKReady(remoteID string, psk PSK) error {
f.mu.Lock()
defer f.mu.Unlock()
f.psks[remoteWgKey] = psk
f.psks[remoteID] = psk
return nil
}
func (f *fakeWG) OnRekeyFailed(remoteWgKey string) error {
func (f *fakeWG) OnRekeyFailed(remoteID string) error {
f.mu.Lock()
defer f.mu.Unlock()
f.failed = append(f.failed, remoteWgKey)
f.failed = append(f.failed, remoteID)
return nil
}

View File

@@ -7,7 +7,7 @@ import (
// Wire framing for the PQ-KEM exchange. Messages are self-contained, versioned,
// transport-agnostic byte blobs: the same bytes ride the Signal offer/answer
// (initial, pre-tunnel) or a WireGuard-tunnel packet (rekey). They are NOT a gRPC
// (initial, pre-tunnel) or a data-tunnel packet (rekey). They are NOT a gRPC
// service — the network layer only sees opaque []byte.
//
// Layout (all messages): [type:1][version:1][exchangeID:16][payload...]
@@ -59,9 +59,9 @@ type AnswerMsg struct {
// derive it to encapsulate), so the initiator needs no confirmation. Only the
// responder is left unsure whether the initiator received the answer and committed
// the key — this message resolves that. As a bonus, being sent under the new PSK it
// triggers the WireGuard handshake with the new key: the responder converges on
// receiving it, and the initiator converges by observing that handshake succeed
// (which fails on a PSK mismatch, so success proves the responder also committed).
// exercises the consumer's channel handshake with the new key: the responder
// converges on receiving it, and the initiator converges by observing that handshake
// succeed (which fails on a PSK mismatch, so success proves the responder committed too).
type ConfirmMsg struct {
ExchangeID ExchangeID
}

View File

@@ -9,7 +9,7 @@ import (
func TestMessageRoundTrip(t *testing.T) {
init, err := NewInitiator()
require.NoError(t, err)
answer, _, err := Respond(init.Offer(), Binding{LocalWgPub: wgB, RemoteWgPub: wgA})
answer, _, err := Respond(init.Offer(), Binding{LocalID: wgB, RemoteID: wgA})
require.NoError(t, err)
id := ExchangeID{1, 2, 3, 4}