Opt-in privileged helper for admin operations - #346
Open
caezium wants to merge 1 commit into
Open
Conversation
Burrow's elevated operations run through `osascript … with administrator privileges`, which is password-only by construction: the `system.privilege.admin` right authenticates through SecurityAgent's classic mechanism, which never offers Touch ID. It also cannot be cancelled safely — killing osascript orphans the root child it spawned. This adds an opt-in `SMAppService` launch daemon as an alternative route, and leaves the osascript path in place as the fallback for anyone who declines it. The helper accepts three typed operations — scan, clean, optimize — and derives argv from the enum itself. There is no field for a path, an argument, a shell string, or an executable, so a caller that fully controls the XPC payload still cannot express "run this". Security properties, each enforced rather than documented: - Fresh authentication per root operation. The right is defined with `timeout: 0`, `shared: false`, and `allow-root: false`, so no credential survives a call, none is shared, and the root daemon cannot satisfy the right by virtue of being root. Registering the helper authorizes nothing. - The prompt is raised by the DAEMON. The client externalizes an empty, unauthenticated `AuthorizationRef`; the daemon rebuilds it and calls `AuthorizationCopyRights` with a non-empty rights set and interaction allowed. A GUI-side prompt would be cosmetic — the privileged side has to be what demands the right. - Callers are pinned by `NSXPCListener.setConnectionCodeSigningRequirement` (bundle identifier + Apple anchor + signing team), evaluated by the system against the real peer. The requirement is built at runtime from the helper's own signing information, so no team ID is hardcoded. - One authorization, one operation: operation IDs must be UUIDs and are served at most once, so a captured payload cannot be replayed. - Version skew is refused. A registered daemon outlives the app that installed it, so a build mismatch routes back to osascript instead of running as root with a stale idea of what `clean` does. - The engine is resolved relative to the helper's own executable and its signature is verified before it runs as root — never PATH, never an environment variable. Routing is a pure function: the helper is used only when the argv maps onto a typed operation, the daemon is registered and enabled, and its build matches. Anything else keeps the existing path unchanged. sign-macos-app.sh now fails closed if the helper is missing, unsigned, not hardened, or misdeclared to launchd — including a BundleProgram that points at anything other than the executable the pipeline just verified. SECURITY.md previously stated that Burrow installs no privileged helper and no XPC root service; that claim is now scoped to the default configuration and the opt-in helper's guarantees are spelled out.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces password-only
osascriptelevation with an opt-in, Developer ID-signed launch daemon, so Clean/Optimize/scan can authenticate with Touch ID. The osascript path stays exactly as it is and remains the fallback for anyone who declines the helper — declining costs the Touch ID prompt and safe cancellation, never the authentication itself.Do not merge yet. The runtime behaviour that can only be checked on a signed build is listed at the bottom.
Why
system.privilege.adminauthenticates through SecurityAgent's classic mechanism, which never offers Touch ID — the existing code comment inMoleCLI.elevatedScriptalready says so. The same path also can't be cancelled: terminatingosascriptorphans the root child it spawned, which is whyOperationFlowhas no safe cancel for elevated runs today.Scope of what the helper can do
Three typed operations —
scan,clean,optimize— and the daemon derives argv from the enum itself:These are exactly what
CleanView/OptimizeView/TuneUpViewpass today, so the helper changes how a command is elevated, never what runs. The request type has three fields — operation, operation ID, client build — and no field for a path, an argument, a shell string, or an executable. A caller that fully controls the XPC payload still cannot express "run this".Security properties, and where each is enforced
timeout: 0,shared: falseallow-root: falseAuthorizationCopyRightswith a non-empty rights set +interactionAllowed; client never pre-authorizesNSXPCListener.setConnectionCodeSigningRequirement— identifier + Apple anchor + signing teamHelperReplayGuard)PATH/env droppedTwo details worth calling out because both are one-line ways to make the whole thing decorative, and both are covered by tests:
LAContextin the app, then send the request — is not an authorization at all: a caller that skipped the prompt would be indistinguishable from one that passed it. The client externalizes an empty, unauthenticatedAuthorizationRefand the root side is what demands the right.connection.processIdentifierand verifying that process is the classic vulnerable pattern (PIDs get recycled and raced). Doing it properly needs the audit token, and reaching anNSXPCConnection's audit token means private API — not a dependency worth taking in a root daemon. The macOS 13 requirement API is supported and evaluated in the kernel against the real peer.Routing
PrivilegeRoute.decideis pure. The helper is used only when the argv maps onto a typed operation and the daemon is registered and enabled and the build matches. Every other combination returns.osascript.PrivilegeRouteTestswalks the full cross-product so no future edit can produce a.helperroute from a state that isn't fully green.Release gate
sign-macos-app.shnow fails closed if the helper is missing, not Mach-O, unsigned, not hardened, on a different team, or misdeclared to launchd — including aBundleProgrampointing at anything other than the executable the pipeline just verified, which is the one that would put an unvetted binary behind root. The gate is mandatory, so "no helper" is an error rather than a skip; the existing signer test fixtures were updated to stage one rather than weakening the check.Docs
SECURITY.md said "Burrow installs no privileged/background helper and no XPC root service." That is now scoped to the default configuration, with the opt-in helper's guarantees spelled out. README's equivalent claim updated too.
Verification done
HelperContractTests,HelperAuthorizationTests,HelperCodeRequirementTests,PrivilegeRouteTests).Contents/MacOS/BurrowHelper, plist atContents/Library/LaunchDaemons/,CFBundleVersionbaked into the binary, strict signature verification passes.Still needs an on-device check before merge
None of this can be exercised without a Developer ID build and a real admin approval:
AuthorizationCopyRightswithinteractionAllowed, called from a root daemon against a client-supplied external form, is the documented pattern — but that the prompt appears correctly in the user's session, and thattimeout: 0doesn't make it re-prompt mid-operation, needs to be seen.class: user/group: adminright, the security properties are unchanged but the headline benefit isn't there, and the rule needs revisiting.LAPolicy.deviceOwnerAuthenticationaccepts a paired Watch; SecurityAgent does not. That is a deviation from the decision as originally framed — the Touch-ID-then-password behaviour is preserved, the Watch is not.SMAppService.register()approval flow and removal via Login Items & Extensions.