Skip to content

Commit

Permalink
Merge pull request #5464 from utmapp/feature/tpm
Browse files Browse the repository at this point in the history
Add TPM 2.0 and Secure Boot support
  • Loading branch information
osy committed Jul 14, 2023
2 parents 421e002 + f8d5203 commit 7e88e99
Show file tree
Hide file tree
Showing 33 changed files with 2,945 additions and 137 deletions.
16 changes: 16 additions & 0 deletions Configuration/QEMUConstant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -444,6 +445,14 @@ extension QEMUArchitecture {
return false
#endif
}

var hasSecureBootSupport: Bool {
switch self {
case .x86_64, .i386: return true
case .aarch64: return true
default: return false
}
}
}

extension QEMUTarget {
Expand All @@ -460,4 +469,11 @@ extension QEMUTarget {
default: return true
}
}

var hasSecureBootSupport: Bool {
switch self.rawValue {
case "microvm": return false
default: return true
}
}
}
57 changes: 49 additions & 8 deletions Configuration/UTMQemuConfiguration+Arguments.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ import Virtualization // for getting network interfaces
QEMUArgumentFragment(final: string)
}

/// Return the socket file for communicating with SPICE
var spiceSocketURL: URL {
/// Shared between helper and main process to store Unix sockets
var socketURL: URL {
#if os(iOS)
let parentURL = FileManager.default.temporaryDirectory
return FileManager.default.temporaryDirectory
#else
let appGroup = Bundle.main.infoDictionary?["AppGroupIdentifier"] as? String
let helper = Bundle.main.infoDictionary?["HelperIdentifier"] as? String
Expand All @@ -44,11 +44,21 @@ import Virtualization // for getting network interfaces
parentURL.appendPathComponent("tmp")
if let appGroup = appGroup, !appGroup.hasPrefix("invalid.") {
if let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) {
parentURL = containerURL
return containerURL
}
}
return parentURL
#endif
return parentURL.appendingPathComponent("\(information.uuid.uuidString).spice")
}

/// Return the socket file for communicating with SPICE
var spiceSocketURL: URL {
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.
Expand Down Expand Up @@ -100,8 +110,7 @@ import Virtualization // for getting network interfaces
@QEMUArgumentBuilder private var spiceArguments: [QEMUArgument] {
f("-spice")
"unix=on"
"addr="
spiceSocketURL
"addr=\(spiceSocketURL.lastPathComponent)"
"disable-ticketing=on"
"image-compression=off"
"playback-compression=off"
Expand Down Expand Up @@ -286,6 +295,10 @@ import Virtualization // for getting network interfaces
system.architecture.hasUsbSupport && system.target.hasUsbSupport && input.usbBusSupport != .disabled
}

private var isSecureBootUsed: Bool {
system.architecture.hasSecureBootSupport && system.target.hasSecureBootSupport && qemu.hasTPMDevice
}

@QEMUArgumentBuilder private var machineArguments: [QEMUArgument] {
f("-machine")
system.target
Expand Down Expand Up @@ -361,7 +374,9 @@ import Virtualization // for getting network interfaces
f("ICH9-LPC.disable_s3=1") // applies for pc-q35-* types
}
if qemu.hasUefiBoot {
let bios = resourceURL.appendingPathComponent("edk2-\(system.architecture.rawValue)-code.fd")
let secure = isSecureBootUsed ? "-secure" : ""
let code = system.target.rawValue == "microvm" ? "microvm" : "code"
let bios = resourceURL.appendingPathComponent("edk2-\(system.architecture.rawValue)\(secure)-\(code).fd")
let vars = qemu.efiVarsURL ?? URL(fileURLWithPath: "/\(QEMUPackageFileName.efiVariables.rawValue)")
if !hasCustomBios && FileManager.default.fileExists(atPath: bios.path) {
f("-drive")
Expand Down Expand Up @@ -903,6 +918,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-crb-device"
} else if system.architecture == .ppc64 {
"tpm-spapr"
} else {
"tpm-crb"
}
"tpmdev=tpm0"
f()
}
}

Expand Down
54 changes: 35 additions & 19 deletions Configuration/UTMQemuConfigurationQEMU.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -63,6 +66,9 @@ struct UTMQemuConfigurationQEMU: Codable {
/// Set to true to request guest tools install. Not saved.
var isGuestToolsInstallRequested: Bool = false

/// Set to true to request UEFI variable reset. Not saved.
var isUefiVariableResetRequested: Bool = false

enum CodingKeys: String, CodingKey {
case hasDebugLog = "DebugLog"
case hasUefiBoot = "UEFIBoot"
Expand Down Expand Up @@ -94,6 +100,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)
}
}

Expand Down Expand Up @@ -153,27 +160,36 @@ 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
if hasDebugLog {
let debugLogURL = dataURL.appendingPathComponent(QEMUPackageFileName.debugLog.rawValue)
existing.append(debugLogURL)
}
efiVarsURL = varsURL
return [varsURL]
return existing
}
}
11 changes: 7 additions & 4 deletions Platform/Shared/VMConfigQEMUView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -96,6 +94,11 @@ struct VMConfigQEMUView: View {
.disabled(!supportsPs2)
.help("Instantiate PS/2 controller even when USB input is supported. Required for older Windows.")
}
DetailedSection("Maintenance", description: "Options here only apply on next boot and are not saved.") {
Toggle("Reset UEFI Variables", isOn: $config.isUefiVariableResetRequested)
.help("You can use this if your boot options are corrupted or if you wish to re-enroll in the default keys for secure boot.")
.disabled(!config.hasUefiBoot)
}
DetailedSection("QEMU Machine Properties", description: "This is appended to the -machine argument.") {
DefaultTextField("", text: $config.machinePropertyOverride.bound, prompt: "Default")
}
Expand Down
9 changes: 9 additions & 0 deletions Platform/Shared/VMWizardOSWindowsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ struct VMWizardOSWindowsView: View {
.onChange(of: wizardState.isWindows10OrHigher) { newValue in
if newValue {
wizardState.systemBootUefi = true
wizardState.systemBootTpm = true
wizardState.isGuestToolsInstallRequested = true
} else {
wizardState.systemBootTpm = false
wizardState.isGuestToolsInstallRequested = false
}
}
Expand Down Expand Up @@ -79,6 +81,13 @@ struct VMWizardOSWindowsView: View {
if !wizardState.isWindows10OrHigher {
DetailedSection("", description: "Some older systems do not support UEFI boot, such as Windows 7 and below.") {
Toggle("UEFI Boot", isOn: $wizardState.systemBootUefi)
.onChange(of: wizardState.systemBootUefi) { newValue in
if !newValue {
wizardState.systemBootTpm = false
}
}
Toggle("Secure Boot with TPM 2.0", isOn: $wizardState.systemBootTpm)
.disabled(!wizardState.systemBootUefi)
}
}

Expand Down
2 changes: 2 additions & 0 deletions Platform/Shared/VMWizardState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ enum VMWizardOS: String, Identifiable {
@Published var alertMessage: AlertMessage?
@Published var isBusy: Bool = false
@Published var systemBootUefi: Bool = true
@Published var systemBootTpm: Bool = true
@Published var isGuestToolsInstallRequested: Bool = true
@Published var useVirtualization: Bool = false {
didSet {
Expand Down Expand Up @@ -369,6 +370,7 @@ enum VMWizardOS: String, Identifiable {
if operatingSystem == .Windows {
// only change UEFI settings for Windows
config.qemu.hasUefiBoot = systemBootUefi
config.qemu.hasTPMDevice = systemBootTpm
}
if operatingSystem == .Linux && config.displays.first != nil {
// change default display to virtio-gpu if supported
Expand Down
6 changes: 5 additions & 1 deletion QEMUHelper/QEMUHelper.m
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ @interface QEMUHelper ()
@implementation QEMUHelper

@synthesize environment;
@synthesize currentDirectoryPath;

- (instancetype)init {
if (self = [super init]) {
Expand Down Expand Up @@ -128,10 +129,13 @@ - (void)startQemuTask:(NSString *)binName standardOutput:(NSFileHandle *)standar
[environment addEntriesFromDictionary:self.environment];
}
task.environment = environment;
if (self.currentDirectoryPath) {
task.currentDirectoryURL = [NSURL fileURLWithPath:self.currentDirectoryPath];
}
task.qualityOfService = NSQualityOfServiceUserInitiated;
task.terminationHandler = ^(NSTask *task) {
_self.childTask = nil;
[_self.connection.remoteObjectProxy qemuHasExited:task.terminationStatus message:nil];
[_self.connection.remoteObjectProxy processHasExited:task.terminationStatus message:nil];
};
if (![task launchAndReturnError:&err]) {
NSLog(@"Error starting QEMU: %@", err);
Expand Down
2 changes: 1 addition & 1 deletion QEMUHelper/QEMUHelperDelegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ NS_ASSUME_NONNULL_BEGIN

@protocol QEMUHelperDelegate <NSObject>

- (void)qemuHasExited:(NSInteger)exitCode message:(nullable NSString *)message;
- (void)processHasExited:(NSInteger)exitCode message:(nullable NSString *)message;

@end

Expand Down
1 change: 1 addition & 0 deletions QEMUHelper/QEMUHelperProtocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ NS_ASSUME_NONNULL_BEGIN
@protocol QEMUHelperProtocol

@property (nonatomic, nullable) NSDictionary<NSString *, NSString *> *environment;
@property (nonatomic, nullable) NSString *currentDirectoryPath;

- (void)accessDataWithBookmark:(NSData *)bookmark securityScoped:(BOOL)securityScoped completion:(void(^)(BOOL, NSData * _Nullable, NSString * _Nullable))completion;
- (void)stopAccessingPath:(nullable NSString *)path;
Expand Down
6 changes: 5 additions & 1 deletion QEMULauncher/Bootstrap.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
Expand All @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion Services/Swift-Bridging-Header.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
#include "UTMLegacyQemuConfiguration+Sharing.h"
#include "UTMLegacyQemuConfiguration+System.h"
#include "UTMLegacyQemuConfigurationPortForward.h"
#include "UTMQemu.h"
#include "UTMProcess.h"
#include "UTMQemuSystem.h"
#include "UTMJailbreak.h"
#include "UTMLogging.h"
Expand Down
Loading

0 comments on commit 7e88e99

Please sign in to comment.