Code Signing Scripts for PPPC Whitelisting

One of the issues with creating a Privacy Preferences Policy Control Payload (PPPCP) profiles is working out whether you will whitelist an entire shell or interpreter (for example, /bin/bash or /usr/bin/python) or go down the route of code signing scripts that trigger a TCC User Consent dialog.

On the face of it, whitelisting an entire shell or interpreter seems like the easiest way to handle this situation, especially if you deploy a tool like outset. However, there is the possibility that some malicious app might drop a LaunchDaemon or LaunchAgent onto a macOS system that executes a shell script, and suddenly, you’ve got a script that is essentially allowed to execute any action that might ordinarily have prompted a consent dialog.

The alternative is to be diligent with what gets whitelisted, and rather than whitelisting /usr/bin/python (as an example), you may choose to code sign your scripts and generate PPPCP profiles to whitelist those scripts. This would allow you to be somewhat more granular in what is pre-approved for running on a system without generating consent dialogs.


Code signing a script does not work in the same way that code signing an app bundle does.

When an app bundle is code signed, the details/requirements are put in a folder called _CodeSignature.

When a plain text file is code signed, the signature ends up in an extended attribute, specifically four different attributes.

[carl@pegasus]:outset # codesign -s "Mac Developer: (ABC01FFFGH)" -i com.github.outset outset
[carl@pegasus]:outset # ls -lha
total 8
drwxr-xr-x 4 carl staff 128B 23 Sep 12:53 .
drwxr-xr-x 4 carl staff 128B 23 Sep 12:30 ..
drwxr-xr-x 4 carl staff 128B 23 Sep 12:09 FoundationPlist
-rwxr-xr-x@ 1 carl staff 1.0K 23 Sep 12:20 outset
[carl@pegasus]:outset # xattr outset
[carl@pegasus]:outset # codesign -dr - outset
designated => identifier "com.github.outset" and anchor apple generic and certificate leaf[subject.CN] = "Mac Developer: (ABC01FFFGH)" and certificate 1[field.1.2.345.678901.] /* exists */

This presents no problem as long as the code signed file stays where it is, or is moved around by tools that keep the extended attributes attached to the file (such as mv, cp, or rsync with appropriate flags, or even certain compressed file formats).

It is a problem if you need to build a package to deploy the file to another machine.

So far in testing, I’ve found that any package built with Packages, or pkgbuild/productbuild, the extended attributes for files are stripped when the package is built, and it seems there are no command line arguments for pkgbuild/productbuild to keep the extended attributes (update: this is not strictly true, see the update below). The Packages app also strips extended attributes when building a package (including when a file is embedded as a resource for the installer).

Work around

Update: Greg Neagle replied to a tweet about the issue of the extended attributes missing after building a package. If you use pkgbuild, simply add the --preserve-xattr flag to the pkgbuild command to preserve the extended attributes.
This is an undocumented flag/feature, so older versions of pkgbuild may not support it. This was tested on macOS Mojave 10.14, with Xcode 10 installed.

Once I know how to do the same thing with Packages, I’ll be sure to update, other wise, the work around below will work in a pinch.

To get around this, there are several compressed file formats that preserve the extended attributes of a file, one of which is the handy tar.gz file type.

In this example, the outset git repo has been cloned and changes are being made to the files within that directory. This also presumes you are familiar with the Packages application and building a package with it.

[carl@pegasus]:~ # cd ~/Desktop/git
[carl@pegasus]:git # git clone
Cloning into 'outset'…
remote: Enumerating objects: 16, done.
remote: Counting objects: 100% (16/16), done.
remote: Compressing objects: 100% (14/14), done.
remote: Total 531 (delta 4), reused 8 (delta 0), pack-reused 515
Receiving objects: 100% (531/531), 281.03 KiB | 456.00 KiB/s, done.
Resolving deltas: 100% (230/230), done.

Code sign the /usr/local/outset/outset script:

[carl@pegasus]:outset # codesign -s "Mac Developer: (ABC01FFFGH)" -i com.github.outset outset

If you want to verify the codesign result:

[carl@pegasus]:outset # codesign -dr - outset 
designated => identifier "com.github.outset" and anchor apple generic and certificate leaf[subject.CN] = "Mac Developer: (ABC01FFFGH)" and certificate 1[field.1.2.345.678901.] /* exists */

Next, a tarball needs to be created containing just the script that has been code signed:

[carl@pegasus]:outset # pwd
[carl@pegasus]:outset # cd pkgroot/usr/local/outset/
[carl@pegasus]:outset # tar -cvf outset.tar.gz outset
a outset
[carl@pegasus]:outset # ls -l
total 32
drwxr-xr-x 4 carl staff 128 23 Sep 12:09 FoundationPlist
-rwxr-xr-x@ 1 carl staff 1024 23 Sep 12:20 outset
-rw-r--r-- 1 carl staff 8704 23 Sep 13:49 outset.tar.gz

From here, include the outset.tar.gz file as a resource in a Packages project that replicates the outset installer, and modify the postinstall script that is used by the normal outset package to look like this:

resources=$(dirname $0)
[[ $3 != "/" ]] && exit 0

/bin/launchctl load /Library/LaunchDaemons/com.github.outset.boot.plist
/bin/launchctl load /Library/LaunchDaemons/com.github.outset.cleanup.plist
/bin/launchctl load /Library/LaunchDaemons/com.github.outset.login-privileged.plist
user=$(/usr/bin/stat -f '%u' /dev/console)
[[ -z "$user" ]] && exit 0
/bin/launchctl asuser ${user} /bin/launchctl load /Library/LaunchAgents/com.github.outset.login.plist
/bin/launchctl asuser ${user} /bin/launchctl load /Library/LaunchAgents/com.github.outset.on-demand.plist
/usr/bin/tar -xpf ${resources}/outset.tar.gz -C /usr/local/outset/
/usr/sbin/chown root:wheel /usr/local/outset/outset
exit 0

The additional lines in the script are (... denotes code from the original script):

resources=$(dirname $0)
/usr/bin/tar -xpf ${resources}/outset.tar.gz -C /usr/local/outset/
/usr/sbin/chown root:wheel /usr/local/outset/outset

Make sure the postinstall script is included in the Packages file, build and test. If all has gone correctly, running codesign -dr - /usr/local/outset/outset should result in the code sign details returned to stdout and you should be able to create a PPPCP profile with the code sign details of that script.


Currently, the script doesn’t used the code sign requirements of a script that has been code signed. This capability is coming soon.

Update: now supports scripts that have been code signed.


Details about how code signing works for text files was found here – Preserving Extended Attributes on OS X.