Skip to content

macOS: native authentication via an SMJobBless privileged helper#1792

Open
damianrickard wants to merge 1 commit into
veracrypt:masterfrom
damianrickard:fix/macos-smjobbless-security-hardening
Open

macOS: native authentication via an SMJobBless privileged helper#1792
damianrickard wants to merge 1 commit into
veracrypt:masterfrom
damianrickard:fix/macos-smjobbless-security-hardening

Conversation

@damianrickard

Copy link
Copy Markdown
Contributor

Summary

Closes #1437.

On macOS, VeraCrypt elevated privileges through the common Unix path: it prompted for the administrator password in its own dialog and started the core service via sudo — hence the single password field, with VeraCrypt handling the admin password directly.

This PR replaces that on macOS with a code-signed launchd privileged helper installed via SMJobBless. The OS now shows its standard authentication dialog, and VeraCrypt never handles the administrator password. Linux/FreeBSD are unchanged.

Design (reuses the existing root entry point)

  • A thin, self-contained helper (src/PrivilegedHelper/Helper.cpp) exposes one privileged Mach service. On request it:
    1. validates the connecting client's code signature (audit token → SecCode → designated requirement), and
    2. validates the client-supplied app binary, then socketpair()/fork()/execs "<app> --core-service" as root and returns one socket end over XPC.
  • The returned descriptor is wrapped into the existing ServiceInputStream/ServiceOutputStream and fed the same {0,0x11,0x22} sync code, so ProcessElevatedRequests() / ProcessRequests() and all CoreServiceRequest serialization and validation are unchanged.
  • The SMJobBless install and XPC connection run in the main application process (CoreService::SendRequest), not in the unprivileged core service. The latter is a fork()ed child that never exec()s, where the Authorization/XPC/ServiceManagement frameworks cannot run.

Security model

  • Install gate: SMJobBless verifies the app's SMPrivilegedExecutables requirement against the helper's signature, and the helper's SMAuthorizedClients requirement against the app's signature (reciprocal, code-signature-pinned).
  • Native auth: AuthorizationCopyRights(kSMRightBlessPrivilegedHelper).
  • Per-connection client check: every privileged request is gated on an audit-token code-signature check of the caller inside the helper.
  • Trusted-location exec: the helper only execs a binary that is a root-owned regular file inside a non-user-writable .app/Contents/MacOS tree — every path component is verified root-owned and not group/other-writable (admin-group write is allowed only outside the bundle, e.g. /Applications), and the opened descriptor's inode is pinned and re-checked immediately before exec. Because macOS has no fexecve() and rejects exec via /dev/fd, this trusted-location requirement is what closes the validate-vs-exec race rather than executing the descriptor directly.
  • The elevated SetFileOwner and the APFS formatter now share one device-node validator (lstat-based; rejects symlinks; requires an actual block/character device) instead of a lexical path check.

Build / packaging

  • App links Security / ServiceManagement / CoreFoundation; the Blocks/XPC client glue is isolated in PrivilegedHelperClient.mm (Objective-C++).
  • Main.make builds the helper, embeds it at Contents/Library/LaunchServices/, and signs it inside-out before the app.
  • Signing identity and Team ID are build-overridable (VC_OSX_SIGN_IDENTITY, VC_OSX_TEAM_ID); committed defaults pin the project (IDRIX) identity, the Team ID is templated into the code-signing requirement strings, and the generated helper plists are regenerated on every build so a Team-ID change always takes effect.
  • notarize.sh signs/verifies the nested helper; a macOS uninstaller (veracrypt-uninstall.sh) removes the helper and its LaunchDaemon.

