Skip to content

Commit ff169e3

Browse files
fix: add code signing requirements to xpc connections (#206)
Continues to address #201. I've manually tested that this change prevents binaries not signed by the Coder Apple development team from connecting to the Helper over XPC. Most of the PR diff is me moving the validator out of `Download.swift` and into `Validate.swift`
1 parent 8533b31 commit ff169e3

File tree

5 files changed

+132
-124
lines changed

5 files changed

+132
-124
lines changed

Coder-Desktop/Coder-Desktop/AppHelperXPCClient.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import VPNLib
3737
_ = self.connect()
3838
}
3939
logger.info("connecting to \(helperAppMachServiceName)")
40+
connection.setCodeSigningRequirement(Validator.xpcPeerRequirement)
4041
connection.resume()
4142
self.connection = connection
4243
return connection

Coder-Desktop/Coder-DesktopHelper/HelperXPCListeners.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class HelperNEXPCServer: NSObject, NSXPCListenerDelegate, @unchecked Sendable {
3232
conns.removeAll { $0 == newConnection }
3333
logger.debug("connection interrupted")
3434
}
35+
newConnection.setCodeSigningRequirement(Validator.xpcPeerRequirement)
3536
newConnection.resume()
3637
conns.append(newConnection)
3738
return true
@@ -145,6 +146,7 @@ class HelperAppXPCServer: NSObject, NSXPCListenerDelegate, @unchecked Sendable {
145146
conns.removeAll { $0 == newConnection }
146147
logger.debug("app connection invalidated")
147148
}
149+
newConnection.setCodeSigningRequirement(Validator.xpcPeerRequirement)
148150
newConnection.resume()
149151
conns.append(newConnection)
150152
return true

