swtpm: add support for SWTPM

This commit is contained in:
osy
2023-07-12 23:18:36 -07:00
parent bd78994000
commit 52506771a9
9 changed files with 216 additions and 25 deletions
+1
View File
@@ -389,6 +389,7 @@ enum QEMUPackageFileName: String {
case images = "Images"
case debugLog = "debug.log"
case efiVariables = "efi_vars.fd"
case tpmData = "tpmdata"
}
// MARK: Supported features
@@ -56,6 +56,11 @@ import Virtualization // for getting network interfaces
socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("spice")
}
/// Return the socket file for communicating with SWTPM
var swtpmSocketURL: URL {
socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("swtpm")
}
/// Combined generated and user specified arguments.
@QEMUArgumentBuilder var allArguments: [QEMUArgument] {
generatedArguments
@@ -907,6 +912,32 @@ import Virtualization // for getting network interfaces
f("-device")
f("virtio-balloon-pci")
}
if qemu.hasTPMDevice {
tpmArguments
}
}
@QEMUArgumentBuilder private var tpmArguments: [QEMUArgument] {
f("-chardev")
"socket"
"id=chrtpm0"
"path=\(swtpmSocketURL.lastPathComponent)"
f()
f("-tpmdev")
"emulator"
"id=tpm0"
"chardev=chrtpm0"
f()
f("-device")
if system.target.rawValue.hasPrefix("virt") {
"tpm-tis-device"
} else if system.architecture == .ppc64 {
"tpm-spapr"
} else {
"tpm-tis"
}
"tpmdev=tpm0"
f()
}
}
+29 -20
View File
@@ -24,6 +24,9 @@ struct UTMQemuConfigurationQEMU: Codable {
/// EFI variables if EFI boot is enabled. This property is not saved to file.
var efiVarsURL: URL?
/// TPM data file if TPM is enabled. This property is not saved to file.
var tpmDataURL: URL?
/// If true, write standard output to debug.log in the VM bundle.
var hasDebugLog: Bool = false
@@ -94,6 +97,7 @@ struct UTMQemuConfigurationQEMU: Codable {
if let dataURL = decoder.userInfo[.dataURL] as? URL {
debugLogURL = dataURL.appendingPathComponent(QEMUPackageFileName.debugLog.rawValue)
efiVarsURL = dataURL.appendingPathComponent(QEMUPackageFileName.efiVariables.rawValue)
tpmDataURL = dataURL.appendingPathComponent(QEMUPackageFileName.tpmData.rawValue)
}
}
@@ -153,27 +157,32 @@ extension UTMQemuConfigurationQEMU {
extension UTMQemuConfigurationQEMU {
@MainActor mutating func saveData(to dataURL: URL, for system: UTMQemuConfigurationSystem) async throws -> [URL] {
guard hasUefiBoot else {
return []
var existing: [URL] = []
if hasUefiBoot {
let fileManager = FileManager.default
// save EFI variables
let resourceURL = Bundle.main.url(forResource: "qemu", withExtension: nil)!
let templateVarsURL: URL
if system.architecture == .arm || system.architecture == .aarch64 {
templateVarsURL = resourceURL.appendingPathComponent("edk2-arm-vars.fd")
} else if system.architecture == .i386 || system.architecture == .x86_64 {
templateVarsURL = resourceURL.appendingPathComponent("edk2-i386-vars.fd")
} else {
throw UTMQemuConfigurationError.uefiNotSupported
}
let varsURL = dataURL.appendingPathComponent(QEMUPackageFileName.efiVariables.rawValue)
if !fileManager.fileExists(atPath: varsURL.path) {
try await Task.detached {
try fileManager.copyItem(at: templateVarsURL, to: varsURL)
}.value
}
efiVarsURL = varsURL
existing.append(varsURL)
}
let fileManager = FileManager.default
// save EFI variables
let resourceURL = Bundle.main.url(forResource: "qemu", withExtension: nil)!
let templateVarsURL: URL
if system.architecture == .arm || system.architecture == .aarch64 {
templateVarsURL = resourceURL.appendingPathComponent("edk2-arm-vars.fd")
} else if system.architecture == .i386 || system.architecture == .x86_64 {
templateVarsURL = resourceURL.appendingPathComponent("edk2-i386-vars.fd")
} else {
throw UTMQemuConfigurationError.uefiNotSupported
if hasTPMDevice {
tpmDataURL = dataURL.appendingPathComponent(QEMUPackageFileName.tpmData.rawValue)
existing.append(tpmDataURL!)
}
let varsURL = dataURL.appendingPathComponent(QEMUPackageFileName.efiVariables.rawValue)
if !fileManager.fileExists(atPath: varsURL.path) {
try await Task.detached {
try fileManager.copyItem(at: templateVarsURL, to: varsURL)
}.value
}
efiVarsURL = varsURL
return [varsURL]
return existing
}
}
+2 -4
View File
@@ -76,10 +76,8 @@ struct VMConfigQEMUView: View {
.help("Should be on always unless the guest cannot boot because of this.")
Toggle("Balloon Device", isOn: $config.hasBalloonDevice)
.help("Should be on always unless the guest cannot boot because of this.")
#if false
Toggle("TPM Device", isOn: $config.hasTPMDevice)
.help("This is required to boot Windows 11.")
#endif
Toggle("TPM 2.0 Device", isOn: $config.hasTPMDevice)
.help("TPM can be used to protect secrets in the guest operating system. Note that the host will always be able to read these secrets and therefore no expectation of physical security is provided.")
Toggle("Use Hypervisor", isOn: $config.hasHypervisor)
.help("Only available if host architecture matches the target. Otherwise, TCG emulation is used.")
.disabled(!system.architecture.hasHypervisorSupport)
+5 -1
View File
@@ -27,6 +27,7 @@ typedef struct {
int (*qemu_init)(int, const char *[], const char *[]);
void (*qemu_main_loop)(void);
void (*qemu_cleanup)(void);
int (*swtpm_main)(int argc, const char *argv[], const char *prgname, const char *iface);
} qemu_main_t;
// http://mac-os-x.10953.n7.nabble.com/Ensure-NSTask-terminates-when-parent-application-does-td31477.html
@@ -61,7 +62,8 @@ static int loadQemu(const char *dylibPath, qemu_main_t *funcs) {
funcs->qemu_init = dlsym(dlctx, "qemu_init");
funcs->qemu_main_loop = dlsym(dlctx, "qemu_main_loop");
funcs->qemu_cleanup = dlsym(dlctx, "qemu_cleanup");
if (funcs->main == NULL && (funcs->qemu_init == NULL || funcs->qemu_main_loop == NULL || funcs->qemu_cleanup == NULL)) {
funcs->swtpm_main = dlsym(dlctx, "swtpm_main");
if (funcs->main == NULL && funcs->swtpm_main == NULL && (funcs->qemu_init == NULL || funcs->qemu_main_loop == NULL || funcs->qemu_cleanup == NULL)) {
fprintf(stderr, "Error resolving %s: %s\n", dylibPath, dlerror());
return -1;
}
@@ -77,6 +79,8 @@ static void __attribute__((noreturn)) runQemu(qemu_main_t *funcs, int argc, cons
pthread_detach(thread);
if (funcs->main) {
funcs->main(argc, argv);
} else if (funcs->swtpm_main) {
funcs->swtpm_main(argc, argv, "swtpm", "socket");
} else {
funcs->qemu_main_loop();
funcs->qemu_cleanup();
+1
View File
@@ -45,6 +45,7 @@ typedef int (*UTMProcessThreadEntry)(UTMProcess *self, int argc, const char * _N
- (void)accessDataWithBookmark:(NSData *)bookmark securityScoped:(BOOL)securityScoped completion:(void(^)(BOOL, NSData * _Nullable, NSString * _Nullable))completion;
- (void)stopAccessingPath:(nullable NSString *)path;
- (void)processHasExited:(NSInteger)exitCode message:(nullable NSString *)message;
- (BOOL)didLoadDylib:(void *)handle;
@end
+14
View File
@@ -138,6 +138,8 @@ final class UTMQemuVirtualMachine: UTMVirtualMachine {
private var startTask: Task<Void, any Error>?
private var swtpm: UTMSWTPM?
@MainActor required init(packageUrl: URL, configuration: UTMQemuConfiguration? = nil, isShortcut: Bool = false) throws {
self.isScopedAccess = packageUrl.startAccessingSecurityScopedResource()
// load configuration
@@ -241,6 +243,16 @@ extension UTMQemuVirtualMachine {
config.qemu.isDisposable = isRunningAsDisposible
}
// start TPM
if await config.qemu.hasTPMDevice {
let swtpm = UTMSWTPM()
swtpm.ctrlSocketUrl = await config.swtpmSocketURL
swtpm.dataUrl = await config.qemu.tpmDataURL
swtpm.currentDirectoryUrl = await config.socketURL
try await swtpm.start()
self.swtpm = swtpm
}
let allArguments = await config.allArguments
let arguments = allArguments.map({ $0.string })
let resources = allArguments.compactMap({ $0.fileUrls }).flatMap({ $0 })
@@ -524,6 +536,8 @@ extension UTMQemuVirtualMachine: QEMUVirtualMachineDelegate {
}
func qemuVMDidStop(_ qemuVM: QEMUVirtualMachine) {
swtpm?.stop()
swtpm = nil
ioService = nil
ioServiceDelegate = nil
state = .stopped
+125
View File
@@ -0,0 +1,125 @@
//
// Copyright © 2023 osy. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
private typealias SwtpmMainFunction = @convention(c) (_ argc: Int32, _ argv: UnsafeMutablePointer<UnsafePointer<CChar>>, _ prgname: UnsafePointer<CChar>, _ iface: UnsafePointer<CChar>) -> Int32
private let kMaxAttempts = 15
private let kRetryDelay = 1*NSEC_PER_SEC
class UTMSWTPM: UTMProcess {
private var swtpmMain: SwtpmMainFunction!
private var hasProcessExited: Bool = false
private var lastErrorLine: String?
var ctrlSocketUrl: URL?
var dataUrl: URL?
private override init(arguments: [String]) {
super.init(arguments: arguments)
entry = { process, argc, argv, envp in
let _self = process as! UTMSWTPM
return _self.swtpmMain(argc, argv, "swtpm", "socket")
}
standardError = Pipe()
standardError!.fileHandleForReading.readabilityHandler = { [weak self] handle in
let string = String(data: handle.availableData, encoding: .utf8) ?? ""
logger.debug("\(string)")
self?.lastErrorLine = string
}
standardOutput = Pipe()
standardOutput!.fileHandleForReading.readabilityHandler = { handle in
let string = String(data: handle.availableData, encoding: .utf8) ?? ""
logger.debug("\(string)")
}
}
convenience init() {
self.init(arguments: [])
}
override func didLoadDylib(_ handle: UnsafeMutableRawPointer) -> Bool {
let sym = dlsym(handle, "swtpm_main")
swtpmMain = unsafeBitCast(sym, to: SwtpmMainFunction.self)
return swtpmMain != nil
}
override func processHasExited(_ exitCode: Int, message: String?) {
hasProcessExited = true
if let message = message {
logger.error("SWTPM exited: \(message)")
}
}
func start() async throws {
guard let ctrlSocketUrl = ctrlSocketUrl else {
throw UTMSWTPMError.socketNotSpecified
}
guard let dataUrl = dataUrl else {
throw UTMSWTPMError.dataNotSpecified
}
let fm = FileManager.default
if !fm.fileExists(atPath: dataUrl.path) {
fm.createFile(atPath: dataUrl.path, contents: nil)
}
let dataBookmark = try dataUrl.bookmarkData()
let (success, _, _) = await accessData(withBookmark: dataBookmark, securityScoped: false)
guard success else {
throw UTMSWTPMError.cannotAccessTpmData
}
clearArgv()
pushArgv("--ctrl")
pushArgv("type=unixio,path=\(ctrlSocketUrl.lastPathComponent),terminate")
pushArgv("--tpmstate")
pushArgv("backend-uri=file://\(dataUrl.path)")
pushArgv("--tpm2")
hasProcessExited = false
try? fm.removeItem(at: ctrlSocketUrl)
try await start("swtpm.0")
// monitor for socket to be created
try await Task {
let fm = FileManager.default
for _ in 0...kMaxAttempts {
if hasProcessExited {
throw UTMSWTPMError.swtpmStartupFailed(lastErrorLine)
}
if fm.fileExists(atPath: ctrlSocketUrl.path) {
return
}
try await Task.sleep(nanoseconds: kRetryDelay)
}
}.value
}
}
enum UTMSWTPMError: Error {
case socketNotSpecified
case dataNotSpecified
case cannotAccessTpmData
case swtpmStartupFailed(String?)
}
extension UTMSWTPMError: LocalizedError {
var errorDescription: String? {
switch self {
case .socketNotSpecified: return NSLocalizedString("Socket not specified.", comment: "UTMSWTPM")
case .dataNotSpecified: return NSLocalizedString("Data not specified.", comment: "UTMSWTPM")
case .cannotAccessTpmData: return NSLocalizedString("Cannot access TPM data.", comment: "UTMSWTPM")
case .swtpmStartupFailed(let message): return String.localizedStringWithFormat(NSLocalizedString("SW TPM failed to start. %@", comment: "UTMSWTPM"), message ?? "")
}
}
}
+8
View File
@@ -141,6 +141,9 @@
845F1709289CA15C00944904 /* VMDisplayAppleTerminalWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845F1708289CA15C00944904 /* VMDisplayAppleTerminalWindowController.swift */; };
845F170B289CB07200944904 /* VMDisplayAppleDisplayWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845F170A289CB07200944904 /* VMDisplayAppleDisplayWindowController.swift */; };
845F170D289CB3DE00944904 /* VMDisplayTerminal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845F170C289CB3DE00944904 /* VMDisplayTerminal.swift */; };
845F95E32A57628400A016D7 /* UTMSWTPM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845F95E22A57628400A016D7 /* UTMSWTPM.swift */; };
845F95E42A57628400A016D7 /* UTMSWTPM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845F95E22A57628400A016D7 /* UTMSWTPM.swift */; };
845F95E52A57628400A016D7 /* UTMSWTPM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845F95E22A57628400A016D7 /* UTMSWTPM.swift */; };
846D878629050B6B0095F10B /* InAppSettingsKit in Frameworks */ = {isa = PBXBuildFile; productRef = 846D878529050B6B0095F10B /* InAppSettingsKit */; };
8471770627CC974F00D3A50B /* DefaultTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8471770527CC974F00D3A50B /* DefaultTextField.swift */; };
8471770727CC974F00D3A50B /* DefaultTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8471770527CC974F00D3A50B /* DefaultTextField.swift */; };
@@ -1317,6 +1320,7 @@
845F1708289CA15C00944904 /* VMDisplayAppleTerminalWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMDisplayAppleTerminalWindowController.swift; sourceTree = "<group>"; };
845F170A289CB07200944904 /* VMDisplayAppleDisplayWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMDisplayAppleDisplayWindowController.swift; sourceTree = "<group>"; };
845F170C289CB3DE00944904 /* VMDisplayTerminal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMDisplayTerminal.swift; sourceTree = "<group>"; };
845F95E22A57628400A016D7 /* UTMSWTPM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMSWTPM.swift; sourceTree = "<group>"; };
8471770527CC974F00D3A50B /* DefaultTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultTextField.swift; sourceTree = "<group>"; };
8471772727CD3CAB00D3A50B /* DetailedSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailedSection.swift; sourceTree = "<group>"; };
847BF9A92A49C783000BD9AA /* VMData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMData.swift; sourceTree = "<group>"; };
@@ -2298,6 +2302,7 @@
E2D64BC7241DB24B0034E0C6 /* UTMSpiceIO.h */,
E2D64BC8241DB24B0034E0C6 /* UTMSpiceIO.m */,
E2D64BE0241EAEBE0034E0C6 /* UTMSpiceIODelegate.h */,
845F95E22A57628400A016D7 /* UTMSWTPM.swift */,
CE020BB524B14F8400B44AB6 /* UTMVirtualMachine.swift */,
CE928C2926ABE6690099F293 /* UTMAppleVirtualMachine.swift */,
841E999728AC817D003C6CB6 /* UTMQemuVirtualMachine.swift */,
@@ -2948,6 +2953,7 @@
CE2D956924AD4F990059923A /* VMPlaceholderView.swift in Sources */,
CE2D931524AD46670059923A /* VMDisplayMetalViewController+Pointer.m in Sources */,
84018692288A73310050AC51 /* VMDisplayViewController.m in Sources */,
845F95E32A57628400A016D7 /* UTMSWTPM.swift in Sources */,
84CE3DB12904C7A100FF068B /* UTMSettingsView.swift in Sources */,
843BF83428450C0B0029D60D /* UTMQemuConfigurationSound.swift in Sources */,
CE2D932224AD46670059923A /* VMScroll.m in Sources */,
@@ -3031,6 +3037,7 @@
CE2D957624AD4F990059923A /* VMConfigInputView.swift in Sources */,
84F746B9276FF40900A20C87 /* VMDisplayAppleWindowController.swift in Sources */,
CE2D958E24AD4F990059923A /* UTMApp.swift in Sources */,
845F95E52A57628400A016D7 /* UTMSWTPM.swift in Sources */,
844EC0FB2773EE49003C104A /* UTMDownloadIPSWTask.swift in Sources */,
8432329228C2CDAD00CFBC97 /* VMNavigationListView.swift in Sources */,
CE0B6CF324AD568400FE012D /* UTMLegacyQemuConfiguration.m in Sources */,
@@ -3195,6 +3202,7 @@
84B36D2A27B790BE00C22685 /* DestructiveButton.swift in Sources */,
842B9F8E28CC58B700031EE7 /* UTMPatches.swift in Sources */,
CEE7E937287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m in Sources */,
845F95E42A57628400A016D7 /* UTMSWTPM.swift in Sources */,
841619AB284315F9000034B2 /* UTMConfigurationInfo.swift in Sources */,
CEA45E6F263519B5002FA97D /* VMConfigInfoView.swift in Sources */,
CEA45E72263519B5002FA97D /* VMKeyboardView.m in Sources */,