mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 02:25:20 +00:00
Prekeys: authenticate bundle packets, fix consume-republish, deflake CI
Fixes the prekey-bundle PR review + CI failure: - CI root cause: the receive queue (mesh.message) is concurrent, so a gossiped prekey bundle can be processed before the announce that binds its owner's signing key. The old handler dropped such bundles outright, so under CI parallel load the bundle was permanently lost and the cache/gossip tests flaked (verifiedBundleEntersGossipStore, prekeySealedMailTravelsViaCourierAndOpens). Bundles that arrive before their binding are now retained per-owner (bounded) and re-attempted when the verified announce lands, atomically to avoid a check-then-act race. - Authenticate the OUTER prekey-bundle packet (Codex P2 / review MEDIUM): require senderID == PeerID(bundle.noiseStaticPublicKey) and verify the packet's Ed25519 signature (covers senderID + timestamp) against the owner's bound signing key, in addition to the inner bundle signature. Stops replay under a fresh timestamp / fake senderID. - Key the gossip prekey-bundle store/dedup by the bundle's authenticated identity (noiseStaticPublicKey), not the unauthenticated packet senderID, so one valid bundle sprayed under many fabricated sender IDs can't multiply entries and exhaust the 200-owner cap. - Bump published-bundle generatedAt strictly on consume (Codex P1): consuming a prekey shrinks the published bundle, so it now republishes with a strictly newer generatedAt and re-gossips, so peers replace the cached copy and stop assigning the consumed ID before its 48h grace. - Guard the panic/clear detached Application Support tree-deletes behind TestEnvironment.isRunningTests: the SPM test process shares that tree, so the wipe could land mid-test and flake file-dependent tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -489,14 +489,23 @@ struct GossipSyncManagerTests {
|
||||
let delegate = RecordingDelegate()
|
||||
manager.delegate = delegate
|
||||
|
||||
let sender = try #require(Data(hexString: "aabbccddeeff0011"))
|
||||
let senderPeer = PeerID(hexData: sender)
|
||||
// Bundles are keyed by their authenticated identity (the noise static
|
||||
// key), not the packet senderID, so the payload must be a real bundle.
|
||||
let noiseKey = Data(repeating: 0xAB, count: 32)
|
||||
let senderPeer = PeerID(publicKey: noiseKey)
|
||||
let sender = try #require(Data(hexString: senderPeer.id))
|
||||
let bundle = PrekeyBundle(
|
||||
noiseStaticPublicKey: noiseKey,
|
||||
prekeys: [PrekeyBundle.Prekey(id: 0, publicKey: Data(repeating: 0x11, count: 32))],
|
||||
generatedAt: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||
signature: Data(count: PrekeyBundle.signatureLength)
|
||||
)
|
||||
let bundlePacket = BitchatPacket(
|
||||
type: MessageType.prekeyBundle.rawValue,
|
||||
senderID: sender,
|
||||
recipientID: nil,
|
||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||
payload: Data([0x01]),
|
||||
payload: try #require(bundle.encode()),
|
||||
signature: nil,
|
||||
ttl: 1
|
||||
)
|
||||
@@ -519,6 +528,41 @@ struct GossipSyncManagerTests {
|
||||
#expect(served.isRSR)
|
||||
}
|
||||
|
||||
@Test func prekeyBundleGossipIsKeyedByOwnerNotSenderID() {
|
||||
// One valid bundle re-broadcast under many fabricated sender IDs must
|
||||
// collapse to a single entry keyed by the bundle's own identity — the
|
||||
// spray-to-exhaust-the-cap DoS produces one entry, not N.
|
||||
let manager = GossipSyncManager(myPeerID: myPeerID, requestSyncManager: RequestSyncManager())
|
||||
let noiseKey = Data(repeating: 0xCD, count: 32)
|
||||
let ownerPeer = PeerID(publicKey: noiseKey)
|
||||
let bundle = PrekeyBundle(
|
||||
noiseStaticPublicKey: noiseKey,
|
||||
prekeys: [PrekeyBundle.Prekey(id: 0, publicKey: Data(repeating: 0x22, count: 32))],
|
||||
generatedAt: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||
signature: Data(count: PrekeyBundle.signatureLength)
|
||||
)
|
||||
guard let payload = bundle.encode() else { return }
|
||||
|
||||
for i in 0..<5 {
|
||||
let fakeSender = Data((0..<8).map { j in UInt8(truncatingIfNeeded: i * 31 + j) })
|
||||
let packet = BitchatPacket(
|
||||
type: MessageType.prekeyBundle.rawValue,
|
||||
senderID: fakeSender,
|
||||
recipientID: nil,
|
||||
timestamp: UInt64(Date().timeIntervalSince1970 * 1000) + UInt64(i),
|
||||
payload: payload,
|
||||
signature: nil,
|
||||
ttl: 1
|
||||
)
|
||||
manager.onPublicPacketSeen(packet)
|
||||
manager._performMaintenanceSynchronously(now: Date())
|
||||
// No fabricated sender ID ever creates its own entry.
|
||||
#expect(!manager._hasPrekeyBundle(for: PeerID(hexData: fakeSender)))
|
||||
}
|
||||
// Exactly the owner-keyed entry exists.
|
||||
#expect(manager._hasPrekeyBundle(for: ownerPeer))
|
||||
}
|
||||
|
||||
// MARK: - Archive persistence
|
||||
|
||||
@Test func publicMessagesRestoreFromArchiveAcrossRestart() async throws {
|
||||
|
||||
Reference in New Issue
Block a user