Last revised: 09Aug2003 GLG
Table of Contents
If you haven't already read Tamper-Resistant Mac OS X App-Bundles, you should. It explains why applications that use AuthKit should be owned by root and unwritable.
Many tools that use AuthKit for specific purposes will have a common structure:The app.authkit.envoy package contains a few relatively simple classes that provide a framework for building tools that share this common structure. It isn't a general-purpose framework for all AuthKit-based applications, so it isn't appropriate for all uses. However, it does make certain kinds of tools, especially developer tools, easier to write.
- an unprivileged main process and a privileged child process
- the unprivileged process handles parameter gathering, validation, and presentation of results
- the privileged process is simple, non-interactive, and as small as possible
- the privileged process may execute other external commands as root
- communication between the two processes may be contrained: by the coupling of the inter-process pipe's streams, by the inability to wait for the child to terminate, by the inability to force the child to terminate, etc.
The app.authkit.envoy package contains the following principal classes:
- Envoy
- This is a concrete main class and entry-point for a privileged process. Its main() method expects to be run via Authorization.execPrivileged(), so it takes instructions on what to do from its args and system-properties. It is designed to run Tasks and return their results to the parent process over the inter-process pipe. It always tries to attach to a privileged session with attachPrivileged(), so it will fail and do nothing if it's executed in any other way. It can't be run from a typical shell command-line, even as root, because attachPrivileged() only works when the process is running as a child by execPrivileged(). This limitation is intentional.
- Task
- This is an abstract interface, representing a task for Envoy to perform. A Task may perform a single action using only Java, or it may execute an external command, or it may use a mix of Java actions and external commands to accomplish its work. The more complex a Task is, the more likely it is to have bugs. Since Tasks are frequently run under an effective user-ID of root, simpler is safer.
- Tool
- This abstract class represents the main class and entry-point for the unprivileged process that runs a privileged Envoy process to perform Tasks with elevated privileges. Although a Tool subclass and the Envoy class run as different processes, they typically share a classpath, so that the pair of processes running together does something useful. This is typical, but not required, as you may have security reasons for keeping Envoy and your Tasks in one location, and your Tool subclasses in another.
The app.authkit.tools package expands on the app.authkit.envoy package, adding some building-block classes along with concrete Tool and Task implementations. Within the package, the Tool implementations are in one package, while the Tasks are in another. This emphasizes that Tasks are run in a separate privileged Envoy process, which is launched by a Tool but is otherwise inaccessible to it.The fundamental separation between Tools in one process and Tasks in another affects how Tasks can be implemented. For example, if a Task stores values in what it thinks is a static Tool class, the Tool class that's really controlling things will be an entirely different instance in an altogether different process . Therefore, a Task can't affect any Tool class directly, nor can the Tool class affect the Task class. You must design your Tool and Task classes to work under this separation. It is an essential part of the Tool/Envoy relationship.
The app.authkit.tools package contains:
The app.authkit.tools.tasks package contains:
- AppBundleLockdown
- This Tool is a hybrid command-line/GUI tool for locking down an app-bundle. If invoked with no args, it will present a FileDialog asking to choose an app-bundle. It validates every target item as an app-bundle, then runs a privileged Envoy process. That process performs the AppBundleLockdownTask with an effective user-id of root.
The lock-down itself consists of making every file and directory in the app-bundle unwritable to everyone (owner, group, and others), removing any setuid bit anywhere in the app-bundle, and then changing the owner of every file and directory to root. The rationale behind this is explained elsewhere.
This Tool is explained in more detail below.
- PerformAsRoot
- This Tool is a general-purpose command-line tool for performing any Task as root. ITS GENERALITY MAKES IT DANGEROUS. It takes a series of task-names and task-args on its command-line, authenticates the user, then launches an Envoy process to run the tasks as root. The Envoy protocol stream is always echoed to stdout.
Two Tasks often used with PerformAsRoot are RunEat and RunOut, which execute an arbitrary external program and either consume or return its stdout.
- AppBundleLockdownTask
- This Task performs the lockdown of a verified app-bundle. It uses the external programs /usr/bin/find, /bin/chmod, and /usr/sbin/chown to perform its operations. It does not produce output, only error-messages and result-codes.
This Task removes setuid and all write permissions from all the files and directories in an app-bundle. The write permissions are removed to prevent any changes to any file or directory in the app-bundle. The setuid permissions are removed to prevent subsequent ownership by root from making a file setuid-root simply because it was setuid-anyone before. This Task does not change the setgid, read, or search/execute permissions, nor the group assigned to any file or directory in the app-bundle.
This Task also normally deletes any file in the app-bundle whose name matches the shell-pattern ".*DS_Store". This can save significant space, especially in a disk-image destined for distribution.
- CommonTask
- This is a base for building concrete Tasks. It contains utility methods for executing external programs, among other things.
- None
- This Task does nothing. It's a test to make sure that Envoy can find it, perform it, and return its status.
- RunEat
- This Task runs any external program and consumes its stdout. Its task-args represent the command to run and the command's args.
- RunOut
- This Task runs any external program and returns its stdout through Envoy to the unprivileged parent process. Its task-args represent the command to run and the command's args.
This Tool works as either a command-line program or a GUI-based interactive one. It requires Mac OS 10.1 or higher, and will refuse to run on 10.0. Its interactiveness varies depending on the command-line args you provide or omit, and on system properties you predefine. The GUI authentication dialog is always interactive.This Tool is primarily intended as a developer tool. Using it on arbitrary existing app-bundles may cause that application to malfunction, depending on what the application is expecting from its own app-bundle. Do not use this tool on applications unless you know they can survive it.
The basic purpose of AppBundleLockdown is to prevent unauthorized changes to an app-bundle, making it tamper-resistant to all but the root user. It does this by removing all write-permissions and setuid-bits on every file and directory within the app-bundle, and then changing the ownership to root. The rationale behind this is explained elsewhere.
AppBundleLockdown does not make a program more trustworthy than it already is. It only prevents a known program from being modified by anyone other than root (who is presumed to be trustworthy). Only you can determine whether a program is initially trustworthy -- worthy of being trusted.
Locking down an app-bundle with AppBundleLockdown does not give the target application root privileges, nor cause it to run as root when executed. It only makes it more tamper-resistant, at best.
Because AppBundleLockdown can act both interactively and non-interactively, presenting an authentication dialog in either case, it is useful in both command-line and interactive environments:
- Launched from a command-line, or in a build or install script, you typically provide it with the exact pathname of the app-bundle(s) to lock down, and only examine the exit-code. AppBundleLockdown then performs the minimal user interaction in an authentication dialog, and returns its results solely on stderr and in its exit-code. For detailed results, you can tell it to produce additional information on stdout, which you can pipe, log, filter, parse, etc.
- Launched by double-clicking the JAR in the Finder, there will be no args, no predefined properties, and the main class will be AppBundleLockdown. The program then behaves as a one-shot GUI program, which terminates after one app-bundle has been locked-down. Its stderr diagnostics appear on the system console, i.e. /Applications/Utilities/Console.app.
- Launched with a double-click in an app-bundle, you typically omit all args and predefine the "alert" property to true, or let it be presupposed as true. AppBundleLockdown then behaves the same as if the JAR is double-clicked. You have more flexibility with an app-bundle than with a simple double-clickable JAR. For example, you can preset other properties, or control other aspects of the JVM.
If the AuthKit's JAR and JNI-lib files are placed into an app-bundle with a suitable "Info.plist", you can create a double-clickable Java app that runs AppBundleLockdown. You should then run it on itself, to prevent any subsequent changes to the app-bundle that might compromise its trustworthiness. To prevent it from being copied, you may first wish to turn off read-permissions in certain parts of the app-bundle, as explained elsewhere. You should only do this on a final installation, or on a distributable disk-image, because the locked-down read-denied app-bundle will be uncopiable except by root.
TOOL DETAILS
AppBundleLockdown always presents an interactive GUI authentication dialog, unless you're already acting as root. This dialog is presented before lockdown occurs. The program may also present an interactive GUI FileDialog to choose an app-bundle, depending on whether it's run with or without command-line args.When AppBundleLockdown receives command-line args, it does not present a FileDialog. It treats the args as pathnames of app-bundles to be locked down. Args are resolved to their canonical pathnames, so symlinks are resolved. The program accepts any number of target pathnames, and locks down each one in turn. If one lockdown fails, then no subsequent app-bundles are locked down. However, all app-bundles processed up to the point of failure will be correctly locked down.
When AppBundleLockdown does not receive any command-line args, it will present a FileDialog so the user can choose one app-bundle to lock down. First, however, the user is authenticated with the GUI authentication dialog. If the user can't authenticate as an admin user, then no FileDialog is presented and no privileged process runs. If the user authenticates but then cancels the FileDialog, no privileged process runs. If the user authenticates but then chooses something other than an app-bundle in the FileDialog, no privileged process runs. If you provide any args, even ones that are invalid app-bundles, no FileDialog is ever presented.
After AppBundleLockdown has at least one pathname to act on, it validates each pathname before running the privileged Envoy process. This ensures each pathname really refers to an app-bundle, and is accessible to the user running the unprivileged program. Users cannot lock down something they lack permission to access. Invalid or inaccessible pathnames abort the program without ever running a privileged process, and without locking down any app-bundles that may be valid.
With all its args validated, AppBundleLockdown builds the Java command that will run the privileged Envoy process, passing it the app-bundle pathnames, various system properties, etc. It then starts the Envoy process, which is a Java program, and waits for it to return results and terminate. The Envoy's communications are briefly analyzed to extract only its termination status, and AppBundleLockdown then decides how to present the outcome.
The unprivileged AppBundleLockdown process always echoes the stderr stream of the privileged Envoy process to its own stderr output. Thus, Tasks which emit diagnostics to stderr will have their diagnostic output appear on the common stderr stream.
If the "verbose" property is true, then AppBundleLockdown echoes the output it receives from the privileged Envoy process onto its own stdout stream. This is useful for debugging or further parsing of Envoy results. See the Envoy class's API docs and its source code for info on the Envoy protocol.
If the "alert" property is false, then AppBundleLockdown only emits a message to its stderr stream and returns an exit code: 0 for success, non-0 for failure. If the "alert" property is true, then AppBundleLockdown ends by presenting a single modal alert summarizing its overall success or failure. Dismissing the alert exits the program. If the "alert" property is undefined, then AppBundleLockdown will presuppose it to be true if a FileDialog was presented, but presuppose it to be false otherwise.
USAGE EXAMPLES
- java -jar AuthKit.jar /path/to/Foo.app
- This is a typical non-interactive use, with the app-bundle to lock down given as a pathname (including the ".app" suffix). The main class is designated in the JAR's manifest as app.authkit.tools.AppBundleLockdown. The authentication dialog will be graphical and interactive, but the results are returned on stderr and as a success/fail exit-code from the process. This invocation is typical for build-scripts, install-scripts, etc. Script execution will block indefinitely for interactive authentication. Since the authentication dialog is always interactive, "non-interactive" really means "minimally interactive".
- java -cp AuthKit.jar app.authkit.tools.AppBundleLockdown /path/to/Foo.app
- This is the same as the previous example, but does not use the "-jar" option to designate the main class.
- java -jar AuthKit.jar
- This is a typical interactive use, with no app-bundle specified and the "alert" property presupposed to be "true" because of the resulting FileDialog interaction. The authentication dialog will appear first, followed by a FileDialog to choose the app-bundle to lock down. Results are displayed in an alert, and are also sent to stderr and returned as the process's exit-code. This invocation can be used in scripts if the target app-bundle is unspecified. You can control the appearance of alerts independently of the FileDialog, if you preset "alert" to "true" or "false".
- java -Dverbose=true -jar AuthKit.jar /path/to/Foo.app
- This is a non-interactive invocation, where the output from the privileged Envoy process will be sent to stdout. You can redirect it, pipe it, log it, parse it, etc. See the Envoy class's API docs for details on the Envoy protocol. The authentication dialog is always interactive, so "non-interactive" really means "minimally interactive".
This Tool is a very simple command-line program. It is similar in principle to the 'sudo' command, which executes other commands as root. The PerformAsRoot tool authenticates the user, then runs Tasks as root in a privileged Envoy process. THIS GENERALITY MAKES IT DANGEROUS.TOOL DETAILS
PerformAsRoot takes a sequence of arbitrary task-names and task-args on its command-line. Tasks are separated from one another by a preceding ":-", which Envoy uses to break the args up into groups. Each Task is identified by a fully-qualified class name, following the ":-" and separated from it by white-space. The Envoy makes an instance of that class, passes it the appropriate group of task-args, then returns the result. If a Task fails (returns non-zero), then Envoy stops executing tasks and exits.The unprivileged PerformAsRoot process has almost nothing to do. It creates an Authorization, builds an Envoy command-line from its own args, executes the privileged process to run Envoy, and waits for the results. Authentication occurs when execPrivileged() is called, which is how the Envoy process is launched. Failure to authenticate will abort the Envoy process launch, and PerformAsRoot will exit with a non-zero failure code. PerformAsRoot will also exit with a non-zero result when the Envoy process or one its Tasks returns a non-zero completion status.
There are almost no safeguards in PerformAsRoot. You can pass it the name of a Task that may delete or overwrites any file anywhere on the system. You can tell a Task to run any available command, including ones that move or overwrite critical system files, recursively delete or change files, or cause countless other kinds of indiscriminate havoc. You should always use PerformAsRoot very carefully, and be absolutely sure that what you tell it to do is really what you want done. As with sudo, there are no safeguards against clumsiness or ignorance.
USAGE EXAMPLES
java -cp AuthKit.jar app.authkit.tools.PerformAsRoot \ :- app.authkit.tools.tasks.RunOut \ /bin/chmod -RP u-s,a-w /Users/me/apps/Trial.app \ :- app.authkit.tools.tasks.RunOut \ /usr/sbin/chown -RP root /Users/me/apps/Trial.app- This imitates some of what AppBundleLockdown does by executing the same commands it uses: chmod and chown. You won't get the app-bundle validation, the FileDialog interaction, the ending alert, or the deletion of .DS_Store files. It performs the essence of the lockdown, though.
- Each taskname is preceded by a ":-" marker. This is how Envoy identifies each taskname and thus separates task groupings from one another.
- The Task is RunOut, which feeds the stdout from the command to the waiting unprivileged process. The RunEat task would also work here, because neither command produces any output normally.
- The \ ending each line tells the shell to continue input on a subsequent line. This is for clarity only; you could type it all on one line if you wanted to.
java -cp AuthKit.jar app.authkit.tools.PerformAsRoot \ app.authkit.tools.tasks.RunOut \ /bin/chmod a+w /Volumes/Disk-Image/.- When you create a new UFS disk-image with Disk Copy, the file-system is owned by root and its permissions allow writing only by root. For a disk-image, this is silly. You typically want a disk-image to be writable to anyone who mounts it. You can change the permissions in Finder 10.2+ with Get Info, but not in Finder 10.1. If you know the pathname of the mounted disk-image, you can run 'chmod' as root to expand the write-permissions:
- Because there's only one Task, you can omit the ":-" marker before the taskname.
- As before, the Task is RunOut, though RunEat would also work here.
- The mounted disk-image's pathname is given with a trailing "/." so the root directory of the mounted volume has its permissions changed, not the mount-point in /Volumes.
- As a practical matter, it may be easier to use Terminal to run 'sudo chmod'. However, if you wanted to follow the example of AppBundleLockdown, you could write a Tool that accepted drag-n-drop disk-image volumes, validated the pathname, presented a GUI, authenticated the user, then ran the RunOut (or RunEat) Task to perform the actual permission change. You wouldn't have to create a special Task for this, because RunOut (or RunEat) is adequate for running a single command as root.
To Greg's Home Page
To Greg's Software Page