Merge pull request #289 from permissionlesstech/implement-ed25519-signatures

Implement Ed25519 signatures and fix session management
This commit is contained in:
jack
2025-07-22 11:10:43 +02:00
committed by GitHub
4 changed files with 123 additions and 36 deletions
+17 -11
View File
@@ -259,6 +259,19 @@ class NoiseSessionManager {
} }
} }
func migrateSession(from oldPeerID: String, to newPeerID: String) {
_ = managerQueue.sync(flags: .barrier) {
// Check if we have a session for the old peer ID
if let session = sessions[oldPeerID] {
// Move the session to the new peer ID
sessions[newPeerID] = session
sessions.removeValue(forKey: oldPeerID)
SecurityLogger.log("Migrated Noise session from \(oldPeerID) to \(newPeerID)", category: SecurityLogger.noise, level: .info)
}
}
}
func getEstablishedSessions() -> [String: NoiseSession] { func getEstablishedSessions() -> [String: NoiseSession] {
return managerQueue.sync { return managerQueue.sync {
return sessions.filter { $0.value.isEstablished() } return sessions.filter { $0.value.isEstablished() }
@@ -306,18 +319,11 @@ class NoiseSessionManager {
var existingSession: NoiseSession? = nil var existingSession: NoiseSession? = nil
if let existing = sessions[peerID] { if let existing = sessions[peerID] {
// If we have an established session, we might need to help the other side complete theirs // If we have an established session, reject new handshake attempts
if existing.isEstablished() { if existing.isEstablished() {
// If this is a handshake initiation (32 bytes), the other side doesn't have a session // Don't destroy our working session just because the other side is confused
// We should complete the handshake to help them establish their session // They should detect the established session through successful message exchange
if message.count == 32 { throw NoiseSessionError.alreadyEstablished
// Remove existing session and create new one
sessions.removeValue(forKey: peerID)
shouldCreateNew = true
} else {
// For other handshake messages, ignore if already established
throw NoiseSessionError.alreadyEstablished
}
} else { } else {
// If we're in the middle of a handshake and receive a new initiation, // If we're in the middle of a handshake and receive a new initiation,
// reset and start fresh (the other side may have restarted) // reset and start fresh (the other side may have restarted)
+15 -5
View File
@@ -347,16 +347,18 @@ struct ChannelMetadata: Codable {
struct NoiseIdentityAnnouncement: Codable { struct NoiseIdentityAnnouncement: Codable {
let peerID: String // Current ephemeral peer ID let peerID: String // Current ephemeral peer ID
let publicKey: Data // Noise static public key let publicKey: Data // Noise static public key
let signingPublicKey: Data // Ed25519 signing public key
let nickname: String // Current nickname let nickname: String // Current nickname
let timestamp: Date // When this binding was created let timestamp: Date // When this binding was created
let previousPeerID: String? // Previous peer ID (for smooth transition) let previousPeerID: String? // Previous peer ID (for smooth transition)
let signature: Data // Signature proving ownership let signature: Data // Signature proving ownership
init(peerID: String, publicKey: Data, nickname: String, previousPeerID: String? = nil, signature: Data) { init(peerID: String, publicKey: Data, signingPublicKey: Data, nickname: String, timestamp: Date, previousPeerID: String? = nil, signature: Data) {
self.peerID = peerID self.peerID = peerID
self.publicKey = publicKey self.publicKey = publicKey
self.signingPublicKey = signingPublicKey
self.nickname = nickname self.nickname = nickname
self.timestamp = Date() self.timestamp = timestamp
self.previousPeerID = previousPeerID self.previousPeerID = previousPeerID
self.signature = signature self.signature = signature
} }
@@ -366,7 +368,7 @@ struct NoiseIdentityAnnouncement: Codable {
} }
static func decode(from data: Data) -> NoiseIdentityAnnouncement? { static func decode(from data: Data) -> NoiseIdentityAnnouncement? {
try? JSONDecoder().decode(NoiseIdentityAnnouncement.self, from: data) return try? JSONDecoder().decode(NoiseIdentityAnnouncement.self, from: data)
} }
} }
@@ -375,14 +377,22 @@ struct PeerIdentityBinding {
let currentPeerID: String // Current ephemeral ID let currentPeerID: String // Current ephemeral ID
let fingerprint: String // Permanent cryptographic identity let fingerprint: String // Permanent cryptographic identity
let publicKey: Data // Noise static public key let publicKey: Data // Noise static public key
let signingPublicKey: Data // Ed25519 signing public key
let nickname: String // Last known nickname let nickname: String // Last known nickname
let bindingTimestamp: Date // When this binding was created let bindingTimestamp: Date // When this binding was created
let signature: Data // Cryptographic proof of binding let signature: Data // Cryptographic proof of binding
// Verify the binding signature // Verify the binding signature
func verify() -> Bool { func verify() -> Bool {
// TODO: Implement signature verification let bindingData = currentPeerID.data(using: .utf8)! + publicKey +
return true String(Int64(bindingTimestamp.timeIntervalSince1970 * 1000)).data(using: .utf8)!
do {
let signingKey = try Curve25519.Signing.PublicKey(rawRepresentation: signingPublicKey)
return signingKey.isValidSignature(signature, for: bindingData)
} catch {
return false
}
} }
} }
+26 -9
View File
@@ -364,6 +364,9 @@ class BluetoothMeshService: NSObject {
// Notify about the change if it's a rotation // Notify about the change if it's a rotation
if let oldID = oldPeerID { if let oldID = oldPeerID {
// Migrate Noise session to new peer ID
self.noiseService.migratePeerSession(from: oldID, to: newPeerID, fingerprint: fingerprint)
self.notifyPeerIDChange(oldPeerID: oldID, newPeerID: newPeerID, fingerprint: fingerprint) self.notifyPeerIDChange(oldPeerID: oldID, newPeerID: newPeerID, fingerprint: fingerprint)
} }
} }
@@ -2303,11 +2306,12 @@ class BluetoothMeshService: NSObject {
return return
} }
// Verify the signature (currently always returns true for compatibility) // Verify the signature using the signing public key
let bindingData = announcement.peerID.data(using: .utf8)! + announcement.publicKey + announcement.timestamp.timeIntervalSince1970.data let timestampData = String(Int64(announcement.timestamp.timeIntervalSince1970 * 1000)).data(using: .utf8)!
if !noiseService.verifySignature(announcement.signature, for: bindingData, publicKey: announcement.publicKey) { let bindingData = announcement.peerID.data(using: .utf8)! + announcement.publicKey + timestampData
// Log but don't reject - signature verification is temporarily disabled if !noiseService.verifySignature(announcement.signature, for: bindingData, publicKey: announcement.signingPublicKey) {
SecurityLogger.log("Signature verification skipped for \(senderID)", category: SecurityLogger.noise, level: .debug) SecurityLogger.log("Signature verification failed for \(senderID)", category: SecurityLogger.noise, level: .warning)
return // Reject announcements with invalid signatures
} }
// Calculate fingerprint from public key // Calculate fingerprint from public key
@@ -2319,6 +2323,7 @@ class BluetoothMeshService: NSObject {
currentPeerID: announcement.peerID, currentPeerID: announcement.peerID,
fingerprint: fingerprint, fingerprint: fingerprint,
publicKey: announcement.publicKey, publicKey: announcement.publicKey,
signingPublicKey: announcement.signingPublicKey,
nickname: announcement.nickname, nickname: announcement.nickname,
bindingTimestamp: announcement.timestamp, bindingTimestamp: announcement.timestamp,
signature: announcement.signature signature: announcement.signature
@@ -3363,6 +3368,14 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
SecurityLogger.log("Initiating Noise handshake with \(peerID)", category: SecurityLogger.noise, level: .info) SecurityLogger.log("Initiating Noise handshake with \(peerID)", category: SecurityLogger.noise, level: .info)
// Check if we already have an established session
if noiseService.hasEstablishedSession(with: peerID) {
SecurityLogger.log("Already have established session with \(peerID)", category: SecurityLogger.noise, level: .debug)
// Clear any lingering handshake attempt time
handshakeAttemptTimes.removeValue(forKey: peerID)
return
}
// Check if we've recently tried to handshake with this peer // Check if we've recently tried to handshake with this peer
if let lastAttempt = handshakeAttemptTimes[peerID], if let lastAttempt = handshakeAttemptTimes[peerID],
Date().timeIntervalSince(lastAttempt) < handshakeTimeout { Date().timeIntervalSince(lastAttempt) < handshakeTimeout {
@@ -3800,23 +3813,27 @@ extension BluetoothMeshService: CBPeripheralManagerDelegate {
lastIdentityAnnounceTimes["*broadcast*"] = now lastIdentityAnnounceTimes["*broadcast*"] = now
} }
// Get our Noise static public key // Get our Noise static public key and signing public key
let staticKey = noiseService.getStaticPublicKeyData() let staticKey = noiseService.getStaticPublicKeyData()
let signingKey = noiseService.getSigningPublicKeyData()
// Get nickname from delegate // Get nickname from delegate
let nickname = (delegate as? ChatViewModel)?.nickname ?? "Anonymous" let nickname = (delegate as? ChatViewModel)?.nickname ?? "Anonymous"
// Create the binding data to sign // Create the binding data to sign (peerID + publicKey + timestamp)
let bindingData = myPeerID.data(using: .utf8)! + staticKey + now.timeIntervalSince1970.data let timestampData = String(Int64(now.timeIntervalSince1970 * 1000)).data(using: .utf8)!
let bindingData = myPeerID.data(using: .utf8)! + staticKey + timestampData
// Sign the binding with our private key // Sign the binding with our Ed25519 signing key
let signature = noiseService.signData(bindingData) ?? Data() let signature = noiseService.signData(bindingData) ?? Data()
// Create the identity announcement // Create the identity announcement
let announcement = NoiseIdentityAnnouncement( let announcement = NoiseIdentityAnnouncement(
peerID: myPeerID, peerID: myPeerID,
publicKey: staticKey, publicKey: staticKey,
signingPublicKey: signingKey,
nickname: nickname, nickname: nickname,
timestamp: now,
previousPeerID: previousPeerID, previousPeerID: previousPeerID,
signature: signature signature: signature
) )
+65 -11
View File
@@ -17,6 +17,10 @@ class NoiseEncryptionService {
private let staticIdentityKey: Curve25519.KeyAgreement.PrivateKey private let staticIdentityKey: Curve25519.KeyAgreement.PrivateKey
public let staticIdentityPublicKey: Curve25519.KeyAgreement.PublicKey public let staticIdentityPublicKey: Curve25519.KeyAgreement.PublicKey
// Ed25519 signing key (persistent across sessions)
private let signingKey: Curve25519.Signing.PrivateKey
public let signingPublicKey: Curve25519.Signing.PublicKey
// Session manager // Session manager
private let sessionManager: NoiseSessionManager private let sessionManager: NoiseSessionManager
@@ -63,6 +67,27 @@ class NoiseEncryptionService {
self.staticIdentityKey = loadedKey self.staticIdentityKey = loadedKey
self.staticIdentityPublicKey = staticIdentityKey.publicKey self.staticIdentityPublicKey = staticIdentityKey.publicKey
// Load or create signing key pair
let loadedSigningKey: Curve25519.Signing.PrivateKey
// Try to load from keychain
if let signingData = KeychainManager.shared.getIdentityKey(forKey: "ed25519SigningKey"),
let key = try? Curve25519.Signing.PrivateKey(rawRepresentation: signingData) {
loadedSigningKey = key
}
// If no signing key exists, create new one
else {
loadedSigningKey = Curve25519.Signing.PrivateKey()
let keyData = loadedSigningKey.rawRepresentation
// Save to keychain
_ = KeychainManager.shared.saveIdentityKey(keyData, forKey: "ed25519SigningKey")
}
// Now assign the signing keys
self.signingKey = loadedSigningKey
self.signingPublicKey = signingKey.publicKey
// Initialize session manager // Initialize session manager
self.sessionManager = NoiseSessionManager(localStaticKey: staticIdentityKey) self.sessionManager = NoiseSessionManager(localStaticKey: staticIdentityKey)
@@ -82,6 +107,11 @@ class NoiseEncryptionService {
return staticIdentityPublicKey.rawRepresentation return staticIdentityPublicKey.rawRepresentation
} }
/// Get our signing public key for sharing
func getSigningPublicKeyData() -> Data {
return signingPublicKey.rawRepresentation
}
/// Get our identity fingerprint /// Get our identity fingerprint
func getIdentityFingerprint() -> String { func getIdentityFingerprint() -> String {
let hash = SHA256.hash(data: staticIdentityPublicKey.rawRepresentation) let hash = SHA256.hash(data: staticIdentityPublicKey.rawRepresentation)
@@ -97,25 +127,31 @@ class NoiseEncryptionService {
func clearPersistentIdentity() { func clearPersistentIdentity() {
// Clear from keychain // Clear from keychain
_ = KeychainManager.shared.deleteIdentityKey(forKey: "noiseStaticKey") _ = KeychainManager.shared.deleteIdentityKey(forKey: "noiseStaticKey")
_ = KeychainManager.shared.deleteIdentityKey(forKey: "ed25519SigningKey")
// Stop rekey timer // Stop rekey timer
stopRekeyTimer() stopRekeyTimer()
} }
/// Sign data with our static identity key /// Sign data with our Ed25519 signing key
func signData(_ data: Data) -> Data? { func signData(_ data: Data) -> Data? {
// For now, use HMAC with the private key as a simple signature do {
// This is not cryptographically ideal but works for identity binding let signature = try signingKey.signature(for: data)
let key = SymmetricKey(data: staticIdentityKey.rawRepresentation) return signature
let signature = HMAC<SHA256>.authenticationCode(for: data, using: key) } catch {
return Data(signature) SecurityLogger.logError(error, context: "Failed to sign data", category: SecurityLogger.noise)
return nil
}
} }
/// Verify signature with a peer's public key /// Verify signature with a peer's Ed25519 public key
func verifySignature(_ signature: Data, for data: Data, publicKey: Data) -> Bool { func verifySignature(_ signature: Data, for data: Data, publicKey: Data) -> Bool {
// For verification, we can't use the same HMAC approach since we don't have the private key do {
// For now, we'll skip signature verification but maintain the protocol structure let signingPublicKey = try Curve25519.Signing.PublicKey(rawRepresentation: publicKey)
// In production, this should use proper Ed25519 signatures return signingPublicKey.isValidSignature(signature, for: data)
return true // Temporarily accept all signatures to fix the immediate issue } catch {
SecurityLogger.logError(error, context: "Failed to verify signature", category: SecurityLogger.noise)
return false
}
} }
// MARK: - Handshake Management // MARK: - Handshake Management
@@ -243,6 +279,24 @@ class NoiseEncryptionService {
} }
} }
/// Migrate session when peer ID changes
func migratePeerSession(from oldPeerID: String, to newPeerID: String, fingerprint: String) {
// First update the fingerprint mappings
serviceQueue.sync(flags: .barrier) {
// Remove old mapping
if let oldFingerprint = peerFingerprints[oldPeerID], oldFingerprint == fingerprint {
peerFingerprints.removeValue(forKey: oldPeerID)
}
// Add new mapping
peerFingerprints[newPeerID] = fingerprint
fingerprintToPeerID[fingerprint] = newPeerID
}
// Migrate the session in session manager
sessionManager.migrateSession(from: oldPeerID, to: newPeerID)
}
// MARK: - Private Helpers // MARK: - Private Helpers
private func handleSessionEstablished(peerID: String, remoteStaticKey: Curve25519.KeyAgreement.PublicKey) { private func handleSessionEstablished(peerID: String, remoteStaticKey: Curve25519.KeyAgreement.PublicKey) {