Notarization is a process that identifying malicious software before it is distributed.
Notarization will perform security checks. The application is submitted to the Apple Notary Service, which will perform inspections and send back a ticket. This ticket will be stapled to the software. Then the software is ready to be distributed.
Once the user downloads the software, Gatekeeper will do a verification:
⚠️ If the ticket is not stapled, Gatekeeper will still perform the verification. However, depending on the OS version or the user security settings, it could result in a slower experience and different behavior.
Code signing ensures that your code was not modified after its distribution, and notarization performs security checks that you, as a developer, could have introduced.
Note that the notarization process will ensure the application is appropriately code signed.
notarytool
In a previous post, we dived into code signing, let’s resume the work we did there to start our journey into notarization.
First, let’s clone the repository
git clone git@github.com:tony-go/codesign-macos.git
Please check out the codesign-only
branch.
As the readme mentions, ensure you have Xcode and CMake installed on your machine.
Then, run the following:
TEAM_ID="<your developer ID>" make
You should see this in the console output:
...
** BUILD SUCCEEDED **
cpack -G DragNDrop -B dist --config ./dist/CPackConfig.cmake -C Release
CPack: Create package using DragNDrop
CPack: Install projects
CPack: - Install project: MyMacOSApp [Release]
CPack: Create package
CPack: - package: /Users/tonygorez/perso/codesign-macos/dist/MyMacOSApp-0.1.1-Darwin.dmg generated.
codesign --verify --verbose=2 ./dist/Release/MyMacOSApp.app
./dist/Release/MyMacOSApp.app: valid on disk
./dist/Release/MyMacOSApp.app: satisfies its Designated Requirement
codesign --force --verbose=2 --sign "<TEAM_ID>" ./dist/MyMacOSApp-0.1.1-Darwin.dmg
./dist/MyMacOSApp-0.1.1-Darwin.dmg: signed []
It means that the application build succeeds and the app is signed correctly.
👉If you don’t know to generate a certificate, please check this post about code signing.Last, check that the
notarytool
util is available on your machine:
xcrun notarytool help
Now, let’s notarize our application.
Along the way, you will probably use notarytool
for different purposes: submit your application, get info on your submission, or inspect logs. You must pass a punch of arguments for each of these actions: Apple ID, organization ID, and an app-specific password.
👉As you need to create an app-specific password, please do.Passing three arguments each time you must perform an action with
notarytool
, could be cumbersome. Moreover, if you need to script out all these actions, you probably prefer not to pass sensitive information into your code.
It’s why we will use notarytool store-credential
command to store our credentials and re-use them through a keychain profile.
Aiming to perform this store-credential
command, you need three pieces of information:
Developer ID Application: JOHN, DOE (X4MF6H9XZ6)
the team ID is: X4MF6H9XZ6
Then you can run the following command:
xcrun notarytool store-credentials <PROFILE_NAME> \
--apple-id <APPLE_ID> \
--team-id <TEAM_ID> \
--password <APP_SPECIFIC_PASSWORD>
Store credentials with notarytool
With values, it should look like this:
xcrun notarytool store-credentials "KC_PROFILE" \
--apple-id john.doe@dunno.com \
--team-id X4MF6H9XZ6 \
--password qini-xppm-lzvl-buwq
You should see:
This process stores your credentials securely in the Keychain. You reference these credentials later using a profile name.
Validating your credentials...
Success. Credentials validated.
Credentials saved to Keychain.
To use them, specify `--keychain-profile "KC_PROFILE"
Now it is time to notarize our application. If the build you did on setup succeeds appropriately, you should find the disk image at: dist/MyMacOSApp-0.1.1-Darwin.dmg
We’ll use these for the next steps.
Let’s submit the dmg
:
xcrun notarytool submit ./dist/MyMacOSApp-0.1.1-Darwin.dmg \
--keychain-profile "KC_PROFILE" \
--wait
Submit the disk image to the Notary service with the keychain profileHere we submitted ./dist/MyMacOSApp-0.1.1-Darwin.dmg
with the keychain profile "KC_PROFILE"
.
You probably noticed the --wait
option. The purpose of this option is to wait until the submission is finished. This is convenient, primarily if you used the atool
command (previous notary command) where you had to poll the Apple server to check the status of your submission.
Oh, but it failed:
Conducting pre-submission checks for MyMacOSApp-0.1.1-Darwin.dmg and initiating connection to the Apple notary service...
Submission ID received
id: e415f471-6352-4ac5-9711-bc7d50730360
Upload progress: 100,00 % (33,5 KB of 33,5 KB)
Successfully uploaded file
id: e415f471-6352-4ac5-9711-bc7d50730360
path: /Users/tonygorez/perso/codesign-macos/dist/MyMacOSApp-0.1.1-Darwin.dmg
Waiting for processing to complete.
Current status: Invalid........
Processing complete
id: e415f471-6352-4ac5-9711-bc7d50730360
status: Invalid
But what is the root cause? Let’s take a look at the log
command.
You can reuse the id and pass it to the log
command:
xcrun notarytool log e415f471-6352-4ac5-9711-bc7d50730360 --keychain-profile "KC_PROFILE"
It will output:
{
"logFormatVersion": 1,
"jobId": "e415f471-6352-4ac5-9711-bc7d50730360",
"status": "Invalid",
"statusSummary": "Archive contains critical validation errors",
"statusCode": 4000,
"archiveFilename": "MyMacOSApp-0.1.1-Darwin.dmg",
"uploadDate": "2023-07-18T07:36:43.926Z",
"sha256": "e5b06cedadd3ae0e61d08f2304e8eb0cbffc4a05d8e9f98be8070bff5b4f4d2e",
"ticketContents": null,
"issues": [
{
"severity": "error",
"code": null,
"path": "MyMacOSApp-0.1.1-Darwin.dmg/MyMacOSApp.app/Contents/MacOS/MyMacOSApp",
"message": "The signature does not include a secure timestamp.",
"docUrl": "https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution/resolving_common_notarization_issues#3087733",
"architecture": "arm64"
},
{
"severity": "error",
"code": null,
"path": "MyMacOSApp-0.1.1-Darwin.dmg/MyMacOSApp.app/Contents/MacOS/MyMacOSApp",
"message": "The executable does not have the hardened runtime enabled.",
"docUrl": "https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution/resolving_common_notarization_issues#3087724",
"architecture": "arm64"
},
{
"severity": "error",
"code": null,
"path": "MyMacOSApp-0.1.1-Darwin.dmg/MyMacOSApp.app/Contents/MacOS/MyMacOSApp",
"message": "The executable requests the com.apple.security.get-task-allow entitlement.",
"docUrl": "https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution/resolving_common_notarization_issues#3087731",
"architecture": "arm64"
}
]
}
Look at the issues
property! It is an array of objects, each containing the payload of an error. What I like is that Apple provides a docUrl
property that links to documentation for the specific error rather than just a message
.
I won’t detail each error. We’ll fix them quickly by adding two properties into the CMakeLists.txt
:
set_target_properties(${APP_NAME} PROPERTIES
XCODE_ATTRIBUTE_CODE_SIGN_STYLE ${XCODE_ATTRIBUTE_CODE_SIGN_STYLE}
XCODE_ATTRIBUTE_DEVELOPMENT_TEAM ${XCODE_ATTRIBUTE_DEVELOPMENT_TEAM}
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY ${XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY}
XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED ${XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED}
# Add these properties
XCODE_ATTRIBUTE_OTHER_CODE_SIGN_FLAGS "--timestamp=http://timestamp.apple.com/ts01 --options=runtime,library"
XCODE_ATTRIBUTE_CODE_SIGN_INJECT_BASE_ENTITLEMENTS "NO"
)
Now, let’s build the app again:
TEAM_ID="<your developer ID>" make
And replay the notarytool submit
command:
Conducting pre-submission checks for MyMacOSApp-0.1.1-Darwin.dmg and initiating connection to the Apple notary service...
Submission ID received
id: 004d775c-3409-4967-b1b4-f3748a7eacb6
Upload progress: 100,00 % (33,6 KB of 33,6 KB)
Successfully uploaded file
id: 004d775c-3409-4967-b1b4-f3748a7eacb6
path: /Users/tonygorez/perso/codesign-macos/dist/MyMacOSApp-0.1.1-Darwin.dmg
Waiting for processing to complete.
Current status: Accepted........
Processing complete
id: 004d775c-3409-4967-b1b4-f3748a7eacb6
status: Accepted
🎉 Notarization finally happened! But it is not over …
Gatekeeper will perform a check for a notarization ticket online. If it can’t reach the server (due to no internet connection, for example), and if the ticket isn’t stapled to the app, macOS will prevent the app from running because it can’t verify that it is notarized.
It’s why we staple the disk image (it also works with a .pkg
or an .app
) with the stapler
command:
xcrun stapler staple ./dist/MyMacOSApp-0.1.1-Darwin.dmg
It should output:
Processing: /Users/tonygorez/perso/codesign-macos/dist/MyMacOSApp-0.1.1-Darwin.dmg
Processing: /Users/tonygorez/perso/codesign-macos/dist/MyMacOSApp-0.1.1-Darwin.dmg
The staple and validate action worked!
spctl
is the command line interface to manage and control the system policy security (Gatekeeper).
We’ll use it to check that the notarization process is okay:
xcrun spctl --assess \
--type open \
--context context:primary-signature \
--ignore-cache \
--verbose=2 \
./dist/MyMacOSApp-0.1.1-Darwin.dmg
Let’s untangle the bunch of options we have here:
--assess
: This option tells spctl
to assess the specified object. In this context, it checks if the .dmg
file conforms to the system’s security policy.--type open
: This option is used to determine the type of assessment to perform. The open
type is generally used for files that result in some form of opened UI when they are run.--context context:primary-signature
: It assesses the primary signature of the application (in some cases, we could have additional, nested code signatures, for instance, a signed app that contains a signed framework).--ignore-cache
: This option tells spctl
to ignore any cached assessment results and reassess the specified object.--verbose=2
: This sets the verbosity level of the output.Once run successfully, the command outputs:
./dist/MyMacOSApp-0.1.1-Darwin.dmg: accepted
source=Notarized Developer ID
Congratulations 🎉! You finally managed to get it done!
If you want to have a pointer to all of these commands, visit this repo on the main branch: