Skip to content

macOS test machine#

For the Secure Enclave keystore backend and the macOS half of the break tests. What macOS gives this product and what it does not is exordos_nac/docs/macos.md; this page is how to get a machine to run the agent on.

What a VM can and cannot answer#

The Secure Enclave is a physical coprocessor. No hypervisor exposes it — not OSX-KVM on x86, not Virtualization.framework on Apple silicon — so in a VM an Enclave key simply cannot be created, and that failure is indistinguishable from "the agent cannot reach the Enclave". Anything about the Enclave has to be measured on hardware.

A VM still answers the rest, and it is most of the surface: the launchd plumbing, paths and permissions, what code signing the LaunchAgent needs, and the fallback backend, which on macOS is the honest software level.

Running the agent's own tests there#

cgo cannot be cross-compiled from Linux, so the darwin backends build on the machine itself:

go build ./...
go test ./...
make dist-darwin        # bare binaries, for working here
make dist-macos-pkg     # the installer a fleet actually takes

Both refuse to run anywhere but a Mac, and for the same reason: a darwin build without cgo cannot reach Security.framework and silently compiles the file-key fallback instead.

The difference between them is what a Mac can do with the result. dist-darwin gives bare binaries, which is what development wants and what a fleet must never be given - the Secure Enclave entitlement is authorised by a provisioning profile, a profile lives inside an app bundle, and a bare Mach-O has nowhere to keep one. dist-macos-pkg builds that bundle, signs it if the environment carries the credentials, and wraps it in the pkg with the LaunchAgent that keeps the lease renewed.

Bringing up the VM#

Built with OSX-KVM; the host needs ignore_msrs=1 and a Ryzen or Intel CPU. Apple's licence permits macOS virtualisation on Apple hardware only — the VM is a development convenience, and anything that decides the design still needs a Mac.

Three things cost hours on this host and are worth writing down:

  • Give the guest two vCPUs, not four. The host is an AMD EPYC and OSX-KVM spoofs the CPU as Intel, which XNU tolerates unevenly: with four vCPUs the guest wedged twice, once mid-boot and once at the end of Setup Assistant. The signature is every core spinning at 100% with zero disk I/O and a frozen menu-bar clock — that is a hang, not a slow step, and only killing QEMU clears it. Two cores boots reliably.
  • Setup Assistant is genuinely slow here. "Creating account…" ran for ten minutes before completing. Distinguish it from the wedge by watching /proc/<qemu>/io and the guest clock: work in progress means writes and a ticking clock.
  • Typing over QMP can double characters. The account password came out entered twice in both fields, so it matched and the account was created with a password nobody intended. Count the dots in a screenshot before submitting, or verify afterwards rather than assuming.

Once it is up, enable SSH from a Terminal in the GUI session — systemsetup -setremotelogin on needs Full Disk Access and will refuse, but launchd will not:

sudo launchctl enable system/com.openssh.sshd
sudo launchctl bootstrap system /System/Library/LaunchDaemons/ssh.plist

A fresh install has neither a compiler nor Go. Command Line Tools is a ~900 MB download and takes roughly fifteen minutes to install on this VM:

softwareupdate -l | grep "Command Line Tools"
sudo softwareupdate -i "Command Line Tools for Xcode 26.6-26.6" --verbose

Fetch the Go tarball on the host and copy it in; the guest's user-mode NAT is slower than the host's network.

The hardware fingerprint is 1/3 on this VM and that is correct. OSX-KVM ships an all-zero IOPlatformUUID, which hwid's placeholder filter rejects on purpose — keeping it would make every OSX-KVM guest match every other one. Only the serial survives. Do not "fix" it.

The launchd domain decides what works#

Measured on a Mac17,2 and worth carrying into anything run here: the Enclave works in the Aqua and System bootstrap domains and is refused with -25308 in Background, which is what an SSH login gets. The separator is the domain, not privilege — root fails in Background and succeeds as the same uid in the other two — and not the GUI, since System has no session at all.

So an SSH measurement does not speak for the agent, and it has pointed the wrong way on both machines this was run on. Reach the login session with launchctl asuser rather than concluding anything from an SSH run.