Skip to content

Verify installer package signatures before running as root#3

Merged
rodchristiansen merged 2 commits into
mainfrom
feat/package-signature-verification
Jun 16, 2026
Merged

Verify installer package signatures before running as root#3
rodchristiansen merged 2 commits into
mainfrom
feat/package-signature-verification

Conversation

@rodchristiansen

Copy link
Copy Markdown
Contributor

Problem

PackageManager.installPackage ran /usr/sbin/installer -pkg (as root) on any file that matched the manifest's SHA-256. But the hash only proves the download matches the manifest — it does not prove the manifest is authentic. If the manifest or its host/CDN is compromised, the hash check happily validates attacker-supplied bytes and we execute them as root on every freshly provisioned Mac. This is the highest-severity gap from the recent audit.

Fix

Add SignatureVerifier, which gates every package install on pkgutil --check-signature:

  • The package must carry a signature trusted by macOS (non-zero pkgutil exit ⇒ unsigned/untrusted).
  • When an expected Apple Team ID is configured, the parsed leaf-certificate Team ID must match.
  • Unsigned/untrusted packages are refused unless allowUnsigned is explicitly set (then permitted with a warning).
  • A Team-ID mismatch is never bypassed, even with allowUnsigned — an explicit mismatch is a strong tampering signal.

Defaults are secure: verification on, no unsigned allowed, any macOS-trusted signature accepted unless a Team ID is pinned.

Configuration

Managed preferences (com.github.bootstrapmate): verifyPackageSignatures (bool, default true), expectedTeamID (string), allowUnsigned (bool, default false).

CLI: --no-verify-signature, --expected-team-id <TEAMID>, --allow-unsigned.

Per-item manifest overrides (fall back to global config): expectedTeamID, allowUnsigned.

Tests

swift build and swift test pass. Adds a SignatureVerifier suite covering Team-ID parsing and the allow/deny policy (including that a Team-ID mismatch is never bypassed). README + example mobileconfig updated.

Notes

The manifest SHA-256 only proves a download matches the manifest; it does
not prove the manifest itself is authentic. If the manifest or its host is
compromised, the hash check validates attacker-supplied bytes and we run
them as root via /usr/sbin/installer.

Add SignatureVerifier, which gates every package install on
pkgutil --check-signature: a package must carry a signature trusted by
macOS and, when configured, match an expected Apple Team ID. Unsigned or
untrusted packages are refused unless allowUnsigned is explicitly set; a
Team-ID mismatch is never bypassed.

Verification is configurable via managed preferences
(verifyPackageSignatures, expectedTeamID, allowUnsigned), CLI flags
(--no-verify-signature, --expected-team-id, --allow-unsigned), and
per-item manifest overrides. Defaults are secure (verify on, no unsigned).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a macOS-trusted signature gate (via pkgutil --check-signature) before running installer packages as root, with optional Team ID pinning and explicit policy controls (global config + per-item overrides + CLI flags).

Changes:

  • Introduces SignatureVerifier to validate .pkg trust and optionally enforce an expected Apple Team ID.
  • Threads signature policy through config/CLI/manifest and enforces it during package installation.
  • Adds unit tests plus documentation/mobileconfig updates for the new settings.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
Tests/BootstrapMateCoreTests/BootstrapMateCoreTests.swift Adds unit tests covering Team ID parsing and allow/deny policy.
Sources/core/Managers/SignatureVerifier.swift New verifier that runs pkgutil --check-signature, parses Team ID, and applies allow/deny policy.
Sources/core/Managers/PackageManager.swift Gates installs on SignatureVerifier before invoking /usr/sbin/installer.
Sources/core/Managers/ManifestManager.swift Adds per-item manifest overrides for expectedTeamID and allowUnsigned.
Sources/core/Managers/IAOrchestrator.swift Computes effective per-item signature policy (item override → global config) and passes to PackageManager.
Sources/core/Managers/ConfigManager.swift Adds managed-pref + CLI wiring for verifyPackageSignatures, expectedTeamID, and allowUnsigned.
Sources/cli/bootstrapmate.swift Adds CLI flags/options to control signature verification and Team ID requirements.
README.md Documents the signature verification feature and configuration keys.
examples/config.mobileconfig Updates example profile with the new signature-verification keys.
Comments suppressed due to low confidence (1)

Sources/core/Managers/PackageManager.swift:62

  • Process.launch() can raise an Objective-C exception if /usr/sbin/installer cannot be executed, which would crash the process. Since this code runs during provisioning, handle launch failures with try task.run() + do/catch and return false with a clear log message.
        let task = Process()
        task.launchPath = "/usr/sbin/installer"
        task.arguments = ["-pkg", pkgPath, "-target", "/"]
        task.launch()
        task.waitUntilExit()

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +55 to +63
let foundTeamID = Self.parseTeamID(from: output)

if let expected = expectedTeamID, !expected.isEmpty {
guard let found = foundTeamID, found == expected else {
return .teamIDMismatch(found: foundTeamID, expected: expected)
}
}

return .signed(teamID: foundTeamID)
- Trim + uppercase both the configured expected Team ID and the parsed
  Team ID before comparing, so stray whitespace/lowercase from MDM or CLI
  input no longer causes false mismatches.
- Launch /usr/sbin/installer via try run() with do/catch so a launch
  failure is handled and logged instead of raising an uncaught ObjC
  exception during provisioning.
@rodchristiansen rodchristiansen merged commit fbf0034 into main Jun 16, 2026
2 checks passed
@rodchristiansen rodchristiansen deleted the feat/package-signature-verification branch June 16, 2026 01:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants