mirror of
https://github.com/utmapp/UTM.git
synced 2026-10-12 02:24:01 +00:00
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
This commit is contained in:
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user