Authorization Toolkit for Java -- Tests and Examples

Last revised: 10Aug2003 GLG

Table of Contents

Introduction

All the test and example classes used below are command-line oriented. For a semi-GUI example, see AppBundleLockdown.

Although the tests themselves are not GUI-oriented, the Mac OS X authentication dialog is a fully graphical dialog with text-fields and buttons. It floats above all windows of all applications, though it doesn't necessarily prevent you from interacting with other windows. Since the authentication dialog is managed by the system, not by Java, it can do this even though there is no way to replicate this behavior in Java itself.

All the test classes are found in this package:
   app.authkit.test

Some of the tests are specialized and single-purpose. A few of the test classes are very generalized, performing an arbitrary series of actions given by command-line parameters.

Each example below lists the full command-line, often as several continuation lines for clarity. Each command-line is followed by sample output. The sample output was produced on my Mac OS X machine. Some things will differ on your machine, but the general results should be the same.

Secret identifiers may be displayed in the output, but since these are transitory and confined to my test machine, they have no long-term need for secrecy. Knowing the secret identifier of a session after the session is destroyed is not a security hole.

Public identifiers are also liberally displayed, so you can identify the session and watch its progression over time, or even from process to process. Since neither secret nor public identifiers are ever the same twice, the actual values you see in your runs will be different. The same applies for any timestamps displayed.

Since the only real concrete implementation is for Mac OS X, 10.1+, that's the required platform for the examples as given. The tests were run under both J2SE 1.4.1 and 1.3.1 with similar results, so you don't need 1.4.1 installed in order to run these examples or to use the AuthKit in your own programes.

One of the examples performs tests under different JVMs (1.3.1 and 1.4.1) in order to illustrate a point. If you don't have 1.4.1 installed, the example will still work, but you'll see slightly different results.

The app.authkit.test.TestAuth Class

TestAuth is the Swiss Army Knife test for Authorization methods. It can call every method of Authorization in one way or another. It does this by acting as a simple command interpreter over its command-line args. Some of its commands are "builtins" that call the Authorization methods. Everything else is an external command, which is run with elevated privileges using the execPrivileged() method.

The command-line args form an interpretable sequence of strings consisting of:

  1. builtin commands, with args
  2. external commands, with args
  3. separators between groupings of command+args

The command parser is very simple, and works with the individual Strings of the String[] received by main(). Those Strings are not subdivided or otherwise parsed or manipulated. The interpreter simply breaks the sequence into groups, examines the first String of the group, and acts accordingly. For example, the grouping separator is ":", but it's only recognized in isolation. The interpreter will not recognize embedded :'s, nor will it subdivide commands or args at :'s.

A command grouping is separated from a subsequent grouping by a single isolated ":". Within a grouping, the first String is the command, and all other Strings are args for that command. A grouping may be empty, which is quietly ignored.

Builtins were named to minimize collisions with external commands on Mac OS X. The builtin commands and their args are:

see [privilege-name ...]
Display the current availability of each privilege-name, by calling isAvailable(). If no privilege-names are given, then list identity, latest date granted, and current availability of the getPastGrantedPrivileges() enumeration.
pre privilege-name [...]
Call preauthorize() for the named privilege(s). Interaction is always allowed. To preauthorize the root-execution privilege, use a name of "system.privilege.admin" on Mac OS X.
au+ privilege-name [...]
Call authorize() for the named privilege(s), with interaction allowed.
au- privilege-name [...]
Call authorize() for the named privilege(s), with interaction disallowed.
put [pathname ...]
Write the secret identifier in binary to each given pathname, overwriting any file of that name. If no pathnames are given, the secret identifier is dumped to stdout in hex.
THIS APPROACH IS INSECURE, AND IS ONLY SUITABLE FOR TESTING.
get [pathname]
If pathname is ".", call attachPrivileged() on a new Authorization, using it if successful. If pathname is anything other than ".", read the secret identifier from the file, call attach() on a new Authorization, and use it if successful. The secret identifier in a file must refer to a still-active session, or it will fail. The "." notation will only work if TestAuth is running in a JVM that was executed as a child of execPrivileged(). Also see the TestAttach class.
#
Call release() on the current Authorization.
##
Call detach( true ) on the current Authorization.
ver
Print the values of the "java.version" and "java.home" system properties, as a diagnostic aid.. This calls no Authorization method.
zzz [seconds]
Sleep for the given integer number of seconds, or for 3 secs if no value given. This calls no Authorization method. It only uses Java code, and does not execute an external command.

Any command not recognized as a builtin is interpreted as an external command, and will be executed using execPrivileged(). Some pre-processing may occur first:

JAVA ...
The all-upper-case "JAVA" command is a quasi-builtin, a little like a shell command-alias. It eventually executes an external command, but some substitution happens first. It takes the "java.home" property, appends "/bin/java" to it, and then executes that as an external command using execPrivileged().

The reason for doing this is that "java.home" will reflect the JVM currently running, which may be 1.3.1 or 1.4.1 on Mac OS X, and execute that same JVM in another process. If you just entered the command "/usr/bin/java", it won't necessarily be the current JVM.

You can customize which command in "java.home" is executed by defining it as a relative pathname in the "auth.bin.java" property. Be sure to use a file-separator appropriate to your platform. The value should always be a partial pathname relative to the location given by "java.home".

@ command [...]
When "@" appears as a separated token before an external command, then that command will have the secret identifier piped to it on its stdin stream. The "@" must be separated from the following command by white-space. Any external command can be run, including the JAVA quasi-builtin. Only commands that read their standard input will be useful. For example, the 'echo' command will be useless at reading the secret.

All external commands are executed using execPrivileged(), so they all run with elevated privileges. This is unwise as a general approach, but is acceptable for a test.

