Browse Source

remote: add SPICE ticket password auth

osy 1 year ago
parent
commit
745cd38827

+ 10 - 1
Configuration/UTMQemuConfiguration+Arguments.swift

@@ -146,7 +146,11 @@ import Virtualization // for getting network interfaces
             "unix=on"
             "addr=\(spiceSocketURL.lastPathComponent)"
         }
-        "disable-ticketing=on"
+        if let _ = qemu.spiceServerPassword {
+            "password-secret=secspice0"
+        } else {
+            "disable-ticketing=on"
+        }
         if !isRemoteSpice {
             "image-compression=off"
             "playback-compression=off"
@@ -176,6 +180,11 @@ import Virtualization // for getting network interfaces
             f("-vga")
             f("none")
         }
+        if let password = qemu.spiceServerPassword {
+            // assume anyone who can read this is in our trust domain
+            f("-object")
+            f("secret,id=secspice0,data=\(password)")
+        }
     }
 
     private func filterDisplayIfRemote(_ display: any QEMUDisplayDevice) -> any QEMUDisplayDevice {

+ 3 - 0
Configuration/UTMQemuConfigurationQEMU.swift

@@ -77,6 +77,9 @@ struct UTMQemuConfigurationQEMU: Codable {
 
     /// Set to TLS public key for SPICE server in SubjectPublicKey. Not saved.
     var spiceServerPublicKey: Data?
+    
+    /// Set to a password shared with the client. Not saved.
+    var spiceServerPassword: String?
 
     enum CodingKeys: String, CodingKey {
         case hasDebugLog = "DebugLog"

+ 2 - 2
Platform/macOS/UTMDataExtension.swift

@@ -82,7 +82,7 @@ extension UTMData {
     ///   - options: Start options
     ///   - server: Remote server
     /// - Returns: Port number to SPICE server
-    func startRemote(vm: VMData, options: UTMVirtualMachineStartOptions, forClient client: UTMRemoteServer.Remote) async throws -> (port: UInt16, publicKey: Data) {
+    func startRemote(vm: VMData, options: UTMVirtualMachineStartOptions, forClient client: UTMRemoteServer.Remote) async throws -> (port: UInt16, publicKey: Data, password: String) {
         guard let wrapped = vm.wrapped as? UTMQemuVirtualMachine, type(of: wrapped).capabilities.supportsRemoteSession else {
             throw UTMDataError.unsupportedBackend
         }
@@ -94,7 +94,7 @@ extension UTMData {
         }
         try await wrapped.start(options: options.union(.remoteSession))
         vmWindows[vm] = session
-        return (wrapped.config.qemu.spiceServerPort!, wrapped.config.qemu.spiceServerPublicKey!)
+        return (wrapped.config.qemu.spiceServerPort!, wrapped.config.qemu.spiceServerPublicKey!, wrapped.config.qemu.spiceServerPassword!)
     }
 
     func stop(vm: VMData) {

+ 2 - 2
Remote/UTMRemoteClient.swift

@@ -268,9 +268,9 @@ extension UTMRemoteClient {
             return fileUrl
         }
 
-        func startVirtualMachine(id: UUID, options: UTMVirtualMachineStartOptions) async throws -> (port: UInt16, publicKey: Data) {
+        func startVirtualMachine(id: UUID, options: UTMVirtualMachineStartOptions) async throws -> (port: UInt16, publicKey: Data, password: String) {
             let reply = try await _startVirtualMachine(parameters: .init(id: id, options: options))
-            return (reply.spiceServerPort, reply.spiceServerPublicKey)
+            return (reply.spiceServerPort, reply.spiceServerPublicKey, reply.spiceServerPassword)
         }
 
         func stopVirtualMachine(id: UUID, method: UTMVirtualMachineStopMethod) async throws {

+ 1 - 0
Remote/UTMRemoteMessage.swift

@@ -130,6 +130,7 @@ extension UTMRemoteMessageServer {
         struct Reply: Serializable, Codable {
             let spiceServerPort: UInt16
             let spiceServerPublicKey: Data
+            let spiceServerPassword: String
         }
     }
 

+ 2 - 2
Remote/UTMRemoteServer.swift

@@ -631,8 +631,8 @@ extension UTMRemoteServer {
 
         private func _startVirtualMachine(parameters: M.StartVirtualMachine.Request) async throws -> M.StartVirtualMachine.Reply {
             let vm = try await findVM(withId: parameters.id)
-            let (port, publicKey) = try await data.startRemote(vm: vm, options: parameters.options, forClient: client)
-            return .init(spiceServerPort: port, spiceServerPublicKey: publicKey)
+            let (port, publicKey, password) = try await data.startRemote(vm: vm, options: parameters.options, forClient: client)
+            return .init(spiceServerPort: port, spiceServerPublicKey: publicKey, spiceServerPassword: password)
         }
 
         private func _stopVirtualMachine(parameters: M.StopVirtualMachine.Request) async throws -> M.StopVirtualMachine.Reply {

+ 5 - 1
Remote/UTMRemoteSpiceVirtualMachine.swift

@@ -170,7 +170,11 @@ extension UTMRemoteSpiceVirtualMachine {
                 options.insert(.hasDebugLog)
             }
             #endif
-            let ioService = UTMSpiceIO(host: server.host, tlsPort: Int(spiceServer.port), serverPublicKey: spiceServer.publicKey, options: options)
+            let ioService = UTMSpiceIO(host: server.host,
+                                       tlsPort: Int(spiceServer.port),
+                                       serverPublicKey: spiceServer.publicKey,
+                                       password: spiceServer.password,
+                                       options: options)
             ioService.logHandler = { (line: String) -> Void in
                 guard !line.contains("spice_make_scancode") else {
                     return // do not log key presses for privacy reasons

+ 5 - 0
Services/UTMExtensions.swift

@@ -384,6 +384,11 @@ extension String {
         }
         return Int(numeric)
     }
+
+    static func random(length: Int) -> String {
+        let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
+        return String((0..<length).map{ _ in letters.randomElement()! })
+    }
 }
 
 extension Encodable {

+ 2 - 0
Services/UTMQemuVirtualMachine.swift

@@ -276,10 +276,12 @@ extension UTMQemuVirtualMachine {
         let isRunningAsDisposible = options.contains(.bootDisposibleMode)
         let isRemoteSession = options.contains(.remoteSession)
         let spicePort = isRemoteSession ? try UTMSocketUtils.reservePort() : nil
+        let spicePassword = isRemoteSession ? String.random(length: 32) : nil
         await MainActor.run {
             config.qemu.isDisposable = isRunningAsDisposible
             config.qemu.spiceServerPort = spicePort
             config.qemu.isSpiceServerTlsEnabled = true
+            config.qemu.spiceServerPassword = spicePassword
         }
 
         // start TPM

+ 1 - 1
Services/UTMSpiceIO.h

@@ -58,7 +58,7 @@ NS_ASSUME_NONNULL_BEGIN
 
 - (instancetype)init NS_UNAVAILABLE;
 - (instancetype)initWithSocketUrl:(NSURL *)socketUrl options:(UTMSpiceIOOptions)options NS_DESIGNATED_INITIALIZER;
-- (instancetype)initWithHost:(NSString *)host tlsPort:(NSInteger)tlsPort serverPublicKey:(NSData *)serverPublicKey options:(UTMSpiceIOOptions)options NS_DESIGNATED_INITIALIZER;
+- (instancetype)initWithHost:(NSString *)host tlsPort:(NSInteger)tlsPort serverPublicKey:(NSData *)serverPublicKey password:(NSString *)password options:(UTMSpiceIOOptions)options NS_DESIGNATED_INITIALIZER;
 - (void)changeSharedDirectory:(NSURL *)url;
 
 - (BOOL)startWithError:(NSError * _Nullable *)error;

+ 4 - 1
Services/UTMSpiceIO.m

@@ -26,6 +26,7 @@ NSString *const kUTMErrorDomain = @"com.utmapp.utm";
 @property (nonatomic, nullable) NSString *host;
 @property (nonatomic) NSInteger tlsPort;
 @property (nonatomic, nullable) NSData *serverPublicKey;
+@property (nonatomic, nullable) NSString *password;
 @property (nonatomic) UTMSpiceIOOptions options;
 @property (nonatomic, readwrite, nullable) CSDisplay *primaryDisplay;
 @property (nonatomic) NSMutableArray<CSDisplay *> *mutableDisplays;
@@ -74,11 +75,12 @@ NSString *const kUTMErrorDomain = @"com.utmapp.utm";
     return self;
 }
 
-- (instancetype)initWithHost:(NSString *)host tlsPort:(NSInteger)tlsPort serverPublicKey:(NSData *)serverPublicKey options:(UTMSpiceIOOptions)options {
+- (instancetype)initWithHost:(NSString *)host tlsPort:(NSInteger)tlsPort serverPublicKey:(NSData *)serverPublicKey password:(NSString *)password options:(UTMSpiceIOOptions)options {
     if (self = [super init]) {
         self.host = host;
         self.tlsPort = tlsPort;
         self.serverPublicKey = serverPublicKey;
+        self.password = password;
         self.options = options;
         self.mutableDisplays = [NSMutableArray array];
         self.mutableSerials = [NSMutableArray array];
@@ -94,6 +96,7 @@ NSString *const kUTMErrorDomain = @"com.utmapp.utm";
             self.spiceConnection = [[CSConnection alloc] initWithUnixSocketFile:relativeSocketFile];
         } else {
             self.spiceConnection = [[CSConnection alloc] initWithHost:self.host tlsPort:[@(self.tlsPort) stringValue] serverPublicKey:self.serverPublicKey];
+            self.spiceConnection.password = self.password;
         }
         self.spiceConnection.delegate = self;
         self.spiceConnection.audioEnabled = (self.options & UTMSpiceIOOptionsHasAudio) == UTMSpiceIOOptionsHasAudio;

+ 1 - 1
UTM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -15,7 +15,7 @@
       "location" : "https://github.com/utmapp/CocoaSpice.git",
       "state" : {
         "branch" : "visionos",
-        "revision" : "9591cdf41282a7e6edbe7b705adbb957592ba347"
+        "revision" : "9d286ba10b8ed953bf21c04ddd64237372163132"
       }
     },
     {