Skip to content

Commit c229789

Browse files
committed
chore: ensure downloaded slim binary version matches server
1 parent 847006f commit c229789

File tree

2 files changed

+53
-9
lines changed

2 files changed

+53
-9
lines changed

Coder-Desktop/Coder-DesktopHelper/Manager.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ actor Manager {
7474
}
7575
pushProgress(stage: .validating)
7676
do {
77-
try Validator.validate(path: dest)
77+
try Validator.validateSignature(binaryPath: dest)
78+
try await Validator.validateVersion(binaryPath: dest, serverVersion: buildInfo.version)
7879
} catch {
7980
// Cleanup unvalid binary
8081
try? FileManager.default.removeItem(at: dest)

Coder-Desktop/VPNLib/Validate.swift

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Foundation
2+
import Subprocess
23

34
public enum ValidationError: Error {
45
case fileNotFound
@@ -7,7 +8,9 @@ public enum ValidationError: Error {
78
case unableToRetrieveSignature
89
case invalidIdentifier(identifier: String?)
910
case invalidTeamIdentifier(identifier: String?)
10-
case invalidVersion(version: String?)
11+
case unableToReadVersion(any Error)
12+
case binaryVersionMismatch(binaryVersion: String, serverVersion: String)
13+
case internalError(OSStatus)
1114

1215
public var description: String {
1316
switch self {
@@ -21,10 +24,14 @@ public enum ValidationError: Error {
2124
"Unable to retrieve signing information."
2225
case let .invalidIdentifier(identifier):
2326
"Invalid identifier: \(identifier ?? "unknown")."
24-
case let .invalidVersion(version):
25-
"Invalid runtime version: \(version ?? "unknown")."
27+
case let .binaryVersionMismatch(binaryVersion, serverVersion):
28+
"Binary version does not match server. Binary: \(binaryVersion), Server: \(serverVersion)."
2629
case let .invalidTeamIdentifier(identifier):
2730
"Invalid team identifier: \(identifier ?? "unknown")."
31+
case let .unableToReadVersion(error):
32+
"Unable to execute the binary to read version: \(error.localizedDescription)"
33+
case let .internalError(status):
34+
"Internal error with OSStatus code: \(status)."
2835
}
2936
}
3037

@@ -37,22 +44,32 @@ public class Validator {
3744
public static let minimumCoderVersion = "2.24.2"
3845

3946
private static let expectedIdentifier = "com.coder.cli"
47+
// The Coder team identifier
4048
private static let expectedTeamIdentifier = "4399GN35BJ"
4149

50+
// Apple-issued certificate chain
51+
public static let anchorRequirement = "anchor apple generic"
52+
4253
private static let signInfoFlags: SecCSFlags = .init(rawValue: kSecCSSigningInformation)
4354

44-
public static func validate(path: URL) throws(ValidationError) {
45-
guard FileManager.default.fileExists(atPath: path.path) else {
55+
public static func validateSignature(binaryPath: URL) throws(ValidationError) {
56+
guard FileManager.default.fileExists(atPath: binaryPath.path) else {
4657
throw .fileNotFound
4758
}
4859

4960
var staticCode: SecStaticCode?
50-
let status = SecStaticCodeCreateWithPath(path as CFURL, SecCSFlags(), &staticCode)
61+
let status = SecStaticCodeCreateWithPath(binaryPath as CFURL, SecCSFlags(), &staticCode)
5162
guard status == errSecSuccess, let code = staticCode else {
5263
throw .unableToCreateStaticCode
5364
}
5465

55-
let validateStatus = SecStaticCodeCheckValidity(code, SecCSFlags(), nil)
66+
var requirement: SecRequirement?
67+
let reqStatus = SecRequirementCreateWithString(anchorRequirement as CFString, SecCSFlags(), &requirement)
68+
guard reqStatus == errSecSuccess, let requirement else {
69+
throw .internalError(OSStatus(reqStatus))
70+
}
71+
72+
let validateStatus = SecStaticCodeCheckValidity(code, SecCSFlags(), requirement)
5673
guard validateStatus == errSecSuccess else {
5774
throw .invalidSignature
5875
}
@@ -78,6 +95,32 @@ public class Validator {
7895
}
7996
}
8097

81-
public static let xpcPeerRequirement = "anchor apple generic" + // Apple-issued certificate chain
98+
// This function executes the binary to read its version, and so it assumes
99+
// the signature has already been validated.
100+
public static func validateVersion(binaryPath: URL, serverVersion: String) async throws(ValidationError) {
101+
guard FileManager.default.fileExists(atPath: binaryPath.path) else {
102+
throw .fileNotFound
103+
}
104+
105+
let version: String
106+
do {
107+
try chmodX(at: binaryPath)
108+
let versionOutput = try await Subprocess.data(for: [binaryPath.path, "version", "--output=json"])
109+
let parsed: VersionOutput = try JSONDecoder().decode(VersionOutput.self, from: versionOutput)
110+
version = parsed.version
111+
} catch {
112+
throw .unableToReadVersion(error)
113+
}
114+
115+
guard version == serverVersion else {
116+
throw .binaryVersionMismatch(binaryVersion: version, serverVersion: serverVersion)
117+
}
118+
}
119+
120+
struct VersionOutput: Codable {
121+
let version: String
122+
}
123+
124+
public static let xpcPeerRequirement = anchorRequirement +
82125
" and certificate leaf[subject.OU] = \"" + expectedTeamIdentifier + "\"" // Signed by the Coder team
83126
}

0 commit comments

Comments
 (0)