Coder-Desktop/VPN/NEHelperXPCClient.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ final class HelperXPCClient: @unchecked Sendable {
2929
connection.interruptionHandler = { [weak self] in
3030
self?.connection = nil
3131
}
32+
connection.setCodeSigningRequirement(Validator.xpcPeerRequirement)
3233
connection.resume()
3334
self.connection = connection
3435
return connection

Coder-Desktop/VPNLib/Download.swift

Lines changed: 0 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -1,130 +1,6 @@
11
import CryptoKit
22
import Foundation
33

4-
public enum ValidationError: Error {
5-
case fileNotFound
6-
case unableToCreateStaticCode
7-
case invalidSignature
8-
case unableToRetrieveInfo
9-
case invalidIdentifier(identifier: String?)
10-
case invalidTeamIdentifier(identifier: String?)
11-
case missingInfoPList
12-
case invalidVersion(version: String?)
13-
case belowMinimumCoderVersion
14-
15-
public var description: String {
16-
switch self {
17-
case .fileNotFound:
18-
"The file does not exist."
19-
case .unableToCreateStaticCode:
20-
"Unable to create a static code object."
21-
case .invalidSignature:
22-
"The file's signature is invalid."
23-
case .unableToRetrieveInfo:
24-
"Unable to retrieve signing information."
25-
case let .invalidIdentifier(identifier):
26-
"Invalid identifier: \(identifier ?? "unknown")."
27-
case let .invalidVersion(version):
28-
"Invalid runtime version: \(version ?? "unknown")."
29-
case let .invalidTeamIdentifier(identifier):
30-
"Invalid team identifier: \(identifier ?? "unknown")."
31-
case .missingInfoPList:
32-
"Info.plist is not embedded within the dylib."
33-
case .belowMinimumCoderVersion:
34-
"""
35-
The Coder deployment must be version \(Validator.minimumCoderVersion)
36-
or higher to use Coder Desktop.
37-
"""
38-
}
39-
}
40-
41-
public var localizedDescription: String { description }
42-
}
43-
44-
public class Validator {
45-
// Whilst older dylibs exist, this app assumes v2.20 or later.
46-
public static let minimumCoderVersion = "2.20.0"
47-
48-
private static let expectedName = "CoderVPN"
49-
private static let expectedIdentifier = "com.coder.Coder-Desktop.VPN.dylib"
50-
private static let expectedTeamIdentifier = "4399GN35BJ"
51-
52-
private static let infoIdentifierKey = "CFBundleIdentifier"
53-
private static let infoNameKey = "CFBundleName"
54-
private static let infoShortVersionKey = "CFBundleShortVersionString"
55-
56-
private static let signInfoFlags: SecCSFlags = .init(rawValue: kSecCSSigningInformation)
57-
58-
// `expectedVersion` must be of the form `[0-9]+.[0-9]+.[0-9]+`
59-
public static func validate(path: URL, expectedVersion: String) throws(ValidationError) {
60-
guard FileManager.default.fileExists(atPath: path.path) else {
61-
throw .fileNotFound
62-
}
63-
64-
var staticCode: SecStaticCode?
65-
let status = SecStaticCodeCreateWithPath(path as CFURL, SecCSFlags(), &staticCode)
66-
guard status == errSecSuccess, let code = staticCode else {
67-
throw .unableToCreateStaticCode
68-
}
69-
70-
let validateStatus = SecStaticCodeCheckValidity(code, SecCSFlags(), nil)
71-
guard validateStatus == errSecSuccess else {
72-
throw .invalidSignature
73-
}
74-
75-
var information: CFDictionary?
76-
let infoStatus = SecCodeCopySigningInformation(code, signInfoFlags, &information)
77-
guard infoStatus == errSecSuccess, let info = information as? [String: Any] else {
78-
throw .unableToRetrieveInfo
79-
}
80-
81-
guard let identifier = info[kSecCodeInfoIdentifier as String] as? String,
82-
identifier == expectedIdentifier
83-
else {
84-
throw .invalidIdentifier(identifier: info[kSecCodeInfoIdentifier as String] as? String)
85-
}
86-
87-
guard let teamIdentifier = info[kSecCodeInfoTeamIdentifier as String] as? String,
88-
teamIdentifier == expectedTeamIdentifier
89-
else {
90-
throw .invalidTeamIdentifier(
91-
identifier: info[kSecCodeInfoTeamIdentifier as String] as? String
92-
)
93-
}
94-
95-
guard let infoPlist = info[kSecCodeInfoPList as String] as? [String: AnyObject] else {
96-
throw .missingInfoPList
97-
}
98-
99-
try validateInfo(infoPlist: infoPlist, expectedVersion: expectedVersion)
100-
}
101-
102-
private static func validateInfo(infoPlist: [String: AnyObject], expectedVersion: String) throws(ValidationError) {
103-
guard let plistIdent = infoPlist[infoIdentifierKey] as? String, plistIdent == expectedIdentifier else {
104-
throw .invalidIdentifier(identifier: infoPlist[infoIdentifierKey] as? String)
105-
}
106-
107-
guard let plistName = infoPlist[infoNameKey] as? String, plistName == expectedName else {
108-
throw .invalidIdentifier(identifier: infoPlist[infoNameKey] as? String)
109-
}
110-
111-
// Downloaded dylib must match the version of the server
112-
guard let dylibVersion = infoPlist[infoShortVersionKey] as? String,
113-
expectedVersion == dylibVersion
114-
else {
115-
throw .invalidVersion(version: infoPlist[infoShortVersionKey] as? String)
116-
}
117-
118-
// Downloaded dylib must be at least the minimum Coder server version
119-
guard let dylibVersion = infoPlist[infoShortVersionKey] as? String,
120-
// x.compare(y) is .orderedDescending if x > y
121-
minimumCoderVersion.compare(dylibVersion, options: .numeric) != .orderedDescending
122-
else {
123-
throw .belowMinimumCoderVersion
124-
}
125-
}
126-
}
127-
1284
public func download(
1295
src: URL,
1306
dest: URL,

Coder-Desktop/VPNLib/Validate.swift

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import Foundation
2+
3+
public enum ValidationError: Error {
4+
case fileNotFound
5+
case unableToCreateStaticCode
6+
case invalidSignature
7+
case unableToRetrieveInfo
8+
case invalidIdentifier(identifier: String?)
9+
case invalidTeamIdentifier(identifier: String?)
10+
case missingInfoPList
11+
case invalidVersion(version: String?)
12+
case belowMinimumCoderVersion
13+
14+
public var description: String {
15+
switch self {
16+
case .fileNotFound:
17+
"The file does not exist."
18+
case .unableToCreateStaticCode:
19+
"Unable to create a static code object."
20+
case .invalidSignature:
21+
"The file's signature is invalid."
22+
case .unableToRetrieveInfo:
23+
"Unable to retrieve signing information."
24+
case let .invalidIdentifier(identifier):
25+
"Invalid identifier: \(identifier ?? "unknown")."
26+
case let .invalidVersion(version):
27+
"Invalid runtime version: \(version ?? "unknown")."
28+
case let .invalidTeamIdentifier(identifier):
29+
"Invalid team identifier: \(identifier ?? "unknown")."
30+
case .missingInfoPList:
31+
"Info.plist is not embedded within the dylib."
32+
case .belowMinimumCoderVersion:
33+
"""
34+
The Coder deployment must be version \(Validator.minimumCoderVersion)
35+
or higher to use Coder Desktop.
36+
"""
37+
}
38+
}
39+
40+
public var localizedDescription: String { description }
41+
}
42+
43+
public class Validator {
44+
// Whilst older dylibs exist, this app assumes v2.20 or later.
45+
public static let minimumCoderVersion = "2.20.0"
46+
47+
private static let expectedName = "CoderVPN"
48+
private static let expectedIdentifier = "com.coder.Coder-Desktop.VPN.dylib"
49+
private static let expectedTeamIdentifier = "4399GN35BJ"
50+
51+
private static let infoIdentifierKey = "CFBundleIdentifier"
52+
private static let infoNameKey = "CFBundleName"
53+
private static let infoShortVersionKey = "CFBundleShortVersionString"
54+
55+
private static let signInfoFlags: SecCSFlags = .init(rawValue: kSecCSSigningInformation)
56+
57+
// `expectedVersion` must be of the form `[0-9]+.[0-9]+.[0-9]+`
58+
public static func validate(path: URL, expectedVersion: String) throws(ValidationError) {
59+
guard FileManager.default.fileExists(atPath: path.path) else {
60+
throw .fileNotFound
61+
}
62+
63+
var staticCode: SecStaticCode?
64+
let status = SecStaticCodeCreateWithPath(path as CFURL, SecCSFlags(), &staticCode)
65+
guard status == errSecSuccess, let code = staticCode else {
66+
throw .unableToCreateStaticCode
67+
}
68+
69+
let validateStatus = SecStaticCodeCheckValidity(code, SecCSFlags(), nil)
70+
guard validateStatus == errSecSuccess else {
71+
throw .invalidSignature
72+
}
73+
74+
var information: CFDictionary?
75+
let infoStatus = SecCodeCopySigningInformation(code, signInfoFlags, &information)
76+
guard infoStatus == errSecSuccess, let info = information as? [String: Any] else {
77+
throw .unableToRetrieveInfo
78+
}
79+
80+
guard let identifier = info[kSecCodeInfoIdentifier as String] as? String,
81+
identifier == expectedIdentifier
82+
else {
83+
throw .invalidIdentifier(identifier: info[kSecCodeInfoIdentifier as String] as? String)
84+
}
85+
86+
guard let teamIdentifier = info[kSecCodeInfoTeamIdentifier as String] as? String,
87+
teamIdentifier == expectedTeamIdentifier
88+
else {
89+
throw .invalidTeamIdentifier(
90+
identifier: info[kSecCodeInfoTeamIdentifier as String] as? String
91+
)
92+
}
93+
94+
guard let infoPlist = info[kSecCodeInfoPList as String] as? [String: AnyObject] else {
95+
throw .missingInfoPList
96+
}
97+
98+
try validateInfo(infoPlist: infoPlist, expectedVersion: expectedVersion)
99+
}
100+
101+
public static let xpcPeerRequirement = "anchor apple generic" + // Apple-issued certificate chain
102+
" and certificate leaf[subject.OU] = \"" + expectedTeamIdentifier + "\"" // Signed by the Coder team
103+
104+
private static func validateInfo(infoPlist: [String: AnyObject], expectedVersion: String) throws(ValidationError) {
105+
guard let plistIdent = infoPlist[infoIdentifierKey] as? String, plistIdent == expectedIdentifier else {
106+
throw .invalidIdentifier(identifier: infoPlist[infoIdentifierKey] as? String)
107+
}
108+
109+
guard let plistName = infoPlist[infoNameKey] as? String, plistName == expectedName else {
110+
throw .invalidIdentifier(identifier: infoPlist[infoNameKey] as? String)
111+
}
112+
113+
// Downloaded dylib must match the version of the server
114+
guard let dylibVersion = infoPlist[infoShortVersionKey] as? String,
115+
expectedVersion == dylibVersion
116+
else {
117+
throw .invalidVersion(version: infoPlist[infoShortVersionKey] as? String)
118+
}
119+
120+
// Downloaded dylib must be at least the minimum Coder server version
121+
guard let dylibVersion = infoPlist[infoShortVersionKey] as? String,
122+
// x.compare(y) is .orderedDescending if x > y
123+
minimumCoderVersion.compare(dylibVersion, options: .numeric) != .orderedDescending
124+
else {
125+
throw .belowMinimumCoderVersion
126+
}
127+
}
128+
}

0 commit comments

Comments
 (0)