Search code examples
xcodeswiftmacossmjobbless

Gain administration privileges with swift for a Mac Application


I am writing a pice of software that needs to often run a command with root privileges.

Right now, I am doing this by asking the user for their password once, saving it and then providing that password to NSAppleScript as an argument along with with administrator privileges.

This obviously is really insecure for the user as someone could gain access to their password.

I've been searching for a better part of a week and cannot find the solution.

SMJobBless seems to allow you to install your application with a higher privilege.

I have followed app's example and I am getting an error from their SMJobBlessUtil script.

Here is the error:

SMJobBlessUtil.py: tool designated requirement (identifier "com.domain.AppName.SampleService" and anchor apple generic and certificate leaf[subject.CN] = "Mac Developer: firstName lastName (XXXXXXXXXX)" and certificate 1[field.1.2.840.113635.100.6.2.1] /* exists */) doesn't match entry in 'SMPrivilegedExecutables' (anchor apple generic and identifier "com.domain.AppName.SampleService" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.CN] = "Mac Developer: firstName lastName (XXXXXXXXXX)")

Obviously, something is wrong. Here are the respective plists

Services Info plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleIdentifier</key>
    <string>com.domain.AppName.SampleService</string>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundleName</key>
    <string>SampleService</string>
    <key>CFBundleVersion</key>
    <string>6</string>
    <key>SMAuthorizedClients</key>
    <array>
        <string>anchor apple generic and identifier "com.domain.AppName" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = xxxxxxxxxx)</string>
    </array>
</dict>
</plist>

Apps Info plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleDevelopmentRegion</key>
    <string>en</string>
    <key>CFBundleDisplayName</key>
    <dict/>
    <key>CFBundleExecutable</key>
    <string>$(EXECUTABLE_NAME)</string>
    <key>CFBundleGetInfoString</key>
    <dict/>
    <key>CFBundleIdentifier</key>
    <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundleName</key>
    <string>Away</string>
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleShortVersionString</key>
    <string>1.0.99</string>
    <key>CFBundleSignature</key>
    <string>????</string>
    <key>CFBundleVersion</key>
    <string>9</string>
    <key>LSApplicationCategoryType</key>
    <string>public.app-category.utilities</string>
    <key>LSMinimumSystemVersion</key>
    <string>$(MACOSX_DEPLOYMENT_TARGET)</string>
    <key>LSUIElement</key>
    <true/>
    <key>NSHumanReadableCopyright</key>
    <string>Copyright © 2016 firstName lastName. All rights reserved.</string>
    <key>NSMainStoryboardFile</key>
    <string>Main</string>
    <key>NSPrincipalClass</key>
    <string>NSApplication</string>
    <key>SMPrivilegedExecutables</key>
    <dict>
        <key>com.domain.AppName.SampleService</key>
        <string>anchor apple generic and identifier "com.domain.AppName.SampleService" and (certificate leaf[field.1.2.840.113635.100.6.1.9] /* exists */ or certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.CN] = "Mac Developer: firstName lastName (XXXXXXXXXX)"</string>
    </dict>
</dict>
</plist>

I've looked at this stackoverflow post and many others like it. As I understand them , I have my plists setup correctly. What am I doing wrong?


Solution

  • The key part of this approach is described under the "PROPERTY LISTS" section under "How It Works" of the ReadMe.txt:

    […] when you sign the helper tool with a Developer ID, Xcode automatically sets the helper tool's designated requirement like this, and that's what you should use for SMPrivilegedExecutables. Moreover, this is what the "setreq" command shown above does: extracts the designated requirement from the built tool and put it into the app's Info.plist source code.

    Since you are not signing the products (at least, not with the certificate described in your examples), this process will always fail.

    If you are not in the Developer Program, you can create a self-signed certificate to sign with. However, this more or less defeats the purpose of the signing requirement. If you do not plan on enrolling in the Developer Program, you should be able to abbreviate the process as follows:

    1. In your app's Info.plist, abbreviate the requirement under SMPrivilegedExecutables to just match the helper's identifier:

    <string>identifier "com.domain.AppName.SampleService"</string>

    • In your helper's Info.plist, abbreviate the requirement under SMAuthorizedClients to just match the app's identifier:

    <string>identifier "com.domain.AppName"</string>

    • Ignore the "Building and Running the Sample" instructions of the ReadMe.txt, and intead simply build and run the project as normal.

    I can't say I recommend this of course; these signing requirements exist for good reason. It is at least better than the final alternative however, which would be using that NSAppleScript to give a helper executable a root setuid bit via chmod and chown.


    Addendum to elaborate on some of the concepts in play here:

    Running privileged code comes with a lot of potential security holes; safely authenticating the user is only the first step. Delegating all privileged operations to a separate process is another strong step, but the major issue that remains is how to ensure that your app – the one the user actually granted privileges for - is the only entity capable of utilizing the privileged access.

    Apple's example demonstrates the use of code signing to solve this problem. Just in case you're not familiar: Code signing involves marking your final products cryptographically in such a way that OS X can verify your programs haven't been replaced with compromised versions. Those extra "certificate leaf" references in the original example's SMAuthorizedClients and SMPrivilegedExecutables are specifically for this; they describe the the certificate that your app and helper must have been signed with in order to interact with one another. are documented to serve other purposes than validating code signing during interaction. As pointed out by this blog post and the corresponding CVE, client programmers are responsible for ensuring cryptographicly safe inter-process communication between helper tool and app, e.g. using the SecCode API.

    To help paint the picture a bit, here's a rough rundown of how this plays out:

    1. Your user grants authorization to launchd to install the helper daemon labeled com.domain.AppName.SampleService.
    2. launchd locates the com.domain.AppName.SampleService entry under SMPrivilegedExecutables in your app's Info.plist; this describes the certificate that the helper's binary should be signed with. (If they do not match, then theoretically an attacker has replaced your helper tool with their own version in order to run it as root.) Note that the documentation specifically describes the key SMPrivilegedExecutables to be only relevant for updating purposes:

    The calling application's Info.plist must include a "SMPrivilegedExecutables" dictionary of strings. Each string is a textual representation of a code signing requirement used to determine whether the application owns the privileged tool once installed (i.e. in order for subsequent versions to update the installed version). 3. With the valid helper tool installed, your app makes a request to launchd to spawn the helper under your control. At this point, launchd consults the SMAuthorizedClients section of your helper tool's Info.plist to ensure the app actually does have the right to run the tool. And, of course, it verifies your app's signature to ensure it hasn't been tampered with. Note, again, that the documentation specifically describes the key SMAuthorizedClients to be only relevant for updating purposes: The helper tool must have an embedded Info.plist containing an "SMAuthorizedClients" array of strings. Each string is a textual representation of a code signing requirement describing a client which is allowed to add and remove the tool.

    Getting back to your scenario, the way your products are currently working is by eliminating the signing steps. The only thing you have instructed launchd to check is whether your app's Info.plist lists its ID as "com.domain.AppName". Since there's nothing stopping an attacker from changing their Info.plist to say this as well, you're banking on the hope that they couldn't use your helper tool to do any harm once they have control of it.

    Additional addendum outlining the alternatives: