Nucleic: The MacOS VM Base Image Provisioning

This commit is contained in:
2026-07-22 23:02:26 -07:00
parent 1c7fc786ad
commit faf6484cf8
9 changed files with 448 additions and 37 deletions
@@ -30,6 +30,8 @@ public struct MacVMBundle: Sendable, Equatable {
public var diskImageURL: URL { root.appendingPathComponent("Disk.img") }
public var macAddressURL: URL { root.appendingPathComponent("MACAddress") }
public var metadataURL: URL { root.appendingPathComponent("bundle.json") }
/// Host-only MDM CA/configuration/device identity. Contains private keys; never copied into clones.
public var mdmStateURL: URL { root.appendingPathComponent("mdm-state.json") }
/// The saved runtime state (full guest RAM + device state) written by a **suspend-to-disk**
/// (`VZVirtualMachine.saveMachineState(to:)`), consumed by the next boot's
/// `restoreMachineState(from:)`. Present only while the VM is suspended to disk; deleted the
+201 -28
View File
@@ -1,11 +1,29 @@
import Foundation
import NucleicMDM
/// Guest execution target for MDM orchestration. Base-maintenance VMs are intentionally absent from
/// the ordinary `live` registry, so Mode A must be able to address their instance directly.
private enum MDMGuestTarget: Sendable {
case registered(name: String)
#if arch(arm64)
case instance(MacVMInstance, name: String)
#endif
var name: String {
switch self {
case .registered(let name): return name
#if arch(arm64)
case .instance(_, let name): return name
#endif
}
}
}
// Host-side orchestration for the Nucleic MDM plane (docs/MACOS_VM_MDM.md §4). These are the reusable
// building blocks the provisioning flow (Mode A) and the live-clone push path (Mode B) call. The MDM
// *protocol* server, CA, and artifact generation live in the `NucleicMDM` module; this file wires them
// to a running guest via the existing transports `run(name:command:)` (vsock exec) for in-guest
// shell, and the `MacVMSurfaceHost` for the one GUI approval click. Everything here is Darwin-only in
// to a running guest via the existing vsock exec transport (registered clone or direct maintenance
// instance), and the `MacVMSurfaceHost` for GUI approval. Everything here is Darwin-only in
// effect (it drives `NucleicMDMServer`, which is `#if canImport(Network)`).
//
// NOT unit-testable without a live guest the guest-side enroll, the UAMDM GUI approval, and the
@@ -24,6 +42,9 @@ extension MacVMEngine {
case guestCommandFailed(String)
case enrollmentNotApproved(String)
case missingPolicyAssets([String])
case invalidPolicyProfile(String)
case policyCommandsFailed([String: String])
case profilesMissing([String])
case drainTimedOut(pending: Int)
public var description: String {
switch self {
@@ -31,6 +52,15 @@ extension MacVMEngine {
case .enrollmentNotApproved(let s): return "MDM enrollment not approved: \(s)"
case .missingPolicyAssets(let names):
return "required MDM policy assets are missing: \(names.joined(separator: ", "))"
case .invalidPolicyProfile(let detail):
return "invalid MDM policy profile: \(detail)"
case .policyCommandsFailed(let statuses):
let summary = statuses.keys.sorted().map { "\($0)=\(statuses[$0]!)" }
.joined(separator: ", ")
return "MDM policy commands did not all acknowledge: \(summary)"
case .profilesMissing(let identifiers):
return "MDM reported success but required profiles are missing: "
+ identifiers.joined(separator: ", ")
case .drainTimedOut(let p): return "MDM command drain timed out with \(p) still queued"
}
}
@@ -61,6 +91,27 @@ extension MacVMEngine {
return nil
}
/// Extract the top-level identifiers that must appear in the guest's configuration inventory.
/// Payload UUIDs and nested payload identifiers are intentionally not accepted as substitutes.
static func configurationProfileIdentifiers(in profiles: [Data]) throws -> [String] {
try profiles.enumerated().map { index, data in
let dictionary: [String: Any]
do { dictionary = try MDMPlist.parse(data) }
catch {
throw MDMOrchestrationError.invalidPolicyProfile(
"profile \(index + 1) is not a property-list dictionary: \(error)")
}
guard dictionary["PayloadType"] as? String == "Configuration",
let identifier = dictionary["PayloadIdentifier"] as? String,
!identifier.isEmpty
else {
throw MDMOrchestrationError.invalidPolicyProfile(
"profile \(index + 1) has no top-level Configuration PayloadIdentifier")
}
return identifier
}
}
/// Load the exact policy payloads to queue during Mode A enrollment.
func resolveBuiltInMDMPolicyProfiles() throws -> [Data] {
guard let urls = Self.builtInMDMPolicyProfileURLs(
@@ -72,6 +123,17 @@ extension MacVMEngine {
return try urls.map { try Data(contentsOf: $0) }
}
/// Load the identity already associated with this base, or create it exactly once. A corrupt or
/// unsupported state file is fatal: replacing it would orphan the enrollment baked into the VM.
func loadOrCreateMDMController(for bundle: MacVMBundle) throws -> MDMServerController {
if FileManager.default.fileExists(atPath: bundle.mdmStateURL.path) {
return try MDMServerController.loadPersistedState(from: bundle.mdmStateURL)
}
let controller = try MDMServerController.makeFresh()
try controller.writePersistedState(to: bundle.mdmStateURL)
return controller
}
// MARK: - Mode A enroll the base at build time
/// Enroll the running base guest in the local MDM and install `profiles`, then verify the Mode A
@@ -84,19 +146,56 @@ extension MacVMEngine {
/// `profiles`. `hostBindAddress` is the host's vmnet-facing address the guest reaches (the gateway).
public func enrollBaseInMDM(
name: String,
bundle: MacVMBundle,
shareDir: URL,
hostBindAddress: String,
profiles: [Data],
surface: any MacVMSurfaceHost,
controller: MDMServerController? = nil,
approvalTimeout: TimeInterval = 120
) async throws -> MDMOutcome {
try await enrollBaseInMDM(
target: .registered(name: name), bundle: bundle, shareDir: shareDir,
hostBindAddress: hostBindAddress, profiles: profiles, surface: surface,
controller: controller, approvalTimeout: approvalTimeout)
}
#if arch(arm64)
/// Mode A entry point for the base-maintenance VM, which is not present in the session registry.
func enrollBaseInMDM(
instance: MacVMInstance,
name: String,
bundle: MacVMBundle,
shareDir: URL,
hostBindAddress: String,
profiles: [Data],
surface: any MacVMSurfaceHost,
controller: MDMServerController? = nil,
approvalTimeout: TimeInterval = 120
) async throws -> MDMOutcome {
try await enrollBaseInMDM(
target: .instance(instance, name: name), bundle: bundle, shareDir: shareDir,
hostBindAddress: hostBindAddress, profiles: profiles, surface: surface,
controller: controller, approvalTimeout: approvalTimeout)
}
#endif
private func enrollBaseInMDM(
target: MDMGuestTarget,
bundle: MacVMBundle,
shareDir: URL,
hostBindAddress: String,
profiles: [Data],
surface: any MacVMSurfaceHost,
controller: MDMServerController?,
approvalTimeout: TimeInterval
) async throws -> MDMOutcome {
var log: [String] = []
let controller = try controller ?? MDMServerController.makeFresh()
let controller = try controller ?? loadOrCreateMDMController(for: bundle)
// Stage artifacts into the share and pre-queue the profiles.
try controller.stageArtifacts(into: shareDir)
await controller.queueInstallProfiles(profiles)
let commandUUIDs = await controller.queueInstallProfiles(profiles)
log.append("staged MDM artifacts into \(shareDir.lastPathComponent); queued \(profiles.count) profile(s)")
// Start the server on the vmnet-facing address (+ IP SAN so the cert also matches the raw IP).
@@ -108,14 +207,13 @@ extension MacVMEngine {
// Run the guest enroll step (root; trusts the CA, pins /etc/hosts, opens the profile). This
// runs through the agent on a normally-booted guest, where the provision share is reachable at
// its automount path (the base build's space-free `provisionMountPoint` is specific to the
// HID-driven bootstrap and is unmounted before the base is sealed).
// its automount path. Mode A addresses the maintenance instance directly because base VMs
// deliberately never enter the ordinary session registry.
let scriptGuestPath =
Self.guestSharePath(os: .macOS, name: Self.provisionShareName)
+ "/\(MDMServerController.Artifact.enrollScript)"
let enroll = try await run(
name: name, command: "sudo /bin/bash \(Self.shQuote(scriptGuestPath))",
workdir: nil)
let enroll = try await runMDMGuest(
target, command: "sudo /bin/bash \(Self.shQuote(scriptGuestPath))")
guard enroll.exitCode == 0, enroll.stdout.contains("MDM_ENROLL_STAGED") else {
throw MDMOrchestrationError.guestCommandFailed(
"mdm-enroll.sh exit \(enroll.exitCode): \(enroll.stderr.suffix(400))")
@@ -123,7 +221,7 @@ extension MacVMEngine {
log.append("guest enroll staged (CA trusted, /etc/hosts pinned, profile opened)")
// Drive the UAMDM approval through System Settings Device Management (spike §8.3).
try await approveEnrollmentViaComputerUse(name: name, surface: surface)
try await approveEnrollmentViaComputerUse(target: target, surface: surface)
log.append("drove System Settings enrollment approval")
// Wait for the device's post-TokenUpdate poll to drain the queue (no APNs).
@@ -132,13 +230,28 @@ extension MacVMEngine {
let pending = await controller.queue.pendingCount
throw MDMOrchestrationError.drainTimedOut(pending: pending)
}
let installed = await controller.queue.acknowledgedCount
log.append("drained \(installed) InstallProfile command(s)")
let installed = try await acknowledgedProfileCount(
commandUUIDs: commandUUIDs, controller: controller)
log.append("acknowledged \(installed) InstallProfile command(s)")
// Verify enrollment + installed profiles from the guest's own view.
let enrolled = await verifyEnrolled(name: name)
log.append(enrolled ? "profiles status: User Approved" : "WARNING: enrollment not User-Approved")
return MDMOutcome(enrolled: enrolled, profilesInstalled: installed, log: log)
// Verify enrollment + exact installed profile identifiers from the guest's own view.
let enrolled = await verifyEnrolled(target: target)
guard enrolled else {
throw MDMOrchestrationError.enrollmentNotApproved(
"`profiles status -type enrollment` did not report User Approved")
}
log.append("profiles status: User Approved")
let requiredIdentifiers = try Self.configurationProfileIdentifiers(in: profiles)
let missingIdentifiers = try await missingInstalledProfileIdentifiers(
target: target, requiredIdentifiers: requiredIdentifiers)
guard missingIdentifiers.isEmpty else {
throw MDMOrchestrationError.profilesMissing(missingIdentifiers)
}
log.append("verified installed profiles: \(requiredIdentifiers.joined(separator: ", "))")
// Persist identifiers only after both MDM acknowledgements and the guest inventory agree.
try controller.withProfileIdentifiers(requiredIdentifiers)
.writePersistedState(to: bundle.mdmStateURL)
return MDMOutcome(enrolled: true, profilesInstalled: installed, log: log)
}
// MARK: - Mode B push to a live clone
@@ -155,7 +268,7 @@ extension MacVMEngine {
timeout: TimeInterval = 90
) async throws -> MDMOutcome {
var log: [String] = []
await controller.queue.enqueue(profiles.map { MDMCommand.installProfile($0) })
let commandUUIDs = await controller.queueInstallProfiles(profiles)
let server = controller.makeServer(log: { line in NSLog("[mdm] %@", line) })
let ipSANs = hostBindAddress == "127.0.0.1" ? [] : [hostBindAddress]
@@ -179,8 +292,9 @@ extension MacVMEngine {
guard drained else {
throw MDMOrchestrationError.drainTimedOut(pending: await controller.queue.pendingCount)
}
let installed = await controller.queue.acknowledgedCount
log.append("drained \(installed) InstallProfile command(s)")
let installed = try await acknowledgedProfileCount(
commandUUIDs: commandUUIDs, controller: controller)
log.append("acknowledged \(installed) InstallProfile command(s)")
return MDMOutcome(enrolled: true, profilesInstalled: installed, log: log)
}
@@ -206,15 +320,75 @@ extension MacVMEngine {
return "no candidate trigger succeeded (spike §8.2)"
}
/// Whether the guest reports User-Approved MDM enrollment.
/// Return required top-level profile identifiers absent from the guest's machine-level profile
/// inventory. The XML form is requested so matching stable identifiers is locale-independent.
private func missingInstalledProfileIdentifiers(
target: MDMGuestTarget, requiredIdentifiers: [String]
) async throws -> [String] {
let result = try await runMDMGuest(
target, command: "sudo profiles show -type configuration -output stdout-xml")
guard result.exitCode == 0 else {
throw MDMOrchestrationError.guestCommandFailed(
"profiles inventory exit \(result.exitCode): \(result.stderr.suffix(400))")
}
let inventory = result.stdout + result.stderr
return requiredIdentifiers.filter { !inventory.contains($0) }
}
/// Whether a registered session guest reports User-Approved MDM enrollment.
func verifyEnrolled(name: String) async -> Bool {
guard let result = try? await run(
name: name, command: "profiles status -type enrollment", workdir: nil)
await verifyEnrolled(target: .registered(name: name))
}
private func verifyEnrolled(target: MDMGuestTarget) async -> Bool {
guard let result = try? await runMDMGuest(
target, command: "profiles status -type enrollment")
else { return false }
let text = (result.stdout + result.stderr).lowercased()
return text.contains("user approved") || text.contains("mdm enrollment: yes")
}
/// Execute one bounded housekeeping command through either the ordinary registry path or the
/// direct base-maintenance instance. Direct execution reuses the same vsock/nash transport.
private func runMDMGuest(
_ target: MDMGuestTarget, command: String
) async throws -> (exitCode: Int32, stdout: String, stderr: String) {
switch target {
case .registered(let name):
return try await run(
name: name, command: command, workdir: nil,
timeoutNanos: 60 * 1_000_000_000)
#if arch(arm64)
case .instance(let instance, let name):
let channel = try await openExecChannel(
instance: instance, label: name, workdir: nil, env: [:], remoteBody: command)
async let stdout = Self.drainLines(channel.stdoutLines)
async let stderr = Self.drainLines(channel.stderrLines)
let outcome = await Self.awaitExit(channel, timeoutNanos: 60 * 1_000_000_000)
let output = await stdout
let errors = await stderr
return Self.finalize(
outcome, stdout: output, stderr: errors,
timeoutNanos: 60 * 1_000_000_000)
#endif
}
}
/// Require every command in this exact enqueue batch to finish as `Acknowledged`. A queue can be
/// drained after an `Error`, so drain alone is not a policy-installation success signal.
private func acknowledgedProfileCount(
commandUUIDs: [String], controller: MDMServerController
) async throws -> Int {
let completed = await controller.queue.completionStatuses(for: commandUUIDs)
let statuses = Dictionary(uniqueKeysWithValues: commandUUIDs.map { uuid in
(uuid, completed[uuid] ?? "Missing")
})
guard statuses.values.allSatisfy({ $0 == "Acknowledged" }) else {
throw MDMOrchestrationError.policyCommandsFailed(statuses)
}
return statuses.count
}
/// Poll the queue until it drains or the deadline passes.
private func waitForDrain(controller: MDMServerController, timeout: TimeInterval) async -> Bool {
let deadline = Date().addingTimeInterval(timeout)
@@ -230,17 +404,16 @@ extension MacVMEngine {
/// macOS 27 is the §8.3 hardware spike. Uses the same synthesized-HID surface the provisioning
/// bootstrap already uses to launch Terminal.
private func approveEnrollmentViaComputerUse(
name: String, surface: any MacVMSurfaceHost
target: MDMGuestTarget, surface: any MacVMSurfaceHost
) async throws {
// Open the Profiles/Device Management pane directly, then let the operator-recorded sequence
// (filled in during the spike) click Install enter the admin password confirm.
_ = try? await run(
name: name,
command: "open 'x-apple.systempreferences:com.apple.preferences.configurationprofiles'",
workdir: nil)
_ = try? await runMDMGuest(
target,
command: "open 'x-apple.systempreferences:com.apple.preferences.configurationprofiles'")
try? await Task.sleep(nanoseconds: 2_000_000_000)
// Placeholder approval sequence REPLACE with the spike-verified coordinates/keys (§8.3).
// The pending profile appears in the pane; double-clicking it opens the Install sheet.
await surface.send(name: name, .key(chord: "return"))
await surface.send(name: target.name, .key(chord: "return"))
}
}
+9
View File
@@ -75,6 +75,15 @@ public actor MDMCommandQueue {
public var inFlightUUID: String? { inFlight?.uuid }
public var completedCommands: [(command: MDMCommand, status: String)] { completed }
/// Terminal status for each requested command that has completed. Callers retain the UUIDs from
/// their own enqueue batch so older acknowledgements cannot make a later policy push look valid.
public func completionStatuses(for commandUUIDs: [String]) -> [String: String] {
let wanted = Set(commandUUIDs)
return Dictionary(uniqueKeysWithValues: completed.compactMap { entry in
wanted.contains(entry.command.uuid) ? (entry.command.uuid, entry.status) : nil
})
}
/// Whether every enqueued command has completed and none is in flight the signal Mode A waits
/// on before sealing the base.
public var isDrained: Bool { pending.isEmpty && inFlight == nil }
@@ -1,5 +1,6 @@
import Crypto
import Foundation
import SwiftASN1
import X509
import _CryptoExtras
@@ -35,6 +36,36 @@ public struct MDMDeviceIdentity: Sendable {
certificate: cert, privateKey: key, password: randomPassword())
}
// MARK: - Persistence (host-side only)
/// Codable representation retained beside the golden base. The private key and PKCS#12 password
/// are secrets and must never be copied into a session clone or logged.
public struct Persisted: Codable, Sendable {
public var certificatePEM: String
public var privateKeyPEM: String
public var password: String
public init(certificatePEM: String, privateKeyPEM: String, password: String) {
self.certificatePEM = certificatePEM
self.privateKeyPEM = privateKeyPEM
self.password = password
}
}
public func persisted() throws -> Persisted {
Persisted(
certificatePEM: try certificate.serializeAsPEM().pemString,
privateKeyPEM: try Certificate.PrivateKey(privateKey).serializeAsPEM().pemString,
password: password)
}
public init(persisted p: Persisted) throws {
let certificate = try Certificate(pemEncoded: p.certificatePEM)
let document = try PEMDocument(pemString: p.privateKeyPEM)
let privateKey = try _RSA.Signing.PrivateKey(derRepresentation: document.derBytes)
self.init(certificate: certificate, privateKey: privateKey, password: p.password)
}
/// The identity as a PKCS#12 blob for the profile payload.
public func pkcs12() throws -> Data {
try PKCS12.export(certificate: certificate, privateKey: privateKey, password: password)
+87 -4
View File
@@ -16,6 +16,8 @@ public struct MDMServerController: Sendable {
public let ca: MDMCertificateAuthority
public let config: MDMConfiguration
public let deviceIdentity: MDMDeviceIdentity
/// Top-level policy identifiers last verified in the enrolled base. Empty before first success.
public let profileIdentifiers: [String]
public let queue: MDMCommandQueue
/// Staged-artifact filenames (shared by the generator here and the enroll script).
@@ -38,14 +40,92 @@ public struct MDMServerController: Sendable {
public init(
ca: MDMCertificateAuthority, config: MDMConfiguration,
deviceIdentity: MDMDeviceIdentity, queue: MDMCommandQueue = MDMCommandQueue()
deviceIdentity: MDMDeviceIdentity, profileIdentifiers: [String] = [],
queue: MDMCommandQueue = MDMCommandQueue()
) {
self.ca = ca
self.config = config
self.deviceIdentity = deviceIdentity
self.profileIdentifiers = profileIdentifiers
self.queue = queue
}
// MARK: - Persistence
/// Complete host-side state required to contact an enrolled base or one of its clones later.
/// Versioning makes future migrations explicit instead of silently replacing a trusted identity.
public struct Persisted: Codable, Sendable {
public var version: Int
public var ca: MDMCertificateAuthority.Persisted
public var config: MDMConfiguration
public var deviceIdentity: MDMDeviceIdentity.Persisted
public var profileIdentifiers: [String]
public init(
version: Int = 1, ca: MDMCertificateAuthority.Persisted,
config: MDMConfiguration, deviceIdentity: MDMDeviceIdentity.Persisted,
profileIdentifiers: [String] = []
) {
self.version = version
self.ca = ca
self.config = config
self.deviceIdentity = deviceIdentity
self.profileIdentifiers = profileIdentifiers
}
}
public enum PersistenceError: Error, CustomStringConvertible {
case unsupportedVersion(Int)
public var description: String {
switch self {
case .unsupportedVersion(let version):
return "unsupported MDM controller state version \(version)"
}
}
}
public func persisted() throws -> Persisted {
try Persisted(
ca: ca.persisted(), config: config, deviceIdentity: deviceIdentity.persisted(),
profileIdentifiers: profileIdentifiers)
}
/// Return the same cryptographic identity and live queue with a newly verified policy inventory.
public func withProfileIdentifiers(_ identifiers: [String]) -> MDMServerController {
MDMServerController(
ca: ca, config: config, deviceIdentity: deviceIdentity,
profileIdentifiers: identifiers, queue: queue)
}
public init(persisted state: Persisted, queue: MDMCommandQueue = MDMCommandQueue()) throws {
guard state.version == 1 else {
throw PersistenceError.unsupportedVersion(state.version)
}
self.init(
ca: try MDMCertificateAuthority(persisted: state.ca), config: state.config,
deviceIdentity: try MDMDeviceIdentity(persisted: state.deviceIdentity),
profileIdentifiers: state.profileIdentifiers, queue: queue)
}
/// Atomically write the controller state and restrict it to the current host user. This file
/// contains the CA and device private keys; it belongs beside the base, never in the guest share.
public func writePersistedState(to url: URL) throws {
let fm = FileManager.default
try fm.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true)
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
try encoder.encode(persisted()).write(to: url, options: .atomic)
try fm.setAttributes([.posixPermissions: 0o600], ofItemAtPath: url.path)
}
public static func loadPersistedState(
from url: URL, queue: MDMCommandQueue = MDMCommandQueue()
) throws -> MDMServerController {
let state = try JSONDecoder().decode(Persisted.self, from: Data(contentsOf: url))
return try MDMServerController(persisted: state, queue: queue)
}
// MARK: - Artifacts
/// The enrollment `.mobileconfig` bytes.
@@ -115,9 +195,12 @@ public struct MDMServerController: Sendable {
// MARK: - Queue helpers
/// Queue `InstallProfile` for each payload (the notification-fix profiles in Mode A).
public func queueInstallProfiles(_ profiles: [Data]) async {
await queue.enqueue(profiles.map { MDMCommand.installProfile($0) })
/// Queue `InstallProfile` for each payload and return the command UUIDs for exact verification.
@discardableResult
public func queueInstallProfiles(_ profiles: [Data]) async -> [String] {
let commands = profiles.map { MDMCommand.installProfile($0) }
await queue.enqueue(commands)
return commands.map(\.uuid)
}
// MARK: - Server (Darwin only)
+34
View File
@@ -347,6 +347,13 @@ import Testing
#expect(desc.hasPrefix("Failed to provision the base VM:"))
}
@Test func baseBundleKeepsMDMCredentialsHostSideBesideMetadata() {
let bundle = MacVMBundle(root: URL(fileURLWithPath: "/tmp/example.bundle"))
#expect(bundle.mdmStateURL.lastPathComponent == "mdm-state.json")
#expect(bundle.mdmStateURL.deletingLastPathComponent() == bundle.root)
#expect(bundle.mdmStateURL != bundle.metadataURL)
}
// MARK: - Base status (bundle.json MacVMBaseStatus, provisioning readback)
@Test func baseStatusJSONRoundTripsAndDefaultsMissingKeys() {
@@ -1468,6 +1475,33 @@ import Testing
// MARK: - Built-in MDM policy assets
@Test func builtInMDMPoliciesExposeExactTopLevelIdentifiers() throws {
let repoRoot = URL(fileURLWithPath: #filePath)
.deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent()
let urls = try #require(MacVMEngine.builtInMDMPolicyProfileURLs(
bundleResourceURL: nil, devRepoRoot: repoRoot))
let identifiers = try MacVMEngine.configurationProfileIdentifiers(
in: urls.map { try Data(contentsOf: $0) })
#expect(identifiers == [
"xyz.blakeslee.nucleic.pppc.vmagent",
"xyz.blakeslee.nucleic.notifications.suppress",
])
}
@Test func configurationProfileIdentifierRejectsNestedOrMalformedPayloads() throws {
let nestedOnly = try PropertyListSerialization.data(
fromPropertyList: [
"PayloadType": "Configuration",
"PayloadContent": [["PayloadIdentifier": "nested.is.not.enough"]],
], format: .xml, options: 0)
#expect(throws: MacVMEngine.MDMOrchestrationError.self) {
_ = try MacVMEngine.configurationProfileIdentifiers(in: [nestedOnly])
}
#expect(throws: MacVMEngine.MDMOrchestrationError.self) {
_ = try MacVMEngine.configurationProfileIdentifiers(in: [Data("not a plist".utf8)])
}
}
@Test func builtInMDMPolicyResolutionRequiresACompleteSet() throws {
let root = FileManager.default.temporaryDirectory
.appendingPathComponent("mdm-policy-\(UUID().uuidString)", isDirectory: true)
@@ -110,4 +110,23 @@ final class MDMCommandQueueTests: XCTestCase {
let flight = await q.inFlightUUID
XCTAssertEqual(flight, "X") // still in flight
}
func testCompletionStatusesAreScopedToTheRequestedBatch() async throws {
let q = MDMCommandQueue()
await q.enqueue([
MDMCommand.installProfile(Data("old".utf8), uuid: "OLD"),
MDMCommand.installProfile(Data("ok".utf8), uuid: "OK"),
MDMCommand.installProfile(Data("bad".utf8), uuid: "BAD"),
])
_ = await q.next(afterReport: idle())
_ = await q.next(afterReport: ack("OLD"))
_ = await q.next(afterReport: ack("OK"))
_ = await q.next(afterReport:
MDMCommandStatusReport(status: "Error", commandUUID: "BAD", udid: "DEV"))
let batch = await q.completionStatuses(for: ["OK", "BAD", "MISSING"])
XCTAssertEqual(batch, ["OK": "Acknowledged", "BAD": "Error"])
XCTAssertNil(batch["OLD"])
XCTAssertNil(batch["MISSING"])
}
}
@@ -64,10 +64,56 @@ final class MDMServerControllerTests: XCTestCase {
XCTAssertGreaterThan(der.count, 300)
}
func testQueueInstallProfilesEnqueues() async throws {
func testQueueInstallProfilesEnqueuesAndReturnsBatchIDs() async throws {
let controller = try MDMServerController.makeFresh()
await controller.queueInstallProfiles([Data("a".utf8), Data("b".utf8)])
let ids = await controller.queueInstallProfiles([Data("a".utf8), Data("b".utf8)])
let count = await controller.queue.pendingCount
XCTAssertEqual(count, 2)
XCTAssertEqual(Set(ids).count, 2)
}
func testPersistedControllerRoundTripsIdentityAndRestrictsFilePermissions() async throws {
let controller = try MDMServerController.makeFresh(
config: .generate(port: 9443, topicUUID: UUID(
uuidString: "11111111-2222-3333-4444-555555555555")!))
.withProfileIdentifiers([
"xyz.blakeslee.nucleic.pppc.vmagent",
"xyz.blakeslee.nucleic.notifications.suppress",
])
let dir = FileManager.default.temporaryDirectory
.appendingPathComponent("nucleic-mdm-state-\(UUID().uuidString)")
defer { try? FileManager.default.removeItem(at: dir) }
let stateURL = dir.appendingPathComponent("mdm-state.json")
try controller.writePersistedState(to: stateURL)
let restored = try MDMServerController.loadPersistedState(from: stateURL)
XCTAssertEqual(restored.config, controller.config)
XCTAssertEqual(try restored.caCertificateDER(), try controller.caCertificateDER())
XCTAssertEqual(
try restored.deviceIdentity.persisted().certificatePEM,
try controller.deviceIdentity.persisted().certificatePEM)
XCTAssertEqual(restored.deviceIdentity.password, controller.deviceIdentity.password)
XCTAssertEqual(restored.profileIdentifiers, controller.profileIdentifiers)
// The reloaded private key still signs as the same device identity under the persisted CA.
let body = Data("persisted identity".utf8)
let signature = try MDMSignatureVerifier.sign(body: body, identity: restored.deviceIdentity)
let signatureVerified = await MDMSignatureVerifier.verify(
body: body, detachedCMS: signature, ca: restored.ca)
XCTAssertTrue(signatureVerified)
let attrs = try FileManager.default.attributesOfItem(atPath: stateURL.path)
let permissions = (attrs[.posixPermissions] as? NSNumber)?.intValue
XCTAssertEqual(permissions.map { $0 & 0o777 }, 0o600)
}
func testPersistedControllerRejectsUnknownVersion() throws {
let controller = try MDMServerController.makeFresh()
var state = try controller.persisted()
state.version = 999
XCTAssertThrowsError(try MDMServerController(persisted: state)) { error in
XCTAssertTrue(String(describing: error).contains("version 999"))
}
}
}
+17 -3
View File
@@ -86,12 +86,12 @@ remains optional until the lightweight MDM supports Declarative Device Managemen
1. declarative account creation and minimal exec-agent install through the automount;
2. MDM enrollment and required policy installation;
3. full toolchain provisioning after policy is active.
- [ ] Refactor enrollment to operate on a direct `MacVMInstance`.
- [ ] Persist the MDM CA, configuration, device identity, and profile identifiers beside the base
- [x] Refactor enrollment to operate on a direct `MacVMInstance`.
- [x] Persist the MDM CA, configuration, device identity, and profile identifiers beside the base
metadata with credential-appropriate permissions.
- [ ] Replace the UAMDM placeholder with a bounded, state-aware Device Management flow that enters
the generated guest password only in the expected authentication sheet.
- [ ] Treat enrollment false, command errors, missing profile identifiers, or failed functional
- [x] Treat enrollment false, command errors, missing profile identifiers, or failed functional
probes as fatal.
- [ ] Make re-provisioning update policy without attempting a second enrollment.
- [ ] Require `mdmEnrolled`, required profiles, agent readiness, and a production-shaped workspace
@@ -127,6 +127,20 @@ A base is releasable only when a clean macOS 27 build proves:
- re-provisioning performs no duplicate enrollment;
- a fresh clone boots, mounts, executes, and shuts down unattended.
## Implementation checkpoint — persisted Mode A state
- `mdm-state.json` now retains a versioned CA, configuration, device certificate/private key,
PKCS#12 password, and the profile identifiers verified in the guest. It is mode `0600`, remains in
the golden-base directory, and is deliberately excluded by the explicit clone copier.
- Mode A accepts a direct maintenance `MacVMInstance` and uses the existing bounded vsock/nash exec
channel instead of requiring the base to appear in the ordinary session registry.
- A drained MDM queue is no longer sufficient: every UUID from the current policy batch must report
`Acknowledged`, enrollment must report User Approved, and the guest's XML profile inventory must
contain the exact top-level identifiers before they are persisted.
- The production provisioning call site remains intentionally disabled until the macOS 27 UAMDM UI
flow and guest-to-host listener binding are validated; enabling the current placeholder would
replace one unattended prompt failure with another.
## Verification notes
- The two existing policy files parse as configuration-profile plists.