If authorization is needed before running an external command, it is obtained interactively if necessary. Failure to authorize or authenticate will fail to run the external command.

On Mac OS X, all commands executed by execPrivileged() must be absolute pathnames. If you don't know the absolute pathname of a command, use the 'which' command in your shell to tell you the pathname of a particular command. For example, 'which java' or 'which id'.

The external command's stdout is piped to the System.out stream, and the bytes are counted and CRC'ed for comparison purposes. On Mac OS X, the stderr is simply shared with the privileged process.

If the Authorization's privileged Process implements waitFor(), the external command is waited for. Otherwise a 3-second delay occurs, then the interpreter resumes parsing and executing commands. Since MacOSXAuthorization's privileged Process doesn't provide waitFor(), long-running external commands may need a subsequent 'zzz' builtin command.

Before each command is executed, whether builtin or external, the Authorization's current public identifier is displayed on stdout. This is done to help track when sessions are created, attached, and detached. It's also useful to compare to public identifiers displayed by other processes that attach to the same session. The identifiers should be identical, even though from different processes.

Several system properties affect the TestAuth command interpreter:

"authkit.imp"
This property's value is the fully qualified class name of an Authorization to instantiate and use. See method main().
"between"
This optional property defines an override for the ":" string separating command groupings. The main use for this property is so you can execute a privileged version of this class in another process, and change the separator. If you couldn't change the separator, you couldn't distinguish its commands from those of its parent. That's because the TestAuth interpreter doesn't do escaping. Usually, you don't have to set "between" or change it in any way.
"auth.bin.java"
This optional property defines an override for the "bin/java" command assembled by the quasi-builtin JAVA command. The main use for this property is when the external command isn't "bin/java", such as on Windows. Usually, you don't have to set "auth.bin.java" or change it in any way. See method doExec().

IMPORTANT WARNINGS

On Mac OS X, you will truly be running commands as root. If you mistype something, you can seriously damage or break things.

I didn't type any of these examples directly into a Terminal window. I wrote them all in a TextEdit file, then dragged them or copied and pasted them into a Terminal window. I recommend you do something similar. I've even provided a text-file containing these examples, already typed in. The commands are appropriate for Mac OS X, and may differ for other platforms.

Most of these examples did not come into existence fully formed. They evolved. That is, I started small and worked my way up by carefully adding lines, tweaking args, etc. And I made mistakes, too. But I was always careful that if I did make a mistake, nothing important was ever at risk.

In short, don't experiment on anything you can't afford to lose.

Example 0: Preliminaries

The following examples use shell command-aliases and presume the existence of some directories. This example contains the shell commands to set these up.

Commands for csh or tcsh:

alias jauthTen 'java -Dauthkit.imp=glguerin.authkit.imp.macosx.MacOSXAuthorization'
alias  java131 '/System/Library/Frameworks/JavaVM.framework/Versions/1.3.1/Commands/java'
cd your-test-directory-here
mkdir -p ./s

Commands for bash or sh:

alias jauthTen='java -Dauthkit.imp=glguerin.authkit.imp.macosx.MacOSXAuthorization'
alias  java131='/System/Library/Frameworks/JavaVM.framework/Versions/1.3.1/Commands/java'
cd your-test-directory-here
mkdir -p ./s

Discussion:

The first two commands create shell command-aliases that we'll use in the subsequent examples. These aliases are mainly so the command-lines shown in the examples are shorter and clearer.

The 'cd' command should change the working directory to wherever you have placed the AuthKit.jar file. It must be a writable directory.

The 'mkdir' command makes a directory named 's' with default privileges and ownership. It's used later to hold files created by some of the examples. Using sub-directories makes it easier to delete all the transient test-files at once.

Example 1: External Command Run as Root

This example is one of the simplest possible. We use TestAuth to execute an external command as root, and then display the resulting past-granted Privileges.

Command-line:

jauthTen -cp AuthKit.jar app.authkit.test.TestAuth \
  : /usr/bin/id \
  : see 

Output:

TestAuth.main(): starting...
Authorization: class glguerin.authkit.imp.macosx.MacOSXAuthorization
-----with: MD5:E8DFBE24774DD5BF8DF9FD24D777E8EF (/usr/bin/id)
Executing: /usr/bin/id
  process: /usr/bin/id
    class: glguerin.authkit.imp.macosx.AuthProcess
  sleeping a few seconds...
uid=501(admin) euid=0(root) gid=20(staff) groups=20(staff), 0(wheel), 80(admin)
....count: 80
....check: 0xA0789106 -- CRC32
-----with: MD5:E8DFBE24774DD5BF8DF9FD24D777E8EF (see)
 Past Granted... 
Privilege: system.privilege.admin:/usr/bin/id:0 at Sun Jun 29 15:37:46 MST 2003 -- true

TestAuth.main(): done...

Discussion:

When you run the command-line, the Mac OS X system authentication dialog will appear. You should click it to bring to into focus, then enter the name and password of an admin user. If you cancel or fail to authenticate, no command will be run, and TestAuth will move to the builtin 'see' command, which needs no authentication.

In the output, the concrete Authorization class is seen to be MacOSXAuthorization. This implementation was defined with the "authkit.imp" property, using the jauthTen command-alias shown in Example 0.

The public identifier is always displayed as each external or builtin command is performed. It can be seen to be the same identifier throughout.

The command-string appears after the public identifier, enclosed in parentheses. In the first case it's /usr/bin/id, which is an external command. In the second case it's see, which is a builtin that lists either specific Privileges, or, as here, lists the past granted Privileges.

