From 855bc0d2dff4de0c6ff863e17e4f4a97e10fbfab Mon Sep 17 00:00:00 2001 From: osy <50960678+osy@users.noreply.github.com> Date: Sat, 19 Sep 2026 23:18:45 -0500 Subject: [PATCH] config: add Secure Boot option for Apple UEFI guests macOS 27 lets the host manage Secure Boot state in the EFI variable store. Add a persisted flag and a toggle in the Boot settings so generic UEFI guests on the Apple backend can opt in. The state is reconciled when the configuration is saved, using the default platform key and the default Microsoft signatures so that mainstream Linux distributions boot unmodified. The store is only written when its state differs from the configuration, and a virtual machine with the option off never fails to save because of it. The option defaults to off because guests without a Microsoft-signed bootloader would otherwise stop booting. Assisted-by: Claude:claude-fable-5-1 --- Configuration/UTMAppleConfiguration.swift | 6 ++++ Configuration/UTMAppleConfigurationBoot.swift | 34 +++++++++++++++++-- Platform/macOS/VMConfigAppleBootView.swift | 8 +++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/Configuration/UTMAppleConfiguration.swift b/Configuration/UTMAppleConfiguration.swift index 995a9e7c..81a275bc 100644 --- a/Configuration/UTMAppleConfiguration.swift +++ b/Configuration/UTMAppleConfiguration.swift @@ -100,6 +100,8 @@ enum UTMAppleConfigurationError: Error { case hardwareModelInvalid case rosettaNotSupported case featureNotSupported + case secureBootEnrollmentFailed + case efiVariablesInaccessible } extension UTMAppleConfigurationError: LocalizedError { @@ -117,6 +119,10 @@ extension UTMAppleConfigurationError: LocalizedError { return NSLocalizedString("Rosetta is not supported on the current host machine.", comment: "UTMAppleConfiguration") case .featureNotSupported: return NSLocalizedString("The host operating system needs to be updated to support one or more features requested by the guest.", comment: "UTMAppleConfiguration") + case .secureBootEnrollmentFailed: + return NSLocalizedString("Failed to set up Secure Boot for this virtual machine.", comment: "UTMAppleConfiguration") + case .efiVariablesInaccessible: + return NSLocalizedString("The UEFI variables of this virtual machine could not be accessed.", comment: "UTMAppleConfiguration") } } } diff --git a/Configuration/UTMAppleConfigurationBoot.swift b/Configuration/UTMAppleConfigurationBoot.swift index bfc34a57..c2e89f9e 100644 --- a/Configuration/UTMAppleConfigurationBoot.swift +++ b/Configuration/UTMAppleConfigurationBoot.swift @@ -40,6 +40,7 @@ struct UTMAppleConfigurationBoot: Codable { var efiVariableStorageURL: URL? var vmSavedStateURL: URL? var hasUefiBoot: Bool = false + var hasSecureBoot: Bool = false /// IPSW for installing macOS. Not saved. var macRecoveryIpswURL: URL? @@ -51,6 +52,7 @@ struct UTMAppleConfigurationBoot: Codable { case linuxInitialRamdiskPath = "LinuxInitialRamdiskPath" case efiVariableStoragePath = "EfiVariableStoragePath" case hasUefiBoot = "UEFIBoot" + case hasSecureBoot = "SecureBoot" } init(from decoder: Decoder) throws { @@ -60,6 +62,7 @@ struct UTMAppleConfigurationBoot: Codable { let container = try decoder.container(keyedBy: CodingKeys.self) operatingSystem = try container.decode(OperatingSystem.self, forKey: .operatingSystem) hasUefiBoot = try container.decodeIfPresent(Bool.self, forKey: .hasUefiBoot) ?? false + hasSecureBoot = try container.decodeIfPresent(Bool.self, forKey: .hasSecureBoot) ?? false if let linuxKernelPath = try container.decodeIfPresent(String.self, forKey: .linuxKernelPath) { linuxKernelURL = dataURL.appendingPathComponent(linuxKernelPath) } @@ -92,6 +95,7 @@ struct UTMAppleConfigurationBoot: Codable { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(operatingSystem, forKey: .operatingSystem) try container.encode(hasUefiBoot, forKey: .hasUefiBoot) + try container.encode(hasSecureBoot, forKey: .hasSecureBoot) if operatingSystem == .linux { try container.encodeIfPresent(linuxKernelURL?.lastPathComponent, forKey: .linuxKernelPath) try container.encodeIfPresent(linuxCommandLine, forKey: .linuxCommandLine) @@ -170,8 +174,14 @@ extension UTMAppleConfigurationBoot { } let fileManager = FileManager.default let efiVariableStorageURL = dataURL.appendingPathComponent(QEMUPackageFileName.efiVariables.rawValue) - if !fileManager.fileExists(atPath: efiVariableStorageURL.path) { - _ = try VZEFIVariableStore(creatingVariableStoreAt: efiVariableStorageURL) + let variableStore: VZEFIVariableStore + if fileManager.fileExists(atPath: efiVariableStorageURL.path) { + variableStore = VZEFIVariableStore(url: efiVariableStorageURL) + } else { + variableStore = try VZEFIVariableStore(creatingVariableStoreAt: efiVariableStorageURL) + } + if #available(macOS 27, *) { + try updateSecureBoot(in: variableStore) } self.linuxKernelURL = nil self.linuxInitialRamdiskURL = nil @@ -184,4 +194,24 @@ extension UTMAppleConfigurationBoot { urls.append(vmSavedStateURL) return urls } + + /// Secure Boot state lives in the variable store, so only write to it when it differs from the configuration. + @available(macOS 27, *) + private func updateSecureBoot(in variableStore: VZEFIVariableStore) throws { + do { + if hasSecureBoot { + guard try !variableStore.isSecureBootEnabled else { + return + } + try variableStore.enrollDefaultSecureBootSignatures() + try variableStore.enableSecureBootUsingDefaultPlatformKey() + } else if (try? variableStore.isSecureBootEnabled) == true { + try variableStore.disableSecureBoot() + } + } catch let error as VZError where error.code == .efiSecureBootEnrollmentFailed { + throw UTMAppleConfigurationError.secureBootEnrollmentFailed + } catch let error as VZError where error.code == .efiVariableInaccessible { + throw UTMAppleConfigurationError.efiVariablesInaccessible + } + } } diff --git a/Platform/macOS/VMConfigAppleBootView.swift b/Platform/macOS/VMConfigAppleBootView.swift index a7b32544..dbdf042d 100644 --- a/Platform/macOS/VMConfigAppleBootView.swift +++ b/Platform/macOS/VMConfigAppleBootView.swift @@ -76,6 +76,9 @@ struct VMConfigAppleBootView: View { } else if newValue { config.genericPlatform = UTMAppleConfigurationGenericPlatform() } + if !newValue { + config.boot.hasSecureBoot = false + } }.alert(item: $alertBootloaderSelection) { selection in let okay = Alert.Button.default(Text("OK")) { importBootloaderSelection = selection @@ -93,6 +96,11 @@ struct VMConfigAppleBootView: View { allowedContentTypes: [importBootloaderSelection == .ipsw ? .ipsw : .data], onCompletion: selectImportedFile) + if #available(macOS 27, *), operatingSystem == .linux && config.boot.hasUefiBoot { + Toggle("Secure Boot", isOn: $config.boot.hasSecureBoot) + .help("Only boot operating systems signed by Microsoft or a trusted vendor. Most major Linux distributions are supported.") + } + if operatingSystem == .linux && !config.boot.hasUefiBoot { Section(header: Text("Linux Settings")) { FileBrowseField("Kernel Image", url: $config.boot.linuxKernelURL, isFileImporterPresented: $importFileShown, hasClearButton: false) {