Skip to content

Commit ea87f52

Browse files
committed
fix: add code signing requirements to xpc connections
1 parent c7dbde8 commit ea87f52

File tree

5 files changed

+132
-124
lines changed

5 files changed

+132
-124
lines changed

Coder-Desktop/Coder-Desktop/XPCInterface.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import VPNLib
3636
_ = self.connect()
3737
}
3838
logger.info("connecting to \(helperAppMachServiceName)")
39+
connection.setCodeSigningRequirement(SignatureValidator.peerRequirement)
3940
connection.resume()
4041
self.connection = connection
4142
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 HelperNEXPCListener: NSObject, NSXPCListenerDelegate, HelperNEXPCInterface
3232
conns.removeAll { $0 == newConnection }
3333
logger.debug("connection interrupted")
3434
}
35+
newConnection.setCodeSigningRequirement(SignatureValidator.peerRequirement)
3536
newConnection.resume()
3637
conns.append(newConnection)
3738
return true
@@ -149,6 +150,7 @@ class HelperAppXPCListener: NSObject, NSXPCListenerDelegate, HelperAppXPCInterfa
149150
conns.removeAll { $0 == newConnection }
150151
logger.debug("app connection invalidated")
151152
}
153+
newConnection.setCodeSigningRequirement(SignatureValidator.peerRequirement)
152154
newConnection.resume()
153155
conns.append(newConnection)
154156
return true

Coder-Desktop/VPN/HelperXPCSpeaker.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ final class HelperXPCSpeaker: NEXPCInterface, @unchecked Sendable {
2929
connection.interruptionHandler = { [weak self] in
3030
self?.connection = nil
3131
}
32+
connection.setCodeSigningRequirement(SignatureValidator.peerRequirement)
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 \(SignatureValidator.minimumCoderVersion)
36-
or higher to use Coder Desktop.
37-
"""
38-
}
39-
}
40-
41-
public var localizedDescription: String { description }
42-
}
43-
44-
public class SignatureValidator {
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 \(SignatureValidator.minimumCoderVersion)
35+
or higher to use Coder Desktop.
36+
"""
37+
}
38+
}
39+
40+
public var localizedDescription: String { description }
41+
}
42+
43+
public class SignatureValidator {
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 peerRequirement = "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)