The output from stdout of /usr/bin/id shows the results on my machine. My real user-ID is 501, which is the uid for the user named 'admin' on my machine. Your machine's output will probably be different, though it may show the same 501 ID. The effective user-ID is 0, confirming that the process is truly running as root. The groups are simply what I have as my 'admin' user's groups.

After the output from /usr/bin/id, we see that 80 bytes were emitted on stdout, and the CRC32 checksum was calculated as 0xA0789106.

The second command is 'see', with no other parameters, so it displays the list of past granted Privileges. Since a MacOSXAuthorization will add a Privilege to the past granted set when execPrivileged() succeeds, we can see exactly what was granted. The Privilege name is "system.privilege.admin", which is the correct name for Mac OS X. The value bound to the Privilege is the command-name: /usr/bin/id. The flags bound to the Privilege are 0. The last-granted date of the Privilege then appears, followed by its current availability (true). Since we just executed an external command that required authentication, we still have the Privilege available, so executing a second external command would not need a second authentication. That is, another command would be authorized without the authentication dialog appearing again.

Example 2: External Command With Preauthorization

This example is similar to Example 1, but it preauthorizes the Privilege first, and then delays a little while before executing the external command. As a result, the TestAuth interpreter will stop for authentication at a different point, and we'll see a different set of past granted Privileges.

Command-line:

jauthTen -cp AuthKit.jar app.authkit.test.TestAuth \
  : pre system.privilege.admin \
  : zzz \
  : /usr/bin/id \
  : see 

Output:

TestAuth.main(): starting...
Authorization: class glguerin.authkit.imp.macosx.MacOSXAuthorization
-----with: MD5:640061B9A9C8392930B4829DDE5D3B7C (pre)
Privilege: system.privilege.admin::0
  preauth: system.privilege.admin::0 at Sun Jun 29 17:46:10 MST 2003 -- true
-----with: MD5:640061B9A9C8392930B4829DDE5D3B7C (zzz)
 sleeping: 3
-----with: MD5:640061B9A9C8392930B4829DDE5D3B7C (/usr/bin/id)
Executing: /usr/bin/id
  process: /usr/bin/id
    class: glguerin.authkit.imp.macosx.AuthProcess
  sleeping a few seconds...
uid=501(admin) euid=0(root) gid=20(staff) groups=20(staff), 0(wheel), 80(admin)
....count: 80
....check: 0xA0789106 -- CRC32
-----with: MD5:640061B9A9C8392930B4829DDE5D3B7C (see)
 Past Granted... 
Privilege: system.privilege.admin::0 at Sun Jun 29 17:46:10 MST 2003 -- true
Privilege: system.privilege.admin:/usr/bin/id:0 at Sun Jun 29 17:46:13 MST 2003 -- true

TestAuth.main(): done...

Discussion:

When you run the command-line, the Mac OS X system authentication dialog will appear. Look carefully at the Terminal window and you'll notice it's stopped with this line showing:
Privilege: system.privilege.admin::0
Click the authentication dialog, enter admin name and password, and proceed. There will be a delay of about 3 seconds (the 'zzz' builtin) before the /usr/bin/id command executes. Notice that you don't have to re-authenticate when /usr/bin/id runs. This shows the preauthorization is still in effect.

Notice that the initial preauthorization did not specify a command, yet the authentication still held for the subsequent execution of /usr/bin/id. This is because the Mac OS X policy rule for the "system.privilege.admin" Privilege does not actually discriminate for each command, and the credential granted remains active for 5 minutes or until the session is destroyed. We'll see an example later that destroys the session, where you will have to authenticate twice.

Some other things to notice in this output:

Example 3: External Commands, Session Released

This example shows that a Privilege granted in one session does not necessarily apply to any other session, nor does it survive indefinitely. We'll execute the /usr/bin/id command again, but we'll release the session between the two executions. Since the root-execution privilege is unshared under the Mac OS X policy rules, you'll have to authenticate each execution separately..

Command-line:

jauthTen -cp AuthKit.jar app.authkit.test.TestAuth \
  : /usr/bin/id \
  : see \
  : # \
  : /usr/bin/id \
  : see

Output:

TestAuth.main(): starting...
Authorization: class glguerin.authkit.imp.macosx.MacOSXAuthorization
-----with: MD5:FABC7D488C21730D8FBC002BBAEA1879 (/usr/bin/id)
Executing: /usr/bin/id
  process: /usr/bin/id
    class: glguerin.authkit.imp.macosx.AuthProcess
  sleeping a few seconds...
uid=501(admin) euid=0(root) gid=20(staff) groups=20(staff), 0(wheel), 80(admin)
....count: 80
....check: 0xA0789106 -- CRC32
-----with: MD5:FABC7D488C21730D8FBC002BBAEA1879 (see)
 Past Granted... 
