Files
UTM/Configuration/UTMQemuConfigurationQEMU.swift
osy 862c507a26 config(qemu): preloading UEFI keys is optional
When resetting UEFI vars, add an option to preload Secure
Boot keys (previously this was always done). This allows for
the user to load their own UEFI keys.

Also update the preloaded UEFI keys to Microsoft 2023 keys.

Resolves #7005
Resolves #7257
2025-08-10 23:00:24 -07:00

231 lines
10 KiB
Swift

//
// Copyright © 2022 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
import System
/// Tweaks and advanced QEMU settings.
struct UTMQemuConfigurationQEMU: Codable {
/// Where to store the debug log. File may not exist yet. This property is not saved to file.
var debugLogURL: URL?
/// 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
/// If true, use UEFI boot on supported architectures.
var hasUefiBoot: Bool = false
/// If true, create a virtio-rng device on supported targets.
var hasRNGDevice: Bool = false
/// If true, create a virtio-balloon device on supported targets.
var hasBalloonDevice: Bool = false
/// If true, create a vTPM device with an emulated backend.
var hasTPMDevice: Bool = false
/// If true, use HVF hypervisor instead of TCG emulation.
var hasHypervisor: Bool = false
/// If true, enable total store ordering.
var hasTSO: Bool = false
/// If true, attempt to sync RTC with the local time.
var hasRTCLocalTime: Bool = false
/// If true, emulate a PS/2 controller instead of relying on USB emulation.
var hasPS2Controller: Bool = false
/// QEMU machine property that overrides the default property defined by UTM.
var machinePropertyOverride: String?
/// Additional QEMU arguments.
var additionalArguments: [QEMUArgument] = []
/// If true, changes to the VM will not be committed to disk. Not saved.
var isDisposable: Bool = false
/// Set to true to request UEFI variable reset. Not saved.
var isUefiVariableResetRequested: Bool = false
/// Set to open a port for remote SPICE session. Not saved.
var spiceServerPort: UInt16?
/// If true, all SPICE channels will be over TLS. Not saved.
var isSpiceServerTlsEnabled: Bool = false
/// Set to a password shared with the client. Not saved.
var spiceServerPassword: String?
/// When resetting UEFI vars, also set up Secure Boot keys
var hasPreloadedSecureBootKeys: Bool = false
enum CodingKeys: String, CodingKey {
case hasDebugLog = "DebugLog"
case hasUefiBoot = "UEFIBoot"
case hasRNGDevice = "RNGDevice"
case hasBalloonDevice = "BalloonDevice"
case hasTPMDevice = "TPMDevice"
case hasHypervisor = "Hypervisor"
case hasTSO = "TSO"
case hasRTCLocalTime = "RTCLocalTime"
case hasPS2Controller = "PS2Controller"
case machinePropertyOverride = "MachinePropertyOverride"
case additionalArguments = "AdditionalArguments"
}
init() {
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
hasDebugLog = try values.decode(Bool.self, forKey: .hasDebugLog)
hasUefiBoot = try values.decode(Bool.self, forKey: .hasUefiBoot)
hasRNGDevice = try values.decode(Bool.self, forKey: .hasRNGDevice)
hasBalloonDevice = try values.decode(Bool.self, forKey: .hasBalloonDevice)
hasTPMDevice = try values.decode(Bool.self, forKey: .hasTPMDevice)
hasHypervisor = try values.decode(Bool.self, forKey: .hasHypervisor)
hasTSO = try values.decodeIfPresent(Bool.self, forKey: .hasTSO) ?? false
hasRTCLocalTime = try values.decode(Bool.self, forKey: .hasRTCLocalTime)
hasPS2Controller = try values.decode(Bool.self, forKey: .hasPS2Controller)
machinePropertyOverride = try values.decodeIfPresent(String.self, forKey: .machinePropertyOverride)
additionalArguments = try values.decode([QEMUArgument].self, forKey: .additionalArguments)
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)
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(hasDebugLog, forKey: .hasDebugLog)
try container.encode(hasUefiBoot, forKey: .hasUefiBoot)
try container.encode(hasRNGDevice, forKey: .hasRNGDevice)
try container.encode(hasBalloonDevice, forKey: .hasBalloonDevice)
try container.encode(hasTPMDevice, forKey: .hasTPMDevice)
try container.encode(hasHypervisor, forKey: .hasHypervisor)
try container.encode(hasTSO, forKey: .hasTSO)
try container.encode(hasRTCLocalTime, forKey: .hasRTCLocalTime)
try container.encode(hasPS2Controller, forKey: .hasPS2Controller)
try container.encodeIfPresent(machinePropertyOverride, forKey: .machinePropertyOverride)
try container.encode(additionalArguments, forKey: .additionalArguments)
}
static func uefiImagePrefix(forArchitecture architecture: QEMUArchitecture, isVars: Bool = false) -> String? {
switch architecture {
case .arm: return "edk2-arm"
case .aarch64: return isVars ? "edk2-arm" : "edk2-aarch64"
case .i386: return "edk2-i386"
case .x86_64: return isVars ? "edk2-i386" : "edk2-x86_64"
case .loongarch64: return "edk2-loongarch64"
case .riscv64: return "edk2-riscv"
default: return nil
}
}
}
// MARK: - Default construction
extension UTMQemuConfigurationQEMU {
init(forArchitecture architecture: QEMUArchitecture, target: any QEMUTarget) {
self.init()
let rawTarget = target.rawValue
if rawTarget.hasPrefix("pc") || rawTarget.hasPrefix("q35") {
hasUefiBoot = true
hasRNGDevice = true
} else if Self.uefiImagePrefix(forArchitecture: architecture) != nil && (rawTarget.hasPrefix("virt-") || rawTarget == "virt") {
hasUefiBoot = true
hasRNGDevice = true
}
hasHypervisor = architecture.hasHypervisorSupport
}
}
// MARK: - Conversion of old config format
extension UTMQemuConfigurationQEMU {
init(migrating oldConfig: UTMLegacyQemuConfiguration) {
self.init()
hasDebugLog = oldConfig.debugLogEnabled
hasUefiBoot = oldConfig.systemBootUefi
hasRNGDevice = oldConfig.systemRngEnabled
hasHypervisor = oldConfig.useHypervisor
hasRTCLocalTime = oldConfig.rtcUseLocalTime
hasPS2Controller = oldConfig.forcePs2Controller
machinePropertyOverride = oldConfig.systemMachineProperties
if let oldAddArgs = oldConfig.systemArguments {
additionalArguments = oldAddArgs.map({ QEMUArgument($0) })
}
debugLogURL = oldConfig.existingPath?.appendingPathComponent(QEMUPackageFileName.debugLog.rawValue)
efiVarsURL = oldConfig.existingPath?.appendingPathComponent(UTMLegacyQemuConfiguration.diskImagesDirectory).appendingPathComponent(QEMUPackageFileName.efiVariables.rawValue)
}
}
// MARK: - Saving data
extension UTMQemuConfigurationQEMU {
@MainActor mutating func saveData(to dataURL: URL, for system: UTMQemuConfigurationSystem) async throws -> [URL] {
var existing: [URL] = []
if hasUefiBoot {
let fileManager = FileManager.default
// save EFI variables
let resourceURL = Bundle.main.url(forResource: "qemu", withExtension: nil)!
let templateVarsURL: URL
let secure = hasPreloadedSecureBootKeys ? "-secure" : ""
if let prefix = Self.uefiImagePrefix(forArchitecture: system.architecture, isVars: true) {
templateVarsURL = resourceURL.appendingPathComponent("\(prefix)\(secure)-vars.fd")
} else {
throw UTMQemuConfigurationError.uefiNotSupported
}
let varsURL = dataURL.appendingPathComponent(QEMUPackageFileName.efiVariables.rawValue)
let isExisting = fileManager.fileExists(atPath: varsURL.path)
if !isExisting || isUefiVariableResetRequested {
try await Task.detached {
if isExisting {
try FileManager.default.removeItem(at: varsURL)
}
try FileManager.default.copyItem(at: templateVarsURL, to: varsURL)
let permissions: FilePermissions = [.ownerReadWrite, .groupRead, .otherRead]
try FileManager.default.setAttributes([.posixPermissions: permissions.rawValue], ofItemAtPath: varsURL.path)
}.value
isUefiVariableResetRequested = false
hasPreloadedSecureBootKeys = false
}
efiVarsURL = varsURL
existing.append(varsURL)
}
let possibleTpmDataURL = dataURL.appendingPathComponent(QEMUPackageFileName.tpmData.rawValue)
if hasTPMDevice {
tpmDataURL = possibleTpmDataURL
existing.append(tpmDataURL!)
} else if FileManager.default.fileExists(atPath: possibleTpmDataURL.path) {
existing.append(possibleTpmDataURL) // do not delete any existing TPM data
}
if hasDebugLog {
let debugLogURL = dataURL.appendingPathComponent(QEMUPackageFileName.debugLog.rawValue)
existing.append(debugLogURL)
}
return existing
}
}