mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 20:45:19 +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:
@@ -79,6 +79,100 @@ final class SecureIdentityStateManagerTests: XCTestCase {
|
||||
XCTAssertEqual(matches.first?.signingPublicKey, signingPublicKey)
|
||||
}
|
||||
|
||||
func test_upsertCryptographicIdentity_refusesToReplacePinnedSigningKey() async {
|
||||
let manager = SecureIdentityStateManager(MockKeychain())
|
||||
let noisePublicKey = Data(repeating: 0x11, count: 32)
|
||||
let fingerprint = noisePublicKey.sha256Fingerprint()
|
||||
let peerID = PeerID(publicKey: noisePublicKey)
|
||||
let victimSigningKey = Data(repeating: 0x22, count: 32)
|
||||
let attackerSigningKey = Data(repeating: 0x66, count: 32)
|
||||
|
||||
manager.upsertCryptographicIdentity(
|
||||
fingerprint: fingerprint,
|
||||
noisePublicKey: noisePublicKey,
|
||||
signingPublicKey: victimSigningKey,
|
||||
claimedNickname: "victim"
|
||||
)
|
||||
let pinned = await waitUntil {
|
||||
manager.getCryptoIdentitiesByPeerIDPrefix(peerID).first?.signingPublicKey == victimSigningKey
|
||||
}
|
||||
XCTAssertTrue(pinned)
|
||||
|
||||
// Attacker upsert with a different signing key must be refused in
|
||||
// full — signing key AND claimed nickname stay the victim's.
|
||||
manager.upsertCryptographicIdentity(
|
||||
fingerprint: fingerprint,
|
||||
noisePublicKey: noisePublicKey,
|
||||
signingPublicKey: attackerSigningKey,
|
||||
claimedNickname: "attacker"
|
||||
)
|
||||
|
||||
// Synchronous reads fence the manager's pending barrier writes.
|
||||
XCTAssertEqual(
|
||||
manager.getCryptoIdentitiesByPeerIDPrefix(peerID).first?.signingPublicKey,
|
||||
victimSigningKey
|
||||
)
|
||||
XCTAssertEqual(manager.getSocialIdentity(for: fingerprint)?.claimedNickname, "victim")
|
||||
|
||||
// The legitimate peer (same signing key) can still update.
|
||||
manager.upsertCryptographicIdentity(
|
||||
fingerprint: fingerprint,
|
||||
noisePublicKey: noisePublicKey,
|
||||
signingPublicKey: victimSigningKey,
|
||||
claimedNickname: "victim-renamed"
|
||||
)
|
||||
let renamed = await waitUntil {
|
||||
manager.getSocialIdentity(for: fingerprint)?.claimedNickname == "victim-renamed"
|
||||
}
|
||||
XCTAssertTrue(renamed)
|
||||
XCTAssertEqual(
|
||||
manager.getCryptoIdentitiesByPeerIDPrefix(peerID).first?.signingPublicKey,
|
||||
victimSigningKey
|
||||
)
|
||||
}
|
||||
|
||||
func test_cryptographicIdentity_persistsAcrossReinitAndKeepsSigningKeyPin() async {
|
||||
let keychain = MockKeychain()
|
||||
let manager = SecureIdentityStateManager(keychain)
|
||||
let noisePublicKey = Data(repeating: 0x13, count: 32)
|
||||
let fingerprint = noisePublicKey.sha256Fingerprint()
|
||||
let peerID = PeerID(publicKey: noisePublicKey)
|
||||
let victimSigningKey = Data(repeating: 0x24, count: 32)
|
||||
let attackerSigningKey = Data(repeating: 0x77, count: 32)
|
||||
|
||||
manager.upsertCryptographicIdentity(
|
||||
fingerprint: fingerprint,
|
||||
noisePublicKey: noisePublicKey,
|
||||
signingPublicKey: victimSigningKey,
|
||||
claimedNickname: "victim"
|
||||
)
|
||||
let pinned = await waitUntil {
|
||||
manager.getCryptoIdentitiesByPeerIDPrefix(peerID).first?.signingPublicKey == victimSigningKey
|
||||
}
|
||||
XCTAssertTrue(pinned)
|
||||
manager.forceSave()
|
||||
|
||||
// Simulated app restart: the pin must survive and still refuse a
|
||||
// different signing key.
|
||||
let reloaded = SecureIdentityStateManager(keychain)
|
||||
XCTAssertEqual(
|
||||
reloaded.getCryptoIdentitiesByPeerIDPrefix(peerID).first?.signingPublicKey,
|
||||
victimSigningKey
|
||||
)
|
||||
|
||||
reloaded.upsertCryptographicIdentity(
|
||||
fingerprint: fingerprint,
|
||||
noisePublicKey: noisePublicKey,
|
||||
signingPublicKey: attackerSigningKey,
|
||||
claimedNickname: "attacker"
|
||||
)
|
||||
XCTAssertEqual(
|
||||
reloaded.getCryptoIdentitiesByPeerIDPrefix(peerID).first?.signingPublicKey,
|
||||
victimSigningKey
|
||||
)
|
||||
XCTAssertEqual(reloaded.getSocialIdentity(for: fingerprint)?.claimedNickname, "victim")
|
||||
}
|
||||
|
||||
func test_setBlocked_clearsFavoriteState() async {
|
||||
let manager = SecureIdentityStateManager(MockKeychain())
|
||||
let fingerprint = String(repeating: "ab", count: 32)
|
||||
|
||||
Reference in New Issue
Block a user