Bind Noise sessions to claimed peer identities

This commit is contained in:
jack
2026-07-25 20:38:58 +02:00
committed by jack
parent f6081dad31
commit 84e79f7641
9 changed files with 436 additions and 65 deletions
@@ -49,7 +49,11 @@ final class BLENoisePacketHandler {
self.environment = environment
}
func handleHandshake(_ packet: BitchatPacket, from peerID: PeerID) {
/// Returns true when the handshake message was processed successfully.
/// Callers use this to distinguish an authenticated replacement completion
/// from a rejected candidate while an older session remains established.
@discardableResult
func handleHandshake(_ packet: BitchatPacket, from peerID: PeerID) -> Bool {
let env = environment
// Use NoiseEncryptionService for handshake processing
if PeerID(hexData: packet.recipientID) == env.localPeerID() {
@@ -72,14 +76,26 @@ final class BLENoisePacketHandler {
// Session establishment will trigger onPeerAuthenticated callback
// which will send any pending messages at the right time
return true
} catch NoiseSessionError.peerIdentityMismatch {
// The candidate was already discarded by the session manager.
// Do not let a spoofed claimed ID trigger a fresh outbound
// handshake or recreate state for the attacker-selected ID.
SecureLogger.warning(
"Rejected Noise handshake whose static key does not match \(peerID.id.prefix(8))",
category: .security
)
return false
} catch {
SecureLogger.error("Failed to process handshake: \(error)")
// Try initiating a new handshake
if !env.hasNoiseSession(peerID) {
env.initiateHandshake(peerID)
}
return false
}
}
return false
}
func handleEncrypted(_ packet: BitchatPacket, from peerID: PeerID) {
+56 -4
View File
@@ -1792,7 +1792,44 @@ final class BLEService: NSObject {
}
}
private func handleLeave(_: BitchatPacket, from peerID: PeerID) {
/// Accept a leave only when the claimed sender proves possession of the
/// signing key bound by a verified announce. The persisted identity cache
/// keeps delayed/relayed leaves verifiable after the live registry entry
/// has aged out.
private func handleLeave(_ packet: BitchatPacket, from peerID: PeerID) -> Bool {
let registrySigningKey = collectionsQueue.sync {
peerRegistry.info(for: peerID)?.signingPublicKey
}
let verifiedViaRegistry = registrySigningKey.map {
noiseService.verifyPacketSignature(packet, publicKey: $0)
} ?? false
let verifiedViaPersistedIdentity = !verifiedViaRegistry
&& identityManager.getCryptoIdentitiesByPeerIDPrefix(peerID).contains { identity in
PeerID(publicKey: identity.publicKey) == peerID
&& identity.signingPublicKey.map {
noiseService.verifyPacketSignature(packet, publicKey: $0)
} == true
}
guard verifiedViaRegistry || verifiedViaPersistedIdentity else {
SecureLogger.warning(
"🚫 Dropping leave with missing/invalid signature for claimed sender \(peerID.id.prefix(8))",
category: .security
)
return false
}
// A valid departure retires transport state too; otherwise
// canDeliverSecurely could remain true for a peer we just removed.
noiseService.clearSession(for: peerID)
readLinkState { _ in
let departedLinks = noiseAuthenticatedLinkOwners.compactMap { link, owner in
owner == peerID ? link : nil
}
for link in departedLinks {
noiseAuthenticatedLinkOwners.removeValue(forKey: link)
}
}
_ = collectionsQueue.sync(flags: .barrier) {
// Remove the peer when they leave
peerRegistry.remove(peerID)
@@ -1809,6 +1846,7 @@ final class BLEService: NSObject {
self.deliverTransportEvent(.peerDisconnected(peerID))
self.deliverTransportEvent(.peerListUpdated(currentPeerIDs))
}
return true
}
private func sendAnnounce(forceSend: Bool = false) {
guard !isPanicSuspended else { return }
@@ -2553,6 +2591,12 @@ extension BLEService {
}
}
func _test_isNoiseAuthenticatedCentral(_ centralUUID: String, for peerID: PeerID) -> Bool {
bleQueue.sync {
noiseAuthenticatedLinkOwners[.central(centralUUID)] == peerID
}
}
func _test_seedConnectedPeer(_ peerID: PeerID, nickname: String) {
collectionsQueue.sync(flags: .barrier) {
peerRegistry.upsert(BLEPeerInfo(
@@ -5084,7 +5128,9 @@ extension BLEService {
handleMeshPong(packet, from: senderID)
case .leave:
handleLeave(packet, from: senderID)
// A forged leave must neither evict the claimed peer nor spread
// to downstream nodes.
guard handleLeave(packet, from: senderID) else { return }
case .none:
SecureLogger.warning("⚠️ Unknown message type: \(packet.type)", category: .session)
@@ -5725,8 +5771,14 @@ extension BLEService {
private func handleNoiseHandshake(_ packet: BitchatPacket, from peerID: PeerID) {
let wasEstablished = noiseService.hasEstablishedSession(with: peerID)
noisePacketHandler.handleHandshake(packet, from: peerID)
if !wasEstablished, noiseService.hasEstablishedSession(with: peerID) {
let processed = noisePacketHandler.handleHandshake(packet, from: peerID)
let isEstablished = noiseService.hasEstablishedSession(with: peerID)
// XX message 1 is exactly the unauthenticated 32-byte ephemeral key.
// While replacing an existing session, do not authenticate its ingress
// link until a later message completes and validates the candidate.
let completedAuthenticatedHandshake = !wasEstablished
|| packet.payload.count != NoiseSecurityConstants.xxInitialMessageSize
if processed, isEstablished, completedAuthenticatedHandshake {
markNoiseAuthenticatedIngressLink(for: packet, peerID: peerID)
}
}