mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-27 06:25:22 +00:00
Pin announce signing keys to stop mesh identity spoofing (#1349)
TOFU-pins the Ed25519 signing key per noise key so a self-consistent announce that reuses a victim's peerID+noise key with the attacker's signing key/nickname is rejected instead of overwriting the registry and persisted identity. Complements #1432: that PR's signed-LEAVE verification reads the stored signing key this PR protects from announce-path poisoning. Rebased onto main as a single commit; swift build --build-tests green, 40/40 targeted tests incl. the spoofing e2e cases, and #1432's suites 50/50 unaffected.
This commit is contained in:
@@ -6,12 +6,12 @@ import Testing
|
||||
@Suite("BLE peer registry tests")
|
||||
struct BLEPeerRegistryTests {
|
||||
@Test("upserted announces track new, reconnect, and rename transitions")
|
||||
func upsertVerifiedAnnounceTracksTransitions() {
|
||||
func upsertVerifiedAnnounceTracksTransitions() throws {
|
||||
var registry = BLEPeerRegistry()
|
||||
let peerID = PeerID(str: "1122334455667788")
|
||||
let firstSeen = Date(timeIntervalSince1970: 100)
|
||||
|
||||
let first = registry.upsertVerifiedAnnounce(
|
||||
let firstResult = registry.upsertVerifiedAnnounce(
|
||||
peerID: peerID,
|
||||
nickname: "alice",
|
||||
noisePublicKey: Data([1, 2, 3]),
|
||||
@@ -19,6 +19,7 @@ struct BLEPeerRegistryTests {
|
||||
isConnected: true,
|
||||
now: firstSeen
|
||||
)
|
||||
let first = try #require(firstResult)
|
||||
|
||||
#expect(first.isNewPeer)
|
||||
#expect(!first.wasDisconnected)
|
||||
@@ -27,7 +28,7 @@ struct BLEPeerRegistryTests {
|
||||
#expect(registry.nickname(for: peerID, connectedOnly: true) == "alice")
|
||||
|
||||
registry.markDisconnected(peerID)
|
||||
let reconnect = registry.upsertVerifiedAnnounce(
|
||||
let reconnectResult = registry.upsertVerifiedAnnounce(
|
||||
peerID: peerID,
|
||||
nickname: "alice-renamed",
|
||||
noisePublicKey: Data([1, 2, 3]),
|
||||
@@ -35,6 +36,7 @@ struct BLEPeerRegistryTests {
|
||||
isConnected: true,
|
||||
now: firstSeen.addingTimeInterval(1)
|
||||
)
|
||||
let reconnect = try #require(reconnectResult)
|
||||
|
||||
#expect(!reconnect.isNewPeer)
|
||||
#expect(reconnect.wasDisconnected)
|
||||
@@ -42,6 +44,85 @@ struct BLEPeerRegistryTests {
|
||||
#expect(registry.info(for: peerID)?.nickname == "alice-renamed")
|
||||
}
|
||||
|
||||
@Test("pinned signing key cannot be silently replaced by a later announce")
|
||||
func upsertVerifiedAnnounceRefusesToReplacePinnedSigningKey() throws {
|
||||
var registry = BLEPeerRegistry()
|
||||
let peerID = PeerID(str: "1122334455667788")
|
||||
let noiseKey = Data(repeating: 0x11, count: 32)
|
||||
let victimSigningKey = Data(repeating: 0x42, count: 32)
|
||||
let attackerSigningKey = Data(repeating: 0x66, count: 32)
|
||||
let firstSeen = Date(timeIntervalSince1970: 100)
|
||||
|
||||
let pinResult = registry.upsertVerifiedAnnounce(
|
||||
peerID: peerID,
|
||||
nickname: "victim",
|
||||
noisePublicKey: noiseKey,
|
||||
signingPublicKey: victimSigningKey,
|
||||
isConnected: true,
|
||||
now: firstSeen
|
||||
)
|
||||
#expect(pinResult != nil)
|
||||
|
||||
// Attacker replays the victim's noiseKey/peerID with their own
|
||||
// signing key and nickname; the upsert must be refused wholesale.
|
||||
let attack = registry.upsertVerifiedAnnounce(
|
||||
peerID: peerID,
|
||||
nickname: "attacker",
|
||||
noisePublicKey: noiseKey,
|
||||
signingPublicKey: attackerSigningKey,
|
||||
isConnected: true,
|
||||
now: firstSeen.addingTimeInterval(1)
|
||||
)
|
||||
|
||||
#expect(attack == nil)
|
||||
let info = try #require(registry.info(for: peerID))
|
||||
#expect(info.nickname == "victim")
|
||||
#expect(info.signingPublicKey == victimSigningKey)
|
||||
|
||||
// A legitimate re-announce with the pinned key is still accepted.
|
||||
let legit = registry.upsertVerifiedAnnounce(
|
||||
peerID: peerID,
|
||||
nickname: "victim-renamed",
|
||||
noisePublicKey: noiseKey,
|
||||
signingPublicKey: victimSigningKey,
|
||||
isConnected: true,
|
||||
now: firstSeen.addingTimeInterval(2)
|
||||
)
|
||||
#expect(legit != nil)
|
||||
#expect(registry.info(for: peerID)?.nickname == "victim-renamed")
|
||||
}
|
||||
|
||||
@Test("announce without a signing key keeps the pinned key")
|
||||
func upsertVerifiedAnnounceKeepsPinnedSigningKeyWhenAnnounceOmitsIt() throws {
|
||||
var registry = BLEPeerRegistry()
|
||||
let peerID = PeerID(str: "1122334455667788")
|
||||
let noiseKey = Data(repeating: 0x11, count: 32)
|
||||
let signingKey = Data(repeating: 0x42, count: 32)
|
||||
let firstSeen = Date(timeIntervalSince1970: 100)
|
||||
|
||||
let initialResult = registry.upsertVerifiedAnnounce(
|
||||
peerID: peerID,
|
||||
nickname: "alice",
|
||||
noisePublicKey: noiseKey,
|
||||
signingPublicKey: signingKey,
|
||||
isConnected: true,
|
||||
now: firstSeen
|
||||
)
|
||||
#expect(initialResult != nil)
|
||||
|
||||
let update = registry.upsertVerifiedAnnounce(
|
||||
peerID: peerID,
|
||||
nickname: "alice",
|
||||
noisePublicKey: noiseKey,
|
||||
signingPublicKey: nil,
|
||||
isConnected: true,
|
||||
now: firstSeen.addingTimeInterval(1)
|
||||
)
|
||||
|
||||
#expect(update != nil)
|
||||
#expect(registry.info(for: peerID)?.signingPublicKey == signingKey)
|
||||
}
|
||||
|
||||
@Test("reachability keeps recent verified offline peers only when mesh is attached")
|
||||
func reachabilityRequiresMeshAttachmentForOfflinePeers() {
|
||||
let offlinePeer = PeerID(str: "1122334455667788")
|
||||
|
||||
Reference in New Issue
Block a user