Privilege: system.privilege.admin:/usr/bin/id:0 at Sun Jun 29 18:03:59 MST 2003 -- true
-----with: MD5:FABC7D488C21730D8FBC002BBAEA1879 (#)
  Release: MD5:FABC7D488C21730D8FBC002BBAEA1879
-----with: MD5:57472F730E49FD8665184B4973674CCA (/usr/bin/id)
Executing: /usr/bin/id
  process: /usr/bin/id
    class: glguerin.authkit.imp.macosx.AuthProcess
  sleeping a few seconds...
uid=501(admin) euid=0(root) gid=20(staff) groups=20(staff), 0(wheel), 80(admin)
....count: 80
....check: 0xA0789106 -- CRC32
-----with: MD5:57472F730E49FD8665184B4973674CCA (see)
 Past Granted... 
Privilege: system.privilege.admin:/usr/bin/id:0 at Sun Jun 29 18:04:06 MST 2003 -- true

TestAuth.main(): done...

Discussion:

When you run the command-line, the Mac OS X system authentication dialog will appear. Enter an admin name and password, then proceed. The command will execute, then the list of past granted Privileges is displayed, followed by the '#' builtin releasing the session. When the second external command is run, a new session must be created, so a new authentication dialog must be presented.

Notice that the public identifer changes after the '#' command performs the release. This shows that a single instance of Authorization can be reused after release() or detach(). It also shows that a completely new session will be created as necessary, and it will not share any previously granted Privileges with the prior session.

If the root-execution policy rule stated that the credential should be shared, then it would be available to any new session for the logged-in user until the credential expired (by default, after 5 minutes). But the root-execution policy rule DOES NOT share the granted credential, so unless a process attaches to the exact same session in which the credential was granted, it will not be available to any other process, even for the same logged-in user account. A later example illustrates attaching two processes to the same session.

In fact, we can see in this example that an unshared credential isn't even granted to different sessions in the same process and the same Authorization instance. This further illustrates that sessions are distinct entities, and unless two agents somehow cooperate to share the Authorization object or the session itself, granted Privileges are managed as completely separate credentials.

Example 4: Default-Rule Shared Privileges

Every Privilege has a name, but not every name has its own policy rule. This example shows how Mac OS X's default policy rule will match to any Privilege-name that doesn't have its own entry in the policy database. Since the default policy rule states that credentials are shared and expire after 5 minutes, we'll also see how that affects the availability of default-rule Privileges.

This example is made entirely of builtin commands. This does not imply that shared credentials apply only to a single process. The discussion below describes how you can demonstrate the longevity of shared credentials across processes.

Command-line:

jauthTen -cp AuthKit.jar app.authkit.test.TestAuth \
  : see TRIAL \
  : au+ TRIAL \
  : see TRIAL \
  : # \
  : zzz \
  : au+ TRIAL \
  : see TRIAL

Output:

TestAuth.main(): starting...
Authorization: class glguerin.authkit.imp.macosx.MacOSXAuthorization
-----with: MD5:2B81FA469BABFA3BA6E2392FEDFE04F1 (see)
Privilege: TRIAL::0 -- false
-----with: MD5:2B81FA469BABFA3BA6E2392FEDFE04F1 (au+)
Privilege: TRIAL::0
     auth: TRIAL::0 at Sun Jun 29 18:34:22 MST 2003 -- true
-----with: MD5:2B81FA469BABFA3BA6E2392FEDFE04F1 (see)
Privilege: TRIAL::0 at Sun Jun 29 18:34:22 MST 2003 -- true
-----with: MD5:2B81FA469BABFA3BA6E2392FEDFE04F1 (#)
  Release: MD5:2B81FA469BABFA3BA6E2392FEDFE04F1
-----with: MD5:15B33FA68D099FE07823B2DE8A229F11 (zzz)
 sleeping: 3
-----with: MD5:15B33FA68D099FE07823B2DE8A229F11 (au+)
Privilege: TRIAL::0
     auth: TRIAL::0 at Sun Jun 29 18:34:26 MST 2003 -- true
-----with: MD5:15B33FA68D099FE07823B2DE8A229F11 (see)
Privilege: TRIAL::0 at Sun Jun 29 18:34:26 MST 2003 -- true

TestAuth.main(): done...

Discussion:

When you run the command-line, the Mac OS X system authentication dialog will appear. Enter an admin user name and password as usual. The output will delay 3 seconds due to the 'zzz' builtin

Notice that you don't authenticate more than once, even though the session is released and the TRIAL Privilege is again authorized with interaction allowd. This shows that the default-rule's "shared" attribute is preserving the credential initially granted for the TRIAL Privilege.

In fact, if you run this example twice in a row, the shared TRIAL credential will still be available, and you won't authenticate at all for any number of subsequent runs, until the credential expires after 5 minutes.

But how can you destroy or revoke a shared credential before it expires? We'll see how in the next example.

Example 5: Revoking Shared Privileges

The Authorization class doesn't have a revoke() method, so how can you revoke a shared credential granted in a session? This example shows how.

Command-line:

jauthTen -cp AuthKit.jar app.authkit.test.TestAuth \
  : see TRIAL \
  : au- TRIAL \
  : ## \
  : see TRIAL

Output:

TestAuth.main(): starting...
Authorization: class glguerin.authkit.imp.macosx.MacOSXAuthorization
-----with: MD5:0D49A30F57E4C7001D25568805CD08F4 (see)
Privilege: TRIAL::0 -- true
-----with: MD5:0D49A30F57E4C7001D25568805CD08F4 (au-)
Privilege: TRIAL::0
     auth: TRIAL::0 at Sun Jun 29 18:54:07 MST 2003 -- true
-----with: MD5:0D49A30F57E4C7001D25568805CD08F4 (##)
  Destroy: MD5:0D49A30F57E4C7001D25568805CD08F4
-----with: MD5:A4F7667DEF4395CD490B58648BB675A0 (see)
Privilege: TRIAL::0 -- false

TestAuth.main(): done...

Discussion:

When you run the command-line, no authentication dialog appears. This happens because we're authorizing with interaction disallowed, and gives us the answer to the question posed above:
 : : : A shared credential can be revoked by calling authorize() on it without interaction, ignoring any exceptions, then calling detach(true) on the session. If more than one shared credential needs to be revoked, you can authorize() them all without interaction, then detach(true) just once.

Calling authorize() effectively binds a granted Privilege's credential to the session, so the ultimate fate of those credentials is bound to the fate of the session. Of course, if a Privilege fails to authorize(), then no credential for it is bound to the session, and nothing happens for that privilege at detach(). Furthermore, since sessions are distinct entities, an unshared Privilege granted in one session won't be affected by anything that happens in another session.

If you run this example within 5 minutes of Example 4, you'll see the output above. The '##' builtin calls detach(true), so any shared credentials that were authorized in the session will be revoked. If you used the '#' builtin instead, then release() is called, and only the unshared credentials are revoked, while the shared ones survive. That's what happened in Example 4.

The 'see' builtin after the '##' clearly shows that the TRIAL Privilege is not available. We also note that a new session public identifier appears, so if the TRIAL credential was still around to be shared, we would expect to see it available. It isn't.

If you run this example a second time, or if you run it more than 5 minutes after Example 4, the output looks like this:

TestAuth.main(): starting...
Authorization: class glguerin.authkit.imp.macosx.MacOSXAuthorization
-----with: MD5:88F4251A718CC1F6F005D21A07F9F8BE (see)
Privilege: TRIAL::0 -- false
-----with: MD5:88F4251A718CC1F6F005D21A07F9F8BE (au-)
Privilege: TRIAL::0
glguerin.authkit.UnauthorizedException: Authorization denied: -60007
        at glguerin.authkit.imp.macosx.MacOSXAuthorization.check(MacOSXAuthorization.java:377)
        at glguerin.authkit.imp.macosx.MacOSXAuthorization.authorize(MacOSXAuthorization.java:288)
        at app.authkit.test.TestAuth.doAuth(TestAuth.java:465)
        at app.authkit.test.TestAuth.decodeActions(TestAuth.java:364)
        at app.authkit.test.TestAuth.testAuth(TestAuth.java:275)
        at app.authkit.test.TestAuth.main(TestAuth.java:241)
-----with: MD5:88F4251A718CC1F6F005D21A07F9F8BE (##)
  Destroy: MD5:88F4251A718CC1F6F005D21A07F9F8BE
-----with: MD5:3EDF5B22960C7F52C71AD41AE2F57194 (see)
Privilege: TRIAL::0 -- false

TestAuth.main(): done...
Notice that TRIAL is initially unavailable, so we would expect a non-interactive authorization to fail. It handily obliges.

If you were doing this in your own program, you might use the initial absence of TRIAL's credential to decide that authorize() should have interaction allowed. Or perhaps to decide that preauthorize() should be called first, followed by a non-interactive authorize() in a separate Thread. So we see that isAvailable(), which is what 'see privName' is based on, can be used in simple ways to guide how authorize() or preauthorize() are used.

Although an UnauthorizedException was thrown, it's not fatal to the TestAuth class, which happily moves on to the next commands -- the builtins '##' and 'see'. This is an acceptable strategy for a test, but you might consider something more appropriate for your own program. Whatever you do, make sure that all the actions following an UnauthorizedException are appropriate to the denial of the authorization. It would be quite foolish if the actions you were trying to restrict ended up occurring even when an UnauthorizedException was thrown.

Note that you can't fool execPrivileged() into running a command as root unless you truly have the root-execution Privilege. It always performs its own internal checks, effectively calling authorize() each time. You can't even fool it by changing the JNI native code. The check is done inside the OS-provided API function, not in the JNI code nor in the Java code. This is one pivotal advantage to using system-enforced restrictions.

Given all the above, doesn't it make more sense why the root-execution Privilege doesn't have a shared credential?

Example 6: Which Java Am I?

This example illustrates the possible differences between the external command /usr/bin/java and the 'JAVA' quasi-builtin.

Command-line:

java131 -Dauthkit.imp=glguerin.authkit.imp.macosx.MacOSXAuthorization \
    -cp AuthKit.jar app.authkit.test.TestAuth \
  : /usr/bin/java -version \
  : JAVA -version

Output:

TestAuth.main(): starting...
Authorization: class glguerin.authkit.imp.macosx.MacOSXAuthorization
-----with: MD5:8F3EB57011D5C21C06960E817AB1BF7D (/usr/bin/java)
Executing: /usr/bin/java
  process: /usr/bin/java
    class: glguerin.authkit.imp.macosx.AuthProcess
  sleeping a few seconds...
java version "1.4.1_01"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.4.1_01-39)
Java HotSpot(TM) Client VM (build 1.4.1_01-14, mixed mode)
....count: 0
....check: 0x00000000 -- CRC32
-----with: MD5:8F3EB57011D5C21C06960E817AB1BF7D (JAVA)
Executing: /System/Library/Frameworks/JavaVM.framework/Versions/1.3.1/Home/bin/java
  process: /System/Library/Frameworks/JavaVM.framework/Versions/1.3.1/Home/bin/java
    class: glguerin.authkit.imp.macosx.AuthProcess
  sleeping a few seconds...
java version "1.3.1"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.3.1-root_1.3.1_021023-23:01)
Java HotSpot(TM) Client VM (build 1.3.1_03-69, mixed mode)
....count: 0
....check: 0x00000000 -- CRC32

TestAuth.main(): done...

Discussion:

When you run the command-line, the Mac OS X system authentication dialog will appear. Enter an admin name and password, then proceed.

If you've installed the Java 1.4.1 upgrade, then you'll see output like above. If you haven't installed the 1.4.1 upgrade, then your output will show the same version of Java for both commands.

This example illustrates that the command /usr/bin/java isn't necessarily the same Java as what a Java program is running under. This is especially applicable to Java apps in app-bundles, because without a specific Info.plist key designating the 1.4.1 JVM, they will run under 1.3.1 by default. This is true even after the 1.4.1 upgrade. In effect, they Java app-bundles run under isn't necessarily the /usr/bin/java command, so Java programs run via exexPrivileged() better be very careful what they ask for.

The source for TestAuth.doExec() has the code that implements the 'JAVA' quasi-builtin. You can copy and paste it, modified or not, into your own programs that use the Authorization Toolkit for Java. Or you can write your own strategy for dealing with the possibility of multiple JVMs installed on Mac OS X.

You should also take into account that the pathnames for Java commands, and the structure in JRE or JDK installs varies across platforms, and even across different releases of Java on the same platform.

Whatever you do, and however you handle it, you need to be aware that /usr/bin/java may not be what's running your Java program.

Example 7: More External Commands as root

This example makes a directory, changes its owner to root, and makes it writable only by root. Subsequent examples will exercise their root powers by creating and writing files in this directory. The directory is readable by anyone, so you can see what it holds after running the other examples.

Command-line:

jauthTen -cp AuthKit.jar app.authkit.test.TestAuth \
  : au+ system.privilege.admin \
  : see \
  : /bin/mkdir -p ./rooted \
  : /bin/chmod 755 ./rooted \
  : /usr/sbin/chown 0:0 ./rooted \
  : /bin/ls -ld ./rooted

Output:

TestAuth.main(): starting...
Authorization: class glguerin.authkit.imp.macosx.MacOSXAuthorization
-----with: MD5:96797C1D3F16CF50A86697AD48D4DEB8 (au+)
Privilege: system.privilege.admin::0
     auth: system.privilege.admin::0 at Mon Jun 30 15:36:36 MST 2003 -- true
-----with: MD5:96797C1D3F16CF50A86697AD48D4DEB8 (see)
 Past Granted... 
Privilege: system.privilege.admin::0 at Mon Jun 30 15:36:36 MST 2003 -- true
-----with: MD5:96797C1D3F16CF50A86697AD48D4DEB8 (/bin/mkdir)
Executing: /bin/mkdir
  process: /bin/mkdir
    class: glguerin.authkit.imp.macosx.AuthProcess
....count: 0
....check: 0x00000000 -- CRC32
  sleeping a few seconds...
-----with: MD5:96797C1D3F16CF50A86697AD48D4DEB8 (/bin/chmod)
Executing: /bin/chmod
  process: /bin/chmod
    class: glguerin.authkit.imp.macosx.AuthProcess
  sleeping a few seconds...
....count: 0
....check: 0x00000000 -- CRC32
-----with: MD5:96797C1D3F16CF50A86697AD48D4DEB8 (/usr/sbin/chown)
Executing: /usr/sbin/chown
  process: /usr/sbin/chown
    class: glguerin.authkit.imp.macosx.AuthProcess
  sleeping a few seconds...
....count: 0
....check: 0x00000000 -- CRC32
-----with: MD5:96797C1D3F16CF50A86697AD48D4DEB8 (/bin/ls)
Executing: /bin/ls
  process: /bin/ls
    class: glguerin.authkit.imp.macosx.AuthProcess
  sleeping a few seconds...
drwxr-xr-x  2 root  wheel  68 Jun 30 15:36 ./rooted
....count: 52
....check: 0x588341BB -- CRC32

TestAuth.main(): done...

Discussion:

When you run the command-line, the Mac OS X system authentication dialog will appear. Enter an admin name and password, then proceed. The series of external commands will execute, creating the directory "./rooted" and assigning it the desired ownership and permissions. The last command is /bin/ls, to show the resulting directory's ownership and permissions.

If a "./rooted" directory already exists, it will have its ownership and permissions changed to be root-owned and root-writable.

The first command is the 'au+' builtin asking for root-execute permission. This is actually unnecessary, but it shows that 'au+' is equivalent to 'pre' for the MacOSXAuthorization implementation.

Observe that the session's public identifier is identical throughout. This shows the same session is being used to run all external commands.

Command-line purists may note that the "-m 0755" option to /bin/mkdir could have been used. True, but if the directory already exists, the mode would not be changed, so a subsequent 'chmod' would be needed anyway.

Example 8: Inter-process Piped Secret Identifier

This example pipes a secret identifier to a privileged process, where it is written to a file in a privileged directory. This is insecure, but is acceptable as a test.

Command-line:

jauthTen -cp AuthKit.jar app.authkit.test.TestAuth \
  : put \
  : @ /usr/bin/tee rooted/t1 \
  : /usr/bin/hexdump rooted/t1 \
  : zzz 1

Output:

TestAuth.main(): starting...
Authorization: class glguerin.authkit.imp.macosx.MacOSXAuthorization
-----with: MD5:139B62096CA273847DA5DA64BB589263 (put)
## Secret identifier:
  57 37 C0 A9  6D F3 4A 1C  00 00 10 03  00 00 00 00  
  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  
-----with: MD5:139B62096CA273847DA5DA64BB589263 (@)
Executing: /usr/bin/tee
  process: /usr/bin/tee
    class: glguerin.authkit.imp.macosx.AuthProcess
gibberish
java.io.IOException: Errno: 9
        at glguerin.authkit.imp.macosx.AuthProcess$FIn.read(AuthProcess.java:186)
        at java.util.zip.CheckedInputStream.read(CheckedInputStream.java:60)
        at java.io.FilterInputStream.read(FilterInputStream.java:90)
        at glguerin.util.Streamer.pump(Streamer.java:98)
        at app.authkit.test.Digester.run(Digester.java:96)
        at java.lang.Thread.run(Thread.java:554)
....count: 0
....check: 0x329DE7D8 -- CRC32
    wrote: 32 secret bytes to child
  sleeping a few seconds...
-----with: MD5:139B62096CA273847DA5DA64BB589263 (/usr/bin/hexdump)
Executing: /usr/bin/hexdump
  process: /usr/bin/hexdump
    class: glguerin.authkit.imp.macosx.AuthProcess
  sleeping a few seconds...
0000000 5737 c0a9 6df3 4a1c 0000 1003 0000 0000
0000010 0000 0000 0000 0000 0000 0000 0000 0000
0000020
....count: 104
....check: 0x35313092 -- CRC32
-----with: MD5:139B62096CA273847DA5DA64BB589263 (zzz)
 sleeping: 1

Discussion:

When you run the command-line, the Mac OS X system authentication dialog will appear. Enter an admin name and password, then proceed. The /usr/bin/tee command will execute, receiving the secret identifier on its stdin stream, and writing it to the file "rooted/t1". We can surmise that /usr/bin/tee is executing as root, because otherwise it would be unable to create a file in the "rooted" directory.

In addition to writing the secret identifier to a file, the tee command also sends those binary bytes to its stdout, which is undesired but unavoidable, and mostly harmless. It appears as gibberish in the displayed output. Finally, /usr/bin/hexdump command dumps the file written by tee, so we can compare it to what the 'put' builtin showed at first.

The 'put' builtin displays hex data in a different form than /usr/bin/hexdump does by default, but you can easily compare the two and see that there is no difference in the secret identifier bytes. This confirms that /usr/bin/tee actually received the secret identifier intact.

The '@' prefix to the /usr/bin/tee command tells TestAuth to pipe the secret identifier to the Process. Without the '@' prefix, nothing would be piped to /usr/bin/tee, and it would simply create an empty file for "rooted/t1". This would confirm that tee is running with root privileges, but that's all.

The IOException is thrown because of the coupling between the stdin and stdout streams of the privileged Process returned by MacOSXAuthorization.execPrivileged(). After the piped secret identifier is written, the stream must be closed, so /usr/bin/tee will see an EOF and terminate. But that close also closes the stdout stream going from /usr/bin/tee back to the TestAuth process. That isn't handled well by the simple-minded code in the Digester, so we see a stack trace from its run() method. We do get confirmation that 32 secret bytes were written to the privileged Process, and we further confirm it by seeing the hex-dump of what tee wrote to the file.

A later example also pipes the secret identifier between processes, but it will go to a Java program, which then attaches itself to the same session.

Example 9: Attaching to a Privileged Session

This example illustrates a privileged process attaching to an existing session using Authorization.attachPrivileged(). It runs a Java program as root to do this.

Command-line:

jauthTen -cp AuthKit.jar app.authkit.test.TestAuth \
  : put \
  : au+ system.privilege.admin PRIV \
  : see system.privilege.admin PRIV \
  : JAVA -cp AuthKit.jar \
    -Dauthkit.imp=glguerin.authkit.imp.macosx.MacOSXAuthorization \
    -Dattach=. \
    -Dout.1=rooted/priv \
    app.authkit.test.TestAttach system.privilege.admin PRIV OTHER \
  : see system.privilege.admin PRIV OTHER

Output:

TestAuth.main(): starting...
Authorization: class glguerin.authkit.imp.macosx.MacOSXAuthorization
-----with: MD5:DA52DFA8C55C0EA5A86F0478B71FE077 (put)
## Secret identifier:
  74 53 A9 01  BD 6C 88 1F  00 00 10 03  00 00 00 00  
  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  
-----with: MD5:DA52DFA8C55C0EA5A86F0478B71FE077 (au+)
Privilege: system.privilege.admin::0
     auth: system.privilege.admin::0 at Mon Jun 30 17:08:04 MST 2003 -- true
Privilege: PRIV::0
     auth: PRIV::0 at Mon Jun 30 17:08:04 MST 2003 -- true
-----with: MD5:DA52DFA8C55C0EA5A86F0478B71FE077 (see)
Privilege: system.privilege.admin::0 at Mon Jun 30 17:08:04 MST 2003 -- true
Privilege: PRIV::0 at Mon Jun 30 17:08:04 MST 2003 -- true
-----with: MD5:DA52DFA8C55C0EA5A86F0478B71FE077 (JAVA)
Executing: /System/Library/Frameworks/JavaVM.framework/Versions/1.4.1/Home/bin/java
  process: /System/Library/Frameworks/JavaVM.framework/Versions/1.4.1/Home/bin/java
    class: glguerin.authkit.imp.macosx.AuthProcess
  sleeping a few seconds...
Authorization: class glguerin.authkit.imp.macosx.MacOSXAuthorization
-- TestAttach --
.. java.version: 1.4.1_01
..... java.home: /System/Library/Frameworks/JavaVM.framework/Versions/1.4.1/Home
.... Session-ID: MD5:DA52DFA8C55C0EA5A86F0478B71FE077
..... Privilege: system.privilege.admin::0 at Mon Jun 30 17:08:05 MST 2003 -- true
..... Privilege: PRIV::0 at Mon Jun 30 17:08:05 MST 2003 -- true
..... Privilege: OTHER::0 at Mon Jun 30 17:08:05 MST 2003 -- true
   writing: rooted/priv
TestAttach.main(): done...

....count: 0
....check: 0x00000000 -- CRC32
-----with: MD5:DA52DFA8C55C0EA5A86F0478B71FE077 (see)
Privilege: system.privilege.admin::0 at Mon Jun 30 17:08:04 MST 2003 -- true
Privilege: PRIV::0 at Mon Jun 30 17:08:04 MST 2003 -- true
Privilege: OTHER::0 -- true

TestAuth.main(): done...

Discussion:

When you run the command-line, the Mac OS X system authentication dialog will appear. Enter an admin name and password, then proceed. The commands will execute, producing output similar to the above.

This example illustrates quite a number of things. We first perform several builtins like in Examples 7 and 8. Specifically:

A notable thing about the 'au+' builtin's behavior here is that only one authentication dialog is presented. This happens because authenticating for the "system.privilege.admin" privilege yields a credential stating that the user is a member of the 'admin' group. Thus, when the system looks for a credential for "PRIV", it sees the existing credential for "system.privilege.admin" and uses it without reauthenticating.

If the command had authorized the privileges in the opposite order, you would have seen two authentication dialogs. That happens because the credential granted for the default-rule "PRIV" privilege is not acceptable as proof of identity for the more restricted "system.privilege.admin" privilege.

After the 'see' command we have the 'JAVA' command, which is a quasi-builtin. Looking at the output, we can see that I was running the 1.4.1 JVM, because that's the full pathname executed by the privileged Process. If I had run this example under the 1.3.1 java command, then the 'JAVA' quasi-builtin would have chosen the 1.3.1 java command's full pathname. Recall the difference between the 'JAVA' builtin and /usr/bin/java illustrated by Example 6.

The other parameters to the 'JAVA' command tell it to run the TestAttach class, and tell TestAttach how to attach to the privileged session. Specifically:

The output produced by TestAttach shows several things:

If you 'cat rooted/priv' you can see for yourself that it contains the same session-ID (public identifier) and the same Privilege grants as the parent process.

Finally, the output shows that the parent process still has the same session and the same Privilege grants it had before launching the child process. This shows that when an attached session is released in a child process, it doesn't affect the state of the session itself, so processes that are still attached to the session don't see any privileges or credentials "evaporate".

Example 10: Attaching via Piped Secret Identifier

This example is similar to Example 9, but instead of using attachPrivileged() it reads the secret identifier from its stdin pipe and uses attach().

Command-line:

jauthTen -cp AuthKit.jar app.authkit.test.TestAuth \
  : put \
  : au+ system.privilege.admin PIPED \
  : @ JAVA -cp AuthKit.jar \
    -Dauthkit.imp=glguerin.authkit.imp.macosx.MacOSXAuthorization \
    -Dattach=@ \
    -Dout.1=rooted/piped \
    app.authkit.test.TestAttach system.privilege.admin PIPED OTHER \
  : see system.privilege.admin PIPED OTHER

Output:

TestAuth.main(): starting...
Authorization: class glguerin.authkit.imp.macosx.MacOSXAuthorization
-----with: MD5:6D678E830FDC284B88B3C8BDD86DDE33 (put)
## Secret identifier:
  A8 E6 0E 2B  E7 DA CD F0  00 00 10 03  00 00 00 00  
  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  
-----with: MD5:6D678E830FDC284B88B3C8BDD86DDE33 (au+)
Privilege: system.privilege.admin::0
     auth: system.privilege.admin::0 at Tue Jul 01 10:30:56 MST 2003 -- true
Privilege: PIPED::0
     auth: PIPED::0 at Tue Jul 01 10:30:56 MST 2003 -- true
-----with: MD5:6D678E830FDC284B88B3C8BDD86DDE33 (@)
Executing: /System/Library/Frameworks/JavaVM.framework/Versions/1.4.1/Home/bin/java
  process: /System/Library/Frameworks/JavaVM.framework/Versions/1.4.1/Home/bin/java
    class: glguerin.authkit.imp.macosx.AuthProcess
    wrote: 32 secret bytes to child
  sleeping a few seconds...
Authorization: class glguerin.authkit.imp.macosx.MacOSXAuthorization
-- TestAttach --
.. java.version: 1.4.1_01
..... java.home: /System/Library/Frameworks/JavaVM.framework/Versions/1.4.1/Home
.... Session-ID: MD5:6D678E830FDC284B88B3C8BDD86DDE33
..... Privilege: system.privilege.admin::0 at Tue Jul 01 10:30:57 MST 2003 -- true
..... Privilege: PIPED::0 at Tue Jul 01 10:30:57 MST 2003 -- true
..... Privilege: OTHER::0 at Tue Jul 01 10:30:57 MST 2003 -- true
   writing: rooted/piped
TestAttach.main(): done...

....count: 0
....check: 0x00000000 -- CRC32
-----with: MD5:6D678E830FDC284B88B3C8BDD86DDE33 (see)
Privilege: system.privilege.admin::0 at Tue Jul 01 10:30:56 MST 2003 -- true
Privilege: PIPED::0 at Tue Jul 01 10:30:56 MST 2003 -- true
Privilege: OTHER::0 -- true

TestAuth.main(): done...

Discussion:

When you run the command-line, the Mac OS X system authentication dialog will appear. Enter an admin name and password, then proceed. The commands will execute, producing output similar to the above.

This example is almost identical to Example 9. The main difference is in how the JAVA quasi-builtin is run. Here, we precede it with "@", indicating that the secret identifier should be piped to the process. We also define the "attach" property's value as "@", so TestAttach will know to read its stdin for the secret identifier.

Once the secret identifier is piped across, we see TestAttach runs in exactly the same way as with attachPrivileged() in Example 8. You can't tell from the output, but TestAttach will block while reading its stdin for the secret identifier, and before it calls attach(). This differs from attachPrivileged(), which does not perform any I/O before attaching to the session. You should keep this in mind when creating your own privileged programs, because handling I/O errors safely and securely may ultimately determine the overall security of your program.

Unlike in Example 7, where we used /usr/bin/tee to read the secret identifier from stdin, we suffer no repercussions or undesired IOExceptions from the coupling between stdin and stdout when closing the pipe's stream. That's because the TestAttach program was written so all its output is produced on stderr, and none on stdout. In short, it was written to work properly with coupled stdin and stdout streams. If you use piping of secret identifiers in your Java programs, you should do the same or better. We may forgive native Unix tools that don't behave well with coupled stdio streams. After all, they weren't written to accomodate, or even be aware of, this situation. In your own programs, though, ignoring this peculiarity of the Authorization Services API is nothing but sloppy or reckless expedience.


To Greg's Home Page
To Greg's Software Page