From 945f8809ee49a6bd4a39ee0eed0ca15fd22147e3 Mon Sep 17 00:00:00 2001 From: riccardom Date: Thu, 16 Jul 2026 17:25:19 +0200 Subject: [PATCH] Rework signal protocol to minimize number of exchanged initial RP 512KB keys --- client/internal/engine.go | 31 +++++++ client/internal/peer/conn.go | 18 ++++ client/internal/peer/handshaker.go | 39 +++++++-- .../peer/handshaker_rosenpass_test.go | 65 +++++++++++++++ client/internal/peer/signaler.go | 12 +-- client/internal/rosenpass/cache_test.go | 62 ++++++++++++++ client/internal/rosenpass/manager.go | 82 ++++++++++++++++++- shared/signal/client/client.go | 21 +++-- shared/signal/proto/signalexchange.pb.go | 77 ++++++++++++----- shared/signal/proto/signalexchange.proto | 11 +++ 10 files changed, 378 insertions(+), 40 deletions(-) create mode 100644 client/internal/peer/handshaker_rosenpass_test.go create mode 100644 client/internal/rosenpass/cache_test.go diff --git a/client/internal/engine.go b/client/internal/engine.go index 82c9d7daa..54fb7d3ae 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -1809,6 +1809,7 @@ func (e *Engine) createPeerConn(pubKey string, allowedIPs []netip.Prefix, agentV PubKey: e.getRosenpassPubKey(), Addr: e.getRosenpassAddr(), PermissiveMode: e.config.RosenpassPermissive, + KeyResolver: e.rosenpassKeyResolver(), }, ICEConfig: e.createICEConfig(), } @@ -1879,6 +1880,8 @@ func (e *Engine) receiveSignalEvents() error { log.Debugf("receiveMSG: took %s to get lock for peer %s with session id %s", gotLock, msg.Key, offerAnswer.SessionID) + e.applyRosenpassKeyExchange(msg, offerAnswer) + if msg.Body.Type == sProto.Body_OFFER { conn.OnRemoteOffer(*offerAnswer) } else { @@ -2222,6 +2225,34 @@ func (e *Engine) getRosenpassAddr() string { return "" } +// rosenpassKeyResolver returns the Rosenpass manager as the offer/answer key +// resolver, or a true nil interface when Rosenpass is disabled (returning the +// typed-nil *Manager would make the interface non-nil and panic on use). +func (e *Engine) rosenpassKeyResolver() peer.RosenpassKeyResolver { + if e.rpManager == nil { + return nil + } + return e.rpManager +} + +// applyRosenpassKeyExchange reconciles the fingerprint/cache fields of an incoming +// offer/answer against the Rosenpass manager's cache: it resolves the remote peer's +// full public key (from the message or the cache) into the OfferAnswer, and records +// whether the peer acknowledged holding our key. No-op when Rosenpass is disabled. +func (e *Engine) applyRosenpassKeyExchange(msg *sProto.Message, oa *peer.OfferAnswer) { + if e.rpManager == nil { + return + } + cfg := msg.GetBody().GetRosenpassConfig() + if cfg == nil { + return + } + + remoteWgKey := msg.GetKey() + oa.RosenpassPubKey = e.rpManager.ResolveRemotePubKey(remoteWgKey, cfg.GetRosenpassPubKey(), cfg.GetRosenpassPubKeyHash()) + e.rpManager.SetRemoteAck(remoteWgKey, cfg.GetAcknowledgedRosenpassPubKeyHash()) +} + // RunHealthProbes executes health checks for Signal, Management, Relay, and WireGuard services // and updates the status recorder with the latest states. // diff --git a/client/internal/peer/conn.go b/client/internal/peer/conn.go index 09a4e8b02..86ffbed1b 100644 --- a/client/internal/peer/conn.go +++ b/client/internal/peer/conn.go @@ -65,6 +65,20 @@ type WgConfig struct { PreSharedKey *wgtypes.Key } +// RosenpassKeyResolver lets the handshaker fill the fingerprint/cache fields of an +// offer/answer without depending on the Rosenpass manager directly. Implemented by +// rosenpass.Manager and wired in by the engine. +type RosenpassKeyResolver interface { + // LocalPubKeyHash is the SHA256 of our own Rosenpass public key. + LocalPubKeyHash() []byte + // RemotePubKeyAck is the SHA256 of the remote peer's cached key (nil if we do + // not hold it), sent back as an acknowledgement. + RemotePubKeyAck(remoteWgKey string) []byte + // RemoteHasLocalKey reports whether the peer already holds our key, so the full + // key may be omitted. + RemoteHasLocalKey(remoteWgKey string) bool +} + type RosenpassConfig struct { // RosenpassPubKey is this peer's Rosenpass public key PubKey []byte @@ -72,6 +86,10 @@ type RosenpassConfig struct { Addr string PermissiveMode bool + + // KeyResolver drives fingerprint-based key caching over signalling. Nil when + // Rosenpass is disabled, which makes the handshaker always send the full key. + KeyResolver RosenpassKeyResolver } // ConnConfig is a peer Connection configuration diff --git a/client/internal/peer/handshaker.go b/client/internal/peer/handshaker.go index 56e82e6e3..646ee4667 100644 --- a/client/internal/peer/handshaker.go +++ b/client/internal/peer/handshaker.go @@ -33,8 +33,13 @@ type OfferAnswer struct { // Version of NetBird Agent Version string // RosenpassPubKey is the Rosenpass public key of the remote peer when receiving this message - // This value is the local Rosenpass server public key when sending the message + // This value is the local Rosenpass server public key when sending the message. + // May be empty on send when the remote peer has acknowledged already holding it (see RosenpassPubKeyAck). RosenpassPubKey []byte + // RosenpassPubKeyHash is the SHA256 of the sender's own RosenpassPubKey. Always set when Rosenpass is enabled. + RosenpassPubKeyHash []byte + // RosenpassPubKeyAck is the SHA256 of the remote peer's key the sender holds cached; empty means "send it in full". + RosenpassPubKeyAck []byte // RosenpassAddr is the Rosenpass server address (IP:port) of the remote peer when receiving this message // This value is the local Rosenpass server address when sending the message RosenpassAddr string @@ -209,11 +214,11 @@ func (h *Handshaker) sendAnswer() error { func (h *Handshaker) buildOfferAnswer() OfferAnswer { answer := OfferAnswer{ - WgListenPort: h.config.LocalWgPort, - Version: version.NetbirdVersion(), - RosenpassPubKey: h.config.RosenpassConfig.PubKey, - RosenpassAddr: h.config.RosenpassConfig.Addr, + WgListenPort: h.config.LocalWgPort, + Version: version.NetbirdVersion(), + RosenpassAddr: h.config.RosenpassConfig.Addr, } + h.setRosenpassPubKey(&answer) if h.ice != nil && h.RemoteICESupported() { uFrag, pwd := h.ice.GetLocalUserCredentials() @@ -230,6 +235,30 @@ func (h *Handshaker) buildOfferAnswer() OfferAnswer { return answer } +// setRosenpassPubKey fills the Rosenpass key fields of an outgoing offer/answer. +// With a resolver wired it advertises our key hash and the ack for the remote key +// we hold, and includes the full public key only when the peer has not yet +// acknowledged holding it. Without a resolver (Rosenpass disabled, or an older +// code path) it always sends the full key, preserving the previous behaviour. +func (h *Handshaker) setRosenpassPubKey(answer *OfferAnswer) { + localKey := h.config.RosenpassConfig.PubKey + if len(localKey) == 0 { + return + } + + resolver := h.config.RosenpassConfig.KeyResolver + if resolver == nil { + answer.RosenpassPubKey = localKey + return + } + + answer.RosenpassPubKeyHash = resolver.LocalPubKeyHash() + answer.RosenpassPubKeyAck = resolver.RemotePubKeyAck(h.config.Key) + if !resolver.RemoteHasLocalKey(h.config.Key) { + answer.RosenpassPubKey = localKey + } +} + func (h *Handshaker) updateRemoteICEState(offer *OfferAnswer) { hasICE := offer.hasICECredentials() prev := h.remoteICESupported.Swap(hasICE) diff --git a/client/internal/peer/handshaker_rosenpass_test.go b/client/internal/peer/handshaker_rosenpass_test.go new file mode 100644 index 000000000..7eaa9722a --- /dev/null +++ b/client/internal/peer/handshaker_rosenpass_test.go @@ -0,0 +1,65 @@ +package peer + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +type fakeRPResolver struct { + localHash []byte + ack []byte + hasLocal bool +} + +func (f fakeRPResolver) LocalPubKeyHash() []byte { return f.localHash } +func (f fakeRPResolver) RemotePubKeyAck(string) []byte { return f.ack } +func (f fakeRPResolver) RemoteHasLocalKey(remote string) bool { return f.hasLocal } + +func TestSetRosenpassPubKey_NoResolverAlwaysSendsFullKey(t *testing.T) { + localKey := []byte{1, 2, 3} + h := &Handshaker{config: ConnConfig{RosenpassConfig: RosenpassConfig{PubKey: localKey}}} + + var a OfferAnswer + h.setRosenpassPubKey(&a) + + require.Equal(t, localKey, a.RosenpassPubKey) + require.Nil(t, a.RosenpassPubKeyHash) + require.Nil(t, a.RosenpassPubKeyAck) +} + +func TestSetRosenpassPubKey_ResolverIncludesFullKeyUntilAcked(t *testing.T) { + localKey := []byte{1, 2, 3} + res := fakeRPResolver{localHash: []byte{9}, ack: []byte{8}, hasLocal: false} + h := &Handshaker{config: ConnConfig{Key: "peerA", RosenpassConfig: RosenpassConfig{PubKey: localKey, KeyResolver: res}}} + + var a OfferAnswer + h.setRosenpassPubKey(&a) + + require.Equal(t, localKey, a.RosenpassPubKey, "full key must be sent until the peer acks it") + require.Equal(t, []byte{9}, a.RosenpassPubKeyHash) + require.Equal(t, []byte{8}, a.RosenpassPubKeyAck) +} + +func TestSetRosenpassPubKey_ResolverOmitsFullKeyOnceAcked(t *testing.T) { + localKey := []byte{1, 2, 3} + res := fakeRPResolver{localHash: []byte{9}, ack: []byte{8}, hasLocal: true} + h := &Handshaker{config: ConnConfig{Key: "peerA", RosenpassConfig: RosenpassConfig{PubKey: localKey, KeyResolver: res}}} + + var a OfferAnswer + h.setRosenpassPubKey(&a) + + require.Nil(t, a.RosenpassPubKey, "full key must be omitted once the peer holds it") + require.Equal(t, []byte{9}, a.RosenpassPubKeyHash) + require.Equal(t, []byte{8}, a.RosenpassPubKeyAck) +} + +func TestSetRosenpassPubKey_DisabledSetsNothing(t *testing.T) { + h := &Handshaker{config: ConnConfig{RosenpassConfig: RosenpassConfig{}}} + + var a OfferAnswer + h.setRosenpassPubKey(&a) + + require.Nil(t, a.RosenpassPubKey) + require.Nil(t, a.RosenpassPubKeyHash) +} diff --git a/client/internal/peer/signaler.go b/client/internal/peer/signaler.go index 5e437d96b..6beac622f 100644 --- a/client/internal/peer/signaler.go +++ b/client/internal/peer/signaler.go @@ -61,11 +61,13 @@ func (s *Signaler) signalOfferAnswer(offerAnswer OfferAnswer, remoteKey string, UFrag: offerAnswer.IceCredentials.UFrag, Pwd: offerAnswer.IceCredentials.Pwd, }, - RosenpassPubKey: offerAnswer.RosenpassPubKey, - RosenpassAddr: offerAnswer.RosenpassAddr, - RelaySrvAddress: offerAnswer.RelaySrvAddress, - RelaySrvIP: offerAnswer.RelaySrvIP, - SessionID: sessionIDBytes, + RosenpassPubKey: offerAnswer.RosenpassPubKey, + RosenpassPubKeyHash: offerAnswer.RosenpassPubKeyHash, + RosenpassPubKeyAck: offerAnswer.RosenpassPubKeyAck, + RosenpassAddr: offerAnswer.RosenpassAddr, + RelaySrvAddress: offerAnswer.RelaySrvAddress, + RelaySrvIP: offerAnswer.RelaySrvIP, + SessionID: sessionIDBytes, }) if err != nil { return err diff --git a/client/internal/rosenpass/cache_test.go b/client/internal/rosenpass/cache_test.go new file mode 100644 index 000000000..8f133cfbf --- /dev/null +++ b/client/internal/rosenpass/cache_test.go @@ -0,0 +1,62 @@ +package rosenpass + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" +) + +func newCacheTestManager(spk []byte) *Manager { + return &Manager{ + spk: spk, + remotePubKeys: make(map[string][]byte), + remoteHasLocalKey: make(map[string]bool), + } +} + +func TestResolveRemotePubKey(t *testing.T) { + m := newCacheTestManager([]byte{0x01, 0x02}) + full := bytes.Repeat([]byte{0xAB}, 64) + + // a received full key is cached and returned + require.Equal(t, full, m.ResolveRemotePubKey("peerA", full, nil)) + + // a later hash-only message resolves from the cache + require.Equal(t, full, m.ResolveRemotePubKey("peerA", nil, rawRosenpassKeyHash(full))) + + // hash mismatch is a cache miss + require.Nil(t, m.ResolveRemotePubKey("peerA", nil, bytes.Repeat([]byte{0x01}, 32))) + + // no key and no hash (remote without Rosenpass) resolves to nil + require.Nil(t, m.ResolveRemotePubKey("peerB", nil, nil)) +} + +func TestRemotePubKeyAck(t *testing.T) { + m := newCacheTestManager([]byte{0x01}) + + // unknown peer -> no ack (signals "send me the full key") + require.Nil(t, m.RemotePubKeyAck("peerA")) + + full := bytes.Repeat([]byte{0x09}, 48) + m.ResolveRemotePubKey("peerA", full, nil) + require.Equal(t, rawRosenpassKeyHash(full), m.RemotePubKeyAck("peerA")) +} + +func TestSetRemoteAckAndRemoteHasLocalKey(t *testing.T) { + m := newCacheTestManager(bytes.Repeat([]byte{0x07}, 100)) + + require.False(t, m.RemoteHasLocalKey("peerA")) + + // an ack matching our own key hash marks the peer as holding our key + m.SetRemoteAck("peerA", m.LocalPubKeyHash()) + require.True(t, m.RemoteHasLocalKey("peerA")) + + // empty ack clears it + m.SetRemoteAck("peerA", nil) + require.False(t, m.RemoteHasLocalKey("peerA")) + + // a non-matching ack does not count + m.SetRemoteAck("peerA", bytes.Repeat([]byte{0x01}, 32)) + require.False(t, m.RemoteHasLocalKey("peerA")) +} diff --git a/client/internal/rosenpass/manager.go b/client/internal/rosenpass/manager.go index 9d477a28b..40f21ff64 100644 --- a/client/internal/rosenpass/manager.go +++ b/client/internal/rosenpass/manager.go @@ -29,6 +29,11 @@ func hashRosenpassKey(key []byte) string { return hex.EncodeToString(hasher.Sum(nil)) } +func rawRosenpassKeyHash(key []byte) []byte { + sum := sha256.Sum256(key) + return sum[:] +} + // rpServer is the subset of rp.Server used by Manager. Defined as an interface // so tests can substitute a mock without spinning up a real UDP server. type rpServer interface { @@ -51,6 +56,15 @@ type Manager struct { lock sync.Mutex port int wgIface PresharedKeySetter + + // remotePubKeys caches remote peers' full Rosenpass public keys keyed by their + // WireGuard public key, so a peer that already sent us its (large) key over + // signalling need only send its hash on subsequent offers/answers. RAM only — + // never persisted (1000 peers x ~512KB would be ~512MB on disk). + remotePubKeys map[string][]byte + // remoteHasLocalKey tracks, per remote WireGuard key, whether that peer has + // acknowledged holding our current Rosenpass public key, letting us omit it. + remoteHasLocalKey map[string]bool } // NewManager creates a new Rosenpass manager. localWgKey is the local @@ -85,8 +99,10 @@ func NewManager(preSharedKey *wgtypes.Key, wgIfaceName string, localWgKey wgtype // nil receiver in addPeer -> m.rpWgHandler.AddPeer. generateConfig will // replace it with a fresh handler on each Run() to clear stale peer // state from previous engine sessions. - rpWgHandler: NewNetbirdHandler((*[32]byte)(preSharedKey), localWgKey), - lock: sync.Mutex{}, + rpWgHandler: NewNetbirdHandler((*[32]byte)(preSharedKey), localWgKey), + lock: sync.Mutex{}, + remotePubKeys: make(map[string][]byte), + remoteHasLocalKey: make(map[string]bool), }, nil } @@ -99,6 +115,68 @@ func (m *Manager) GetAddress() *net.UDPAddr { return &net.UDPAddr{Port: m.port} } +// LocalPubKeyHash returns the raw SHA256 of the local Rosenpass public key. It is +// advertised on every offer/answer so the remote peer can tell (via its cache) +// whether it already holds our full key. +func (m *Manager) LocalPubKeyHash() []byte { + return rawRosenpassKeyHash(m.spk) +} + +// RemotePubKeyAck returns the SHA256 of the remote peer's cached public key, used +// as the acknowledgement we send back. Nil means we do not hold the peer's key, +// which signals the peer to include its full key next time. +func (m *Manager) RemotePubKeyAck(remoteWgKey string) []byte { + m.lock.Lock() + defer m.lock.Unlock() + + key, ok := m.remotePubKeys[remoteWgKey] + if !ok { + return nil + } + return rawRosenpassKeyHash(key) +} + +// RemoteHasLocalKey reports whether the remote peer acknowledged holding our +// current public key, so we may omit the full key from the next offer/answer. +func (m *Manager) RemoteHasLocalKey(remoteWgKey string) bool { + m.lock.Lock() + defer m.lock.Unlock() + + return m.remoteHasLocalKey[remoteWgKey] +} + +// ResolveRemotePubKey reconciles the Rosenpass key material from a received +// offer/answer: it caches a received full key, or — when only a hash was sent — +// returns the cached key matching that hash. It returns nil when the remote peer +// does not use Rosenpass (no key, no hash) or on a cache miss (hash sent but not +// held); a miss self-heals because our resulting empty ack makes the peer resend +// its full key. +func (m *Manager) ResolveRemotePubKey(remoteWgKey string, full, hash []byte) []byte { + m.lock.Lock() + defer m.lock.Unlock() + + if len(full) > 0 { + m.remotePubKeys[remoteWgKey] = full + return full + } + if len(hash) == 0 { + return nil + } + if cached, ok := m.remotePubKeys[remoteWgKey]; ok && bytes.Equal(rawRosenpassKeyHash(cached), hash) { + return cached + } + return nil +} + +// SetRemoteAck records whether the remote peer's acknowledgement matches our +// current public key hash, i.e. whether it already holds our key. +func (m *Manager) SetRemoteAck(remoteWgKey string, ack []byte) { + m.lock.Lock() + defer m.lock.Unlock() + + m.remoteHasLocalKey[remoteWgKey] = len(ack) > 0 && bytes.Equal(ack, rawRosenpassKeyHash(m.spk)) +} + // addPeer adds a new peer to the Rosenpass server func (m *Manager) addPeer(rosenpassPubKey []byte, rosenpassAddr string, wireGuardIP string, wireGuardPubKey string) error { // Defense in depth against issue #4341 (Android crash): if Run() has not diff --git a/shared/signal/client/client.go b/shared/signal/client/client.go index fb77cb90f..088ea6c4a 100644 --- a/shared/signal/client/client.go +++ b/shared/signal/client/client.go @@ -51,10 +51,17 @@ type CredentialPayload struct { WgListenPort int Credential *Credential RosenpassPubKey []byte - RosenpassAddr string - RelaySrvAddress string - RelaySrvIP netip.Addr - SessionID []byte + // RosenpassPubKeyHash is the SHA256 of the sender's own RosenpassPubKey (empty + // when Rosenpass is disabled). RosenpassPubKey may be omitted when the peer has + // already acknowledged this hash. See RosenpassConfig in the proto. + RosenpassPubKeyHash []byte + // RosenpassPubKeyAck is the SHA256 of the remote peer's key the sender holds + // cached; empty means "send me the full key". + RosenpassPubKeyAck []byte + RosenpassAddr string + RelaySrvAddress string + RelaySrvIP netip.Addr + SessionID []byte } // UnMarshalCredential parses the credentials from the message and returns a Credential instance @@ -78,8 +85,10 @@ func MarshalCredential(myKey wgtypes.Key, remoteKey string, p CredentialPayload) WgListenPort: uint32(p.WgListenPort), NetBirdVersion: version.NetbirdVersion(), RosenpassConfig: &proto.RosenpassConfig{ - RosenpassPubKey: p.RosenpassPubKey, - RosenpassServerAddr: p.RosenpassAddr, + RosenpassPubKey: p.RosenpassPubKey, + RosenpassServerAddr: p.RosenpassAddr, + RosenpassPubKeyHash: p.RosenpassPubKeyHash, + AcknowledgedRosenpassPubKeyHash: p.RosenpassPubKeyAck, }, SessionId: p.SessionID, } diff --git a/shared/signal/proto/signalexchange.pb.go b/shared/signal/proto/signalexchange.pb.go index 8e07977f0..654968889 100644 --- a/shared/signal/proto/signalexchange.pb.go +++ b/shared/signal/proto/signalexchange.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.26.0 -// protoc v3.21.12 +// protoc v6.33.1 // source: signalexchange.proto package proto @@ -399,6 +399,17 @@ type RosenpassConfig struct { RosenpassPubKey []byte `protobuf:"bytes,1,opt,name=rosenpassPubKey,proto3" json:"rosenpassPubKey,omitempty"` // rosenpassServerAddr is an IP:port of the rosenpass service RosenpassServerAddr string `protobuf:"bytes,2,opt,name=rosenpassServerAddr,proto3" json:"rosenpassServerAddr,omitempty"` + // rosenpassPubKeyHash is the SHA256 of the sender's own rosenpassPubKey. It is + // always set when Rosenpass is enabled and lets the receiver detect (via a + // per-peer cache) whether it already holds the sender's full public key, + // avoiding re-sending the large key on every offer/answer. + RosenpassPubKeyHash []byte `protobuf:"bytes,3,opt,name=rosenpassPubKeyHash,proto3" json:"rosenpassPubKeyHash,omitempty"` + // acknowledgedRosenpassPubKeyHash is the SHA256 of the remote peer's rosenpassPubKey + // that the sender currently holds cached. When it matches the receiver's own key hash + // the receiver may omit its full rosenpassPubKey from the message. Empty means the + // sender does not have the remote key and needs it sent in full. Absent from peers + // that predate this field, which keeps them receiving the full key as before. + AcknowledgedRosenpassPubKeyHash []byte `protobuf:"bytes,4,opt,name=acknowledgedRosenpassPubKeyHash,proto3" json:"acknowledgedRosenpassPubKeyHash,omitempty"` } func (x *RosenpassConfig) Reset() { @@ -447,6 +458,20 @@ func (x *RosenpassConfig) GetRosenpassServerAddr() string { return "" } +func (x *RosenpassConfig) GetRosenpassPubKeyHash() []byte { + if x != nil { + return x.RosenpassPubKeyHash + } + return nil +} + +func (x *RosenpassConfig) GetAcknowledgedRosenpassPubKeyHash() []byte { + if x != nil { + return x.AcknowledgedRosenpassPubKeyHash + } + return nil +} + var File_signalexchange_proto protoreflect.FileDescriptor var file_signalexchange_proto_rawDesc = []byte{ @@ -506,27 +531,35 @@ var file_signalexchange_proto_rawDesc = []byte{ 0x65, 0x72, 0x49, 0x50, 0x4a, 0x04, 0x08, 0x09, 0x10, 0x0a, 0x22, 0x2e, 0x0a, 0x04, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x1b, 0x0a, 0x06, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x06, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x88, 0x01, 0x01, 0x42, - 0x09, 0x0a, 0x07, 0x5f, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x22, 0x6d, 0x0a, 0x0f, 0x52, 0x6f, - 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x28, 0x0a, - 0x0f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, - 0x73, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, - 0x70, 0x61, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x53, - 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x32, 0xb9, 0x01, 0x0a, 0x0e, 0x53, 0x69, - 0x67, 0x6e, 0x61, 0x6c, 0x45, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x4c, 0x0a, 0x04, - 0x53, 0x65, 0x6e, 0x64, 0x12, 0x20, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x65, 0x78, 0x63, - 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x20, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x65, - 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, - 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x59, 0x0a, 0x0d, 0x43, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x20, 0x2e, 0x73, 0x69, - 0x67, 0x6e, 0x61, 0x6c, 0x65, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x45, 0x6e, 0x63, - 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x20, 0x2e, - 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x65, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x45, - 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, - 0x00, 0x28, 0x01, 0x30, 0x01, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x09, 0x0a, 0x07, 0x5f, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x22, 0xe9, 0x01, 0x0a, 0x0f, 0x52, + 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x28, + 0x0a, 0x0f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x75, 0x62, 0x4b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, + 0x73, 0x73, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, 0x73, 0x65, + 0x6e, 0x70, 0x61, 0x73, 0x73, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, + 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x12, 0x30, 0x0a, 0x13, 0x72, 0x6f, + 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x48, 0x61, 0x73, + 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x13, 0x72, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, + 0x73, 0x73, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x48, 0x61, 0x73, 0x68, 0x12, 0x48, 0x0a, 0x1f, + 0x61, 0x63, 0x6b, 0x6e, 0x6f, 0x77, 0x6c, 0x65, 0x64, 0x67, 0x65, 0x64, 0x52, 0x6f, 0x73, 0x65, + 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x48, 0x61, 0x73, 0x68, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x1f, 0x61, 0x63, 0x6b, 0x6e, 0x6f, 0x77, 0x6c, 0x65, 0x64, + 0x67, 0x65, 0x64, 0x52, 0x6f, 0x73, 0x65, 0x6e, 0x70, 0x61, 0x73, 0x73, 0x50, 0x75, 0x62, 0x4b, + 0x65, 0x79, 0x48, 0x61, 0x73, 0x68, 0x32, 0xb9, 0x01, 0x0a, 0x0e, 0x53, 0x69, 0x67, 0x6e, 0x61, + 0x6c, 0x45, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x4c, 0x0a, 0x04, 0x53, 0x65, 0x6e, + 0x64, 0x12, 0x20, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x65, 0x78, 0x63, 0x68, 0x61, 0x6e, + 0x67, 0x65, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x1a, 0x20, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x6c, 0x65, 0x78, 0x63, 0x68, + 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x59, 0x0a, 0x0d, 0x43, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x20, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x61, + 0x6c, 0x65, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, + 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x20, 0x2e, 0x73, 0x69, 0x67, + 0x6e, 0x61, 0x6c, 0x65, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x45, 0x6e, 0x63, 0x72, + 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x28, 0x01, + 0x30, 0x01, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/shared/signal/proto/signalexchange.proto b/shared/signal/proto/signalexchange.proto index 8c304e37c..e8f7eb16c 100644 --- a/shared/signal/proto/signalexchange.proto +++ b/shared/signal/proto/signalexchange.proto @@ -86,4 +86,15 @@ message RosenpassConfig { bytes rosenpassPubKey = 1; // rosenpassServerAddr is an IP:port of the rosenpass service string rosenpassServerAddr = 2; + // rosenpassPubKeyHash is the SHA256 of the sender's own rosenpassPubKey. It is + // always set when Rosenpass is enabled and lets the receiver detect (via a + // per-peer cache) whether it already holds the sender's full public key, + // avoiding re-sending the large key on every offer/answer. + bytes rosenpassPubKeyHash = 3; + // acknowledgedRosenpassPubKeyHash is the SHA256 of the remote peer's rosenpassPubKey + // that the sender currently holds cached. When it matches the receiver's own key hash + // the receiver may omit its full rosenpassPubKey from the message. Empty means the + // sender does not have the remote key and needs it sent in full. Absent from peers + // that predate this field, which keeps them receiving the full key as before. + bytes acknowledgedRosenpassPubKeyHash = 4; }