Notes for review

  • Trusted-location behavior change: because the helper refuses to elevate a binary that isn't root-owned in a non-user-writable bundle, VeraCrypt must be installed (e.g. the .pkg to /Applications, which is root-owned) to elevate — running it directly from a user-writable location or a mounted DMG will not. This is intentional (don't grant root to a user-writable binary), but it differs from the old sudo path, which elevated from anywhere.
  • Full Disk Access: opening raw devices requires FDA on macOS regardless of elevation mechanism (the previous sudo path's responsible process is also VeraCrypt.app). This is not a new requirement, and since the signed release keeps the org.idrix.VeraCrypt identity, an existing user's FDA grant carries over to the helper-spawned core service.
  • Deployment target: kept at macOS 12 using SMJobBless (deprecated in 13 but fully functional). If you'd prefer to require macOS 13+ and use the modern SMAppService instead, that's a contained follow-up — I didn't want to bundle a minimum-OS bump into this PR.
  • The APFS-formatter validation tightening touches pre-existing code adjacent to the new work; happy to split it out if you'd rather keep this PR strictly to the elevation mechanism.

Testing

Built and verified locally on Apple Silicon (macOS, FUSE-T):

  • SMJobBless install shows the native auth dialog; reciprocal signing requirements verified with codesign -R and a successful install.
  • Helper installs to /Library/PrivilegedHelperTools/, LaunchDaemon loads, per-connection client validation works.
  • End-to-end mount of a real external VeraCrypt device, including a hidden volume, through the helper-spawned root core service, from a root-owned /Applications install.
  • The trusted-location check rejects a user-writable copy of the same signed app ("not in a trusted install location"), confirming the exec hardening.
  • Subsequent elevated operations reuse the channel without re-prompting.

On macOS, VeraCrypt elevated privileges through the common Unix path: it
prompted for the administrator password in its own dialog and started the
core service via "sudo". This replaces that with a code-signed launchd
privileged helper installed via SMJobBless, so the OS shows its standard
authentication dialog and VeraCrypt never handles the administrator
password. Closes veracrypt#1437. Linux/FreeBSD are unchanged.

Design (reuses the existing root entry point):
- A thin, self-contained helper (PrivilegedHelper/Helper.cpp) exposes one
  privileged Mach service. On request it validates the connecting client's
  code signature (audit token -> SecCode -> designated requirement) and the
  client-supplied app binary, then socketpair()/fork()/execs
  "<app> --core-service" as root and returns one socket end over XPC.
- The returned descriptor is wrapped into the existing Service streams and
  fed the same {0,0x11,0x22} sync code, so ProcessElevatedRequests() /
  ProcessRequests() and all CoreServiceRequest serialization and validation
  are unchanged.
- The SMJobBless install and XPC connection run in the main application
  process (CoreService::SendRequest), not in the unprivileged core service:
  the latter is a fork()ed child that never calls exec(), where the
  Authorization / XPC / ServiceManagement frameworks cannot run.

Security:
- SMJobBless install gate matches the app's SMPrivilegedExecutables against
  the helper signature and the helper's SMAuthorizedClients against the app.
- Native auth via AuthorizationCopyRights(kSMRightBlessPrivilegedHelper).
- Per-connection client code-signature check inside the helper.
- The helper only execs a binary that is a root-owned regular file inside a
  non-user-writable ".app/Contents/MacOS" tree (every path component is
  verified root-owned and not group/other-writable, admin-group write
  allowed only outside the bundle), with the opened descriptor pinned and
  re-checked immediately before exec. macOS has no fexecve() and rejects
  exec via /dev/fd, so this trusted-location requirement is what closes the
  validate/exec race rather than executing the descriptor directly.
- The elevated SetFileOwner and the APFS formatter now share one device-node
  validator (lstat-based; rejects symlinks; requires a block/char device).

Build / packaging:
- Link the app with Security/ServiceManagement/CoreFoundation; the Blocks/XPC
  client glue is isolated in PrivilegedHelperClient.mm.
- Main.make builds the helper, embeds it at Contents/Library/LaunchServices,
  and signs it inside-out before the app. Signing identity and Team ID are
  build-overridable (defaults pin the IDRIX project identity); the Team ID is
  templated into the code-signing requirement strings and the generated
  plists are regenerated on every build so a Team ID change always takes
  effect.
- notarize.sh signs/verifies the nested helper; a macOS uninstaller removes
  the helper and its LaunchDaemon.
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.

VeraCrypt should adopt the macOS standard authentication dialog

1 participant