import Foundation import CryptoKit import Darwin public final class NodeIdentity: @unchecked Sendable { private struct StoredIdentity: Codable { let version: Int let signing: Data let agreement: Data } public static let currentVersion = 1 public let signingKey: Curve25519.Signing.PrivateKey public let agreementKey: Curve25519.KeyAgreement.PrivateKey public let record: NodeRecord public init(signingKey: Curve25519.Signing.PrivateKey = .init(), agreementKey: Curve25519.KeyAgreement.PrivateKey = .init()) throws { self.signingKey = signingKey; self.agreementKey = agreementKey let signing = signingKey.publicKey.rawRepresentation self.record = NodeRecord(address: try MeshAddress.derive(from: signing), signingPublicKey: signing, agreementPublicKey: agreementKey.publicKey.rawRepresentation) } private static func writeAtomicallyWithoutReplacing(_ data: Data, to url: URL) throws { let temporaryURL = url.deletingLastPathComponent() .appendingPathComponent(".\(url.lastPathComponent).\(UUID().uuidString).tmp") let descriptor = Darwin.open(temporaryURL.path, O_WRONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR) guard descriptor >= 0 else { throw POSIXError(.init(rawValue: errno) ?? .EIO) } defer { Darwin.close(descriptor) Darwin.unlink(temporaryURL.path) } try data.withUnsafeBytes { bytes in guard let base = bytes.baseAddress else { return } var offset = 0 while offset < data.count { let written = Darwin.write(descriptor, base.advanced(by: offset), data.count - offset) if written < 0 { if errno == EINTR { continue } throw POSIXError(.init(rawValue: errno) ?? .EIO) } offset += written } } guard Darwin.fsync(descriptor) == 0 else { throw POSIXError(.init(rawValue: errno) ?? .EIO) } // A hard link publishes the fully-written same-filesystem temporary file in // one operation and fails with EEXIST instead of replacing existing state. guard Darwin.link(temporaryURL.path, url.path) == 0 else { throw POSIXError(.init(rawValue: errno) ?? .EIO) } } private static func itemExists(at url: URL) throws -> Bool { var status = stat() if Darwin.lstat(url.path, &status) == 0 { return true } let code = errno if code == ENOENT || code == ENOTDIR { return false } throw POSIXError(.init(rawValue: code) ?? .EIO) } public static func loadOrCreate(at url: URL) throws -> NodeIdentity { let fileManager = FileManager.default func load() throws -> NodeIdentity { let data: Data do { data = try Data(contentsOf: url) } catch { throw UMNError.message("Cannot read the UltraMesh identity at \(url.path). Restore access to this file; it was not replaced.") } let value: StoredIdentity do { value = try PropertyListDecoder().decode(StoredIdentity.self, from: data) } catch { throw UMNError.message("The UltraMesh identity at \(url.path) is corrupt. Restore it from backup or explicitly reset UltraMesh state; it was not replaced.") } guard value.version == currentVersion else { throw UMNError.message("The UltraMesh identity at \(url.path) uses unsupported version \(value.version). Upgrade UltraMesh or restore a compatible identity; it was not replaced.") } do { return try NodeIdentity(signingKey: .init(rawRepresentation: value.signing), agreementKey: .init(rawRepresentation: value.agreement)) } catch { throw UMNError.message("The UltraMesh identity at \(url.path) contains invalid cryptographic keys. Restore it from backup or explicitly reset UltraMesh state; it was not replaced.") } } let exists: Bool do { exists = try itemExists(at: url) } catch { throw UMNError.message("Cannot inspect the UltraMesh identity path at \(url.path): \(error.localizedDescription). No new identity was created.") } if exists { return try load() } let identity = try NodeIdentity() do { try fileManager.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true) } catch { throw UMNError.message("Cannot create the UltraMesh state directory at \(url.deletingLastPathComponent().path): \(error.localizedDescription)") } let encoder = PropertyListEncoder(); encoder.outputFormat = .binary let data = try encoder.encode(StoredIdentity(version: currentVersion, signing: identity.signingKey.rawRepresentation, agreement: identity.agreementKey.rawRepresentation)) do { try writeAtomicallyWithoutReplacing(data, to: url) } catch { // Another daemon may have won the first-run race. Its complete identity is // authoritative; never overwrite it with the identity generated above. if (try? itemExists(at: url)) == true { return try load() } throw UMNError.message("Cannot create the UltraMesh identity at \(url.path): \(error.localizedDescription)") } do { try fileManager.setAttributes([.posixPermissions: 0o600], ofItemAtPath: url.path) } catch { throw UMNError.message("The UltraMesh identity was created at \(url.path), but its permissions could not be restricted to 0600: \(error.localizedDescription)") } return identity } /// Loads the persistent identity in a state directory and creates or verifies /// its derived, human-readable address sidecar. public static func loadOrCreate(in directory: URL) throws -> NodeIdentity { let fileManager = FileManager.default let identityURL = directory.appendingPathComponent("identity.plist") let addressURL = directory.appendingPathComponent("address") let identityExists: Bool let addressExists: Bool do { identityExists = try itemExists(at: identityURL) addressExists = try itemExists(at: addressURL) } catch { throw UMNError.message("Cannot inspect UltraMesh state in \(directory.path): \(error.localizedDescription). No state was changed.") } guard identityExists || !addressExists else { throw UMNError.message("Found \(addressURL.path) without \(identityURL.path). Restore the matching identity or explicitly reset UltraMesh state; no new identity was created.") } let identity = try loadOrCreate(at: identityURL) func verifyAddress() throws { let data: Data do { data = try Data(contentsOf: addressURL) } catch { throw UMNError.message("Cannot read the stored UltraMesh address at \(addressURL.path). Restore access to this file; it was not replaced.") } guard let stored = String(data: data, encoding: .utf8) else { throw UMNError.message("The stored UltraMesh address at \(addressURL.path) is not UTF-8 text. Restore the matching record or explicitly reset UltraMesh state; it was not replaced.") } let text = stored.trimmingCharacters(in: .whitespacesAndNewlines) guard let address = try? MeshAddress(text), address == identity.record.address else { throw UMNError.message("The stored UltraMesh address at \(addressURL.path) does not match the persistent identity. Restore the matching identity/address pair or explicitly reset UltraMesh state; neither file was replaced.") } } if addressExists { try verifyAddress() } else { let data = Data("\(identity.record.address)\n".utf8) do { try writeAtomicallyWithoutReplacing(data, to: addressURL) } catch { // As with identity creation, tolerate only a concurrent complete write. if (try? itemExists(at: addressURL)) == true { try verifyAddress() } else { throw UMNError.message("Cannot create the stored UltraMesh address at \(addressURL.path): \(error.localizedDescription)") } } } do { try fileManager.setAttributes([.posixPermissions: 0o600], ofItemAtPath: addressURL.path) } catch { throw UMNError.message("Cannot restrict the stored UltraMesh address at \(addressURL.path) to mode 0600: \(error.localizedDescription)") } return identity } public func sign(_ data: Data) throws -> Data { try signingKey.signature(for: data) } public func makeLinkState(sequence: UInt64, neighbors: [MeshAddress]) throws -> LinkState { let unsigned = LinkState(origin: record, sequence: sequence, neighbors: neighbors, signature: Data()) return LinkState(origin: record, sequence: sequence, neighbors: neighbors, signature: try sign(unsigned.signingBytes())) } public func seal(_ inner: InnerFrame, to destination: NodeRecord) throws -> SealedPayload { guard destination.validate() else { throw UMNError.invalidIdentity } let encoder = PropertyListEncoder(); encoder.outputFormat = .binary let innerData = try encoder.encode(inner) struct Signed: Codable { let inner: Data; let signature: Data } let signed = try encoder.encode(Signed(inner: innerData, signature: sign(innerData))) let ephemeral = Curve25519.KeyAgreement.PrivateKey() let remote = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: destination.agreementPublicKey) let secret = try ephemeral.sharedSecretFromKeyAgreement(with: remote) let key = secret.hkdfDerivedSymmetricKey(using: SHA256.self, salt: Data("umn-e2e-v1".utf8), sharedInfo: destination.address.bytes, outputByteCount: 32) let box = try ChaChaPoly.seal(signed, using: key) return SealedPayload(ephemeralPublicKey: ephemeral.publicKey.rawRepresentation, combinedCiphertext: box.combined) } public func open(_ payload: SealedPayload, expectedSource: MeshAddress) throws -> InnerFrame { struct Signed: Codable { let inner: Data; let signature: Data } let ephemeral = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: payload.ephemeralPublicKey) let secret = try agreementKey.sharedSecretFromKeyAgreement(with: ephemeral) let key = secret.hkdfDerivedSymmetricKey(using: SHA256.self, salt: Data("umn-e2e-v1".utf8), sharedInfo: record.address.bytes, outputByteCount: 32) let box = try ChaChaPoly.SealedBox(combined: payload.combinedCiphertext) let data = try ChaChaPoly.open(box, using: key) let decoder = PropertyListDecoder() let signed = try decoder.decode(Signed.self, from: data) let inner = try decoder.decode(InnerFrame.self, from: signed.inner) guard inner.sourceRecord.address == expectedSource, inner.sourceRecord.validate() else { throw UMNError.invalidIdentity } let keyVerify = try Curve25519.Signing.PublicKey(rawRepresentation: inner.sourceRecord.signingPublicKey) guard keyVerify.isValidSignature(signed.signature, for: signed.inner) else { throw UMNError.invalidIdentity } return inner } }