From 8fceeab489565228155e3e5591d444d33bef3012 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Thu, 31 Jul 2025 17:10:45 +1000 Subject: [PATCH] chore: ensure downloaded slim binary version matches server --- .../Coder-DesktopHelper/Manager.swift | 3 +- Coder-Desktop/VPNLib/Validate.swift | 59 ++++++++++++++++--- 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/Coder-Desktop/Coder-DesktopHelper/Manager.swift b/Coder-Desktop/Coder-DesktopHelper/Manager.swift index aada7b2..7ef3d61 100644 --- a/Coder-Desktop/Coder-DesktopHelper/Manager.swift +++ b/Coder-Desktop/Coder-DesktopHelper/Manager.swift @@ -76,7 +76,8 @@ actor Manager { } pushProgress(stage: .validating) do { - try Validator.validate(path: dest) + try Validator.validateSignature(binaryPath: dest) + try await Validator.validateVersion(binaryPath: dest, serverVersion: buildInfo.version) } catch { // Cleanup unvalid binary try? FileManager.default.removeItem(at: dest) diff --git a/Coder-Desktop/VPNLib/Validate.swift b/Coder-Desktop/VPNLib/Validate.swift index f7e1c83..12237d8 100644 --- a/Coder-Desktop/VPNLib/Validate.swift +++ b/Coder-Desktop/VPNLib/Validate.swift @@ -1,4 +1,5 @@ import Foundation +import Subprocess public enum ValidationError: Error { case fileNotFound @@ -7,7 +8,9 @@ public enum ValidationError: Error { case unableToRetrieveSignature case invalidIdentifier(identifier: String?) case invalidTeamIdentifier(identifier: String?) - case invalidVersion(version: String?) + case unableToReadVersion(any Error) + case binaryVersionMismatch(binaryVersion: String, serverVersion: String) + case internalError(OSStatus) public var description: String { switch self { @@ -21,10 +24,14 @@ public enum ValidationError: Error { "Unable to retrieve signing information." case let .invalidIdentifier(identifier): "Invalid identifier: \(identifier ?? "unknown")." - case let .invalidVersion(version): - "Invalid runtime version: \(version ?? "unknown")." + case let .binaryVersionMismatch(binaryVersion, serverVersion): + "Binary version does not match server. Binary: \(binaryVersion), Server: \(serverVersion)." case let .invalidTeamIdentifier(identifier): "Invalid team identifier: \(identifier ?? "unknown")." + case let .unableToReadVersion(error): + "Unable to execute the binary to read version: \(error.localizedDescription)" + case let .internalError(status): + "Internal error with OSStatus code: \(status)." } } @@ -37,22 +44,32 @@ public class Validator { public static let minimumCoderVersion = "2.24.2" private static let expectedIdentifier = "com.coder.cli" + // The Coder team identifier private static let expectedTeamIdentifier = "4399GN35BJ" + // Apple-issued certificate chain + public static let anchorRequirement = "anchor apple generic" + private static let signInfoFlags: SecCSFlags = .init(rawValue: kSecCSSigningInformation) - public static func validate(path: URL) throws(ValidationError) { - guard FileManager.default.fileExists(atPath: path.path) else { + public static func validateSignature(binaryPath: URL) throws(ValidationError) { + guard FileManager.default.fileExists(atPath: binaryPath.path) else { throw .fileNotFound } var staticCode: SecStaticCode? - let status = SecStaticCodeCreateWithPath(path as CFURL, SecCSFlags(), &staticCode) + let status = SecStaticCodeCreateWithPath(binaryPath as CFURL, SecCSFlags(), &staticCode) guard status == errSecSuccess, let code = staticCode else { throw .unableToCreateStaticCode } - let validateStatus = SecStaticCodeCheckValidity(code, SecCSFlags(), nil) + var requirement: SecRequirement? + let reqStatus = SecRequirementCreateWithString(anchorRequirement as CFString, SecCSFlags(), &requirement) + guard reqStatus == errSecSuccess, let requirement else { + throw .internalError(OSStatus(reqStatus)) + } + + let validateStatus = SecStaticCodeCheckValidity(code, SecCSFlags(), requirement) guard validateStatus == errSecSuccess else { throw .invalidSignature } @@ -78,6 +95,32 @@ public class Validator { } } - public static let xpcPeerRequirement = "anchor apple generic" + // Apple-issued certificate chain + // This function executes the binary to read its version, and so it assumes + // the signature has already been validated. + public static func validateVersion(binaryPath: URL, serverVersion: String) async throws(ValidationError) { + guard FileManager.default.fileExists(atPath: binaryPath.path) else { + throw .fileNotFound + } + + let version: String + do { + try chmodX(at: binaryPath) + let versionOutput = try await Subprocess.data(for: [binaryPath.path, "version", "--output=json"]) + let parsed: VersionOutput = try JSONDecoder().decode(VersionOutput.self, from: versionOutput) + version = parsed.version + } catch { + throw .unableToReadVersion(error) + } + + guard version == serverVersion else { + throw .binaryVersionMismatch(binaryVersion: version, serverVersion: serverVersion) + } + } + + struct VersionOutput: Codable { + let version: String + } + + public static let xpcPeerRequirement = anchorRequirement + " and certificate leaf[subject.OU] = \"" + expectedTeamIdentifier + "\"" // Signed by the Coder team }