11import Foundation
2+ import Subprocess
23
34public 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