Files
bitchat/bitchatTests/Mocks/MockIdentityManager.swift
jackandClaude Fable 5 26b247c329 Transitive verification: vouch for verified peers over Noise
When a Noise session establishes with a peer I verified and that peer
advertises the .vouch capability, send signed attestations (up to 16,
most recently verified first, at most once per peer per 24h) for the
OTHER fingerprints I verified. Receivers accept vouches only from
senders they verified themselves, verify the Ed25519 signature against
the sender's announce-bound signing key, and surface the result as a
new derived trust tier: vouched (unfilled seal) between casual and
trusted.

Protocol:
- NoisePayloadType.vouch = 0x12 carries a batch of TLV attestations:
  voucheeFingerprint (32B), voucheeSigningKey (32B), timestamp
  (uint64 ms BE), Ed25519 signature over
  "bitchat-vouch-v1" | fingerprint | signingKey | timestamp.
  The voucher is implicit in the authenticated session.
- PeerCapabilities.localSupported now advertises .vouch.

Storage (SecureIdentityStateManager / IdentityCache):
- vouches keyed by vouchee, capped at 8 vouchers each; validity is
  recomputed on read (voucher still verified-by-me, < 30 days old), so
  unverifying a voucher retires their vouches without cascade deletes.
- New IdentityCache fields are Optional so pre-existing encrypted
  caches decode cleanly; TrustLevel.vouched is inserted mid-ladder but
  raw values are strings, so persisted values are unaffected (and
  vouched itself is never persisted).
- Panic wipe clears vouch state with the rest of the identity cache.

UI: unfilled checkmark.seal badge in the mesh peer list (filled seal
stays exclusive to verified) and a "vouched for by N people you
verified" section with voucher names in FingerprintView; VoiceOver
labels and xcstrings entries included.

Tests: attestation encode/decode + signature (forged/tampered/expired),
accept-policy gates, batch cap, trust-level derivation incl. voucher
invalidation, persistence compat, and coordinator exchange/accept
policies. Full macOS suite: 1088 tests passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 20:13:48 +02:00

162 lines
4.9 KiB
Swift

//
// MockIdentityManager.swift
// bitchat
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
import Foundation
import BitFoundation
@testable import bitchat
final class MockIdentityManager: SecureIdentityStateManagerProtocol {
private let keychain: KeychainManagerProtocol
private var blockedFingerprints: Set<String> = []
private var blockedNostrPubkeys: Set<String> = []
private var socialIdentities: [String: SocialIdentity] = [:]
init(_ keychain: KeychainManagerProtocol) {
self.keychain = keychain
}
func loadIdentityCache() {}
func saveIdentityCache() {}
func forceSave() {}
func getSocialIdentity(for fingerprint: String) -> SocialIdentity? {
socialIdentities[fingerprint]
}
func upsertCryptographicIdentity(fingerprint: String, noisePublicKey: Data, signingPublicKey: Data?, claimedNickname: String?) {}
func getCryptoIdentitiesByPeerIDPrefix(_ peerID: PeerID) -> [CryptographicIdentity] {
[]
}
func updateSocialIdentity(_ identity: SocialIdentity) {
socialIdentities[identity.fingerprint] = identity
if identity.isBlocked {
blockedFingerprints.insert(identity.fingerprint)
} else {
blockedFingerprints.remove(identity.fingerprint)
}
}
func getFavorites() -> Set<String> {
Set()
}
func setFavorite(_ fingerprint: String, isFavorite: Bool) {}
func isFavorite(fingerprint: String) -> Bool {
false
}
func isBlocked(fingerprint: String) -> Bool {
blockedFingerprints.contains(fingerprint) || socialIdentities[fingerprint]?.isBlocked == true
}
func setBlocked(_ fingerprint: String, isBlocked: Bool) {
if var identity = socialIdentities[fingerprint] {
identity.isBlocked = isBlocked
socialIdentities[fingerprint] = identity
} else {
let identity = SocialIdentity(
fingerprint: fingerprint,
localPetname: nil,
claimedNickname: "",
trustLevel: .unknown,
isFavorite: false,
isBlocked: isBlocked,
notes: nil
)
socialIdentities[fingerprint] = identity
}
if isBlocked {
blockedFingerprints.insert(fingerprint)
} else {
blockedFingerprints.remove(fingerprint)
}
}
func isNostrBlocked(pubkeyHexLowercased: String) -> Bool {
blockedNostrPubkeys.contains(pubkeyHexLowercased)
}
func setNostrBlocked(_ pubkeyHexLowercased: String, isBlocked: Bool) {
if isBlocked {
blockedNostrPubkeys.insert(pubkeyHexLowercased)
} else {
blockedNostrPubkeys.remove(pubkeyHexLowercased)
}
}
func getBlockedNostrPubkeys() -> Set<String> {
blockedNostrPubkeys
}
func registerEphemeralSession(peerID: PeerID, handshakeState: HandshakeState) {}
func updateHandshakeState(peerID: PeerID, state: HandshakeState) {}
func clearAllIdentityData() {}
func removeEphemeralSession(peerID: PeerID) {}
func setVerified(fingerprint: String, verified: Bool) {}
func isVerified(fingerprint: String) -> Bool {
true
}
func getVerifiedFingerprints() -> Set<String> {
Set()
}
// MARK: Vouching (transitive verification)
private var vouchesByVouchee: [String: [VouchRecord]] = [:]
private var vouchBatchSentAt: [String: Date] = [:]
@discardableResult
func recordVouch(voucheeFingerprint: String, voucherFingerprint: String, timestamp: Date) -> Bool {
guard voucheeFingerprint != voucherFingerprint else { return false }
var records = vouchesByVouchee[voucheeFingerprint] ?? []
records.removeAll { $0.voucherFingerprint == voucherFingerprint }
records.append(VouchRecord(voucherFingerprint: voucherFingerprint, timestamp: timestamp))
vouchesByVouchee[voucheeFingerprint] = records
return true
}
func validVouchers(for fingerprint: String) -> [VouchRecord] {
vouchesByVouchee[fingerprint] ?? []
}
func isVouched(fingerprint: String) -> Bool {
!(vouchesByVouchee[fingerprint] ?? []).isEmpty
}
func effectiveTrustLevel(for fingerprint: String) -> TrustLevel {
socialIdentities[fingerprint]?.trustLevel ?? .unknown
}
func lastVouchBatchSent(to fingerprint: String) -> Date? {
vouchBatchSentAt[fingerprint]
}
func markVouchBatchSent(to fingerprint: String, at date: Date) {
vouchBatchSentAt[fingerprint] = date
}
func signingPublicKey(forFingerprint fingerprint: String) -> Data? {
nil
}
func mostRecentlyVerifiedFingerprints(limit: Int, excluding fingerprint: String) -> [String] {
[]
}
}