diff --git a/Coder-Desktop/Coder-Desktop/AppHelperXPCClient.swift b/Coder-Desktop/Coder-Desktop/AppHelperXPCClient.swift index 7b90734..b663533 100644 --- a/Coder-Desktop/Coder-Desktop/AppHelperXPCClient.swift +++ b/Coder-Desktop/Coder-Desktop/AppHelperXPCClient.swift @@ -37,6 +37,7 @@ import VPNLib _ = self.connect() } logger.info("connecting to \(helperAppMachServiceName)") + connection.setCodeSigningRequirement(Validator.xpcPeerRequirement) connection.resume() self.connection = connection return connection diff --git a/Coder-Desktop/Coder-DesktopHelper/HelperXPCListeners.swift b/Coder-Desktop/Coder-DesktopHelper/HelperXPCListeners.swift index ae95578..9b65d8e 100644 --- a/Coder-Desktop/Coder-DesktopHelper/HelperXPCListeners.swift +++ b/Coder-Desktop/Coder-DesktopHelper/HelperXPCListeners.swift @@ -32,6 +32,7 @@ class HelperNEXPCServer: NSObject, NSXPCListenerDelegate, @unchecked Sendable { conns.removeAll { $0 == newConnection } logger.debug("connection interrupted") } + newConnection.setCodeSigningRequirement(Validator.xpcPeerRequirement) newConnection.resume() conns.append(newConnection) return true @@ -145,6 +146,7 @@ class HelperAppXPCServer: NSObject, NSXPCListenerDelegate, @unchecked Sendable { conns.removeAll { $0 == newConnection } logger.debug("app connection invalidated") } + newConnection.setCodeSigningRequirement(Validator.xpcPeerRequirement) newConnection.resume() conns.append(newConnection) return true diff --git a/Coder-Desktop/VPN/NEHelperXPCClient.swift b/Coder-Desktop/VPN/NEHelperXPCClient.swift index b61cb58..05737c4 100644 --- a/Coder-Desktop/VPN/NEHelperXPCClient.swift +++ b/Coder-Desktop/VPN/NEHelperXPCClient.swift @@ -29,6 +29,7 @@ final class HelperXPCClient: @unchecked Sendable { connection.interruptionHandler = { [weak self] in self?.connection = nil } + connection.setCodeSigningRequirement(Validator.xpcPeerRequirement) connection.resume() self.connection = connection return connection diff --git a/Coder-Desktop/VPNLib/Download.swift b/Coder-Desktop/VPNLib/Download.swift index 055ca53..16a9203 100644 --- a/Coder-Desktop/VPNLib/Download.swift +++ b/Coder-Desktop/VPNLib/Download.swift @@ -1,130 +1,6 @@ import CryptoKit import Foundation -public enum ValidationError: Error { - case fileNotFound - case unableToCreateStaticCode - case invalidSignature - case unableToRetrieveInfo - case invalidIdentifier(identifier: String?) - case invalidTeamIdentifier(identifier: String?) - case missingInfoPList - case invalidVersion(version: String?) - case belowMinimumCoderVersion - - public var description: String { - switch self { - case .fileNotFound: - "The file does not exist." - case .unableToCreateStaticCode: - "Unable to create a static code object." - case .invalidSignature: - "The file's signature is invalid." - case .unableToRetrieveInfo: - "Unable to retrieve signing information." - case let .invalidIdentifier(identifier): - "Invalid identifier: \(identifier ?? "unknown")." - case let .invalidVersion(version): - "Invalid runtime version: \(version ?? "unknown")." - case let .invalidTeamIdentifier(identifier): - "Invalid team identifier: \(identifier ?? "unknown")." - case .missingInfoPList: - "Info.plist is not embedded within the dylib." - case .belowMinimumCoderVersion: - """ - The Coder deployment must be version \(Validator.minimumCoderVersion) - or higher to use Coder Desktop. - """ - } - } - - public var localizedDescription: String { description } -} - -public class Validator { - // Whilst older dylibs exist, this app assumes v2.20 or later. - public static let minimumCoderVersion = "2.20.0" - - private static let expectedName = "CoderVPN" - private static let expectedIdentifier = "com.coder.Coder-Desktop.VPN.dylib" - private static let expectedTeamIdentifier = "4399GN35BJ" - - private static let infoIdentifierKey = "CFBundleIdentifier" - private static let infoNameKey = "CFBundleName" - private static let infoShortVersionKey = "CFBundleShortVersionString" - - private static let signInfoFlags: SecCSFlags = .init(rawValue: kSecCSSigningInformation) - - // `expectedVersion` must be of the form `[0-9]+.[0-9]+.[0-9]+` - public static func validate(path: URL, expectedVersion: String) throws(ValidationError) { - guard FileManager.default.fileExists(atPath: path.path) else { - throw .fileNotFound - } - - var staticCode: SecStaticCode? - let status = SecStaticCodeCreateWithPath(path as CFURL, SecCSFlags(), &staticCode) - guard status == errSecSuccess, let code = staticCode else { - throw .unableToCreateStaticCode - } - - let validateStatus = SecStaticCodeCheckValidity(code, SecCSFlags(), nil) - guard validateStatus == errSecSuccess else { - throw .invalidSignature - } - - var information: CFDictionary? - let infoStatus = SecCodeCopySigningInformation(code, signInfoFlags, &information) - guard infoStatus == errSecSuccess, let info = information as? [String: Any] else { - throw .unableToRetrieveInfo - } - - guard let identifier = info[kSecCodeInfoIdentifier as String] as? String, - identifier == expectedIdentifier - else { - throw .invalidIdentifier(identifier: info[kSecCodeInfoIdentifier as String] as? String) - } - - guard let teamIdentifier = info[kSecCodeInfoTeamIdentifier as String] as? String, - teamIdentifier == expectedTeamIdentifier - else { - throw .invalidTeamIdentifier( - identifier: info[kSecCodeInfoTeamIdentifier as String] as? String - ) - } - - guard let infoPlist = info[kSecCodeInfoPList as String] as? [String: AnyObject] else { - throw .missingInfoPList - } - - try validateInfo(infoPlist: infoPlist, expectedVersion: expectedVersion) - } - - private static func validateInfo(infoPlist: [String: AnyObject], expectedVersion: String) throws(ValidationError) { - guard let plistIdent = infoPlist[infoIdentifierKey] as? String, plistIdent == expectedIdentifier else { - throw .invalidIdentifier(identifier: infoPlist[infoIdentifierKey] as? String) - } - - guard let plistName = infoPlist[infoNameKey] as? String, plistName == expectedName else { - throw .invalidIdentifier(identifier: infoPlist[infoNameKey] as? String) - } - - // Downloaded dylib must match the version of the server - guard let dylibVersion = infoPlist[infoShortVersionKey] as? String, - expectedVersion == dylibVersion - else { - throw .invalidVersion(version: infoPlist[infoShortVersionKey] as? String) - } - - // Downloaded dylib must be at least the minimum Coder server version - guard let dylibVersion = infoPlist[infoShortVersionKey] as? String, - // x.compare(y) is .orderedDescending if x > y - minimumCoderVersion.compare(dylibVersion, options: .numeric) != .orderedDescending - else { - throw .belowMinimumCoderVersion - } - } -} - public func download( src: URL, dest: URL, diff --git a/Coder-Desktop/VPNLib/Validate.swift b/Coder-Desktop/VPNLib/Validate.swift new file mode 100644 index 0000000..f663bcb --- /dev/null +++ b/Coder-Desktop/VPNLib/Validate.swift @@ -0,0 +1,128 @@ +import Foundation + +public enum ValidationError: Error { + case fileNotFound + case unableToCreateStaticCode + case invalidSignature + case unableToRetrieveInfo + case invalidIdentifier(identifier: String?) + case invalidTeamIdentifier(identifier: String?) + case missingInfoPList + case invalidVersion(version: String?) + case belowMinimumCoderVersion + + public var description: String { + switch self { + case .fileNotFound: + "The file does not exist." + case .unableToCreateStaticCode: + "Unable to create a static code object." + case .invalidSignature: + "The file's signature is invalid." + case .unableToRetrieveInfo: + "Unable to retrieve signing information." + case let .invalidIdentifier(identifier): + "Invalid identifier: \(identifier ?? "unknown")." + case let .invalidVersion(version): + "Invalid runtime version: \(version ?? "unknown")." + case let .invalidTeamIdentifier(identifier): + "Invalid team identifier: \(identifier ?? "unknown")." + case .missingInfoPList: + "Info.plist is not embedded within the dylib." + case .belowMinimumCoderVersion: + """ + The Coder deployment must be version \(Validator.minimumCoderVersion) + or higher to use Coder Desktop. + """ + } + } + + public var localizedDescription: String { description } +} + +public class Validator { + // Whilst older dylibs exist, this app assumes v2.20 or later. + public static let minimumCoderVersion = "2.20.0" + + private static let expectedName = "CoderVPN" + private static let expectedIdentifier = "com.coder.Coder-Desktop.VPN.dylib" + private static let expectedTeamIdentifier = "4399GN35BJ" + + private static let infoIdentifierKey = "CFBundleIdentifier" + private static let infoNameKey = "CFBundleName" + private static let infoShortVersionKey = "CFBundleShortVersionString" + + private static let signInfoFlags: SecCSFlags = .init(rawValue: kSecCSSigningInformation) + + // `expectedVersion` must be of the form `[0-9]+.[0-9]+.[0-9]+` + public static func validate(path: URL, expectedVersion: String) throws(ValidationError) { + guard FileManager.default.fileExists(atPath: path.path) else { + throw .fileNotFound + } + + var staticCode: SecStaticCode? + let status = SecStaticCodeCreateWithPath(path as CFURL, SecCSFlags(), &staticCode) + guard status == errSecSuccess, let code = staticCode else { + throw .unableToCreateStaticCode + } + + let validateStatus = SecStaticCodeCheckValidity(code, SecCSFlags(), nil) + guard validateStatus == errSecSuccess else { + throw .invalidSignature + } + + var information: CFDictionary? + let infoStatus = SecCodeCopySigningInformation(code, signInfoFlags, &information) + guard infoStatus == errSecSuccess, let info = information as? [String: Any] else { + throw .unableToRetrieveInfo + } + + guard let identifier = info[kSecCodeInfoIdentifier as String] as? String, + identifier == expectedIdentifier + else { + throw .invalidIdentifier(identifier: info[kSecCodeInfoIdentifier as String] as? String) + } + + guard let teamIdentifier = info[kSecCodeInfoTeamIdentifier as String] as? String, + teamIdentifier == expectedTeamIdentifier + else { + throw .invalidTeamIdentifier( + identifier: info[kSecCodeInfoTeamIdentifier as String] as? String + ) + } + + guard let infoPlist = info[kSecCodeInfoPList as String] as? [String: AnyObject] else { + throw .missingInfoPList + } + + try validateInfo(infoPlist: infoPlist, expectedVersion: expectedVersion) + } + + public static let xpcPeerRequirement = "anchor apple generic" + // Apple-issued certificate chain + " and certificate leaf[subject.OU] = \"" + expectedTeamIdentifier + "\"" // Signed by the Coder team + + private static func validateInfo(infoPlist: [String: AnyObject], expectedVersion: String) throws(ValidationError) { + guard let plistIdent = infoPlist[infoIdentifierKey] as? String, plistIdent == expectedIdentifier else { + throw .invalidIdentifier(identifier: infoPlist[infoIdentifierKey] as? String) + } + + guard let plistName = infoPlist[infoNameKey] as? String, plistName == expectedName else { + throw .invalidIdentifier(identifier: infoPlist[infoNameKey] as? String) + } + + // Downloaded dylib must match the version of the server + guard let dylibVersion = infoPlist[infoShortVersionKey] as? String, + expectedVersion == dylibVersion + else { + throw .invalidVersion(version: infoPlist[infoShortVersionKey] as? String) + } + + // Downloaded dylib must be at least the minimum Coder server version + guard let dylibVersion = infoPlist[infoShortVersionKey] as? String, + // x.compare(y) is .orderedDescending if x > y + minimumCoderVersion.compare(dylibVersion, options: .numeric) != .orderedDescending + else { + throw .belowMinimumCoderVersion + } + } +}