Appendix I: Notarizer
notarizer
The notarizer tool allows notarizing DMG and .app bundles without depending on Apple built-in tools (such as notarytool
, altool
and the stapler
), allowing submiting your software from macOS, Linux and Windows.
Setting up the tool
The easiest way to set it up is to invoke the setup command:
$> notarizer setup --help
setup writes the required configuration file to be able to use the notarizer
Usage:
notarizer setup [flags]
Flags:
-h, --help help for setup
--issuer-id string App Store Connect API issuer ID
--key-id string App Store Connect API key ID
--password string Apple app-specific password (app-specific)
--private-key-file string Path to the App Store Connect API private key file
--team-id string Apple Developer Team ID (app-specific)
--username string Apple ID username (app-specific)
-y, --yes Automatically answer yes to all overwrite and create questions
Global Flags:
--config-file string Configuration file
--log-level string Log Level: (trace, debug, info, warn, error, fatal, panic) (default "info")
--no-color Do not use colors in the output messages
--plain-output Do not use colors nor animations in the output messages
The notarizer is able to use either app-spacific passwords or App Store Connect API keys. The tool will ask which of them do you have, but if you manually create the config.json
file or you provide both set of credentials using flags, the former (app-specific passwords) will take precedence.
Saving the configuration to a custom file
By default, the tool will try setup the configuration file under home directory but you can override the location using the --config-file
flag:
$> notarizer setup --config-file custom.json
Configuration file "custom.json" does not exist. Do you want to create it? [Y/n]: y
...
Configuration file "custom.json" successfully created
Configuring app-specific passwors
First, get your app-specific password.
Then, run the setup process:
### Using app-specific passwords
$> notarizer setup
Configuration file "/Users/backstaff/.installbuilder/notarizer/etc/config.json" does not exist. Do you want to create it? [Y/n]:
Do you have an Apple app-specific password? [y/n]: y
Enter your Apple ID username: testuser
Enter you Apple app-specific password: *****
Enter your Apple Developer Team ID: My Team
Configuration file "/Users/backstaff/.installbuilder/notarizer/etc/config.json" successfully created
Or all at once:
$> notarizer setup --config-file config.json --username testuser --password 'secret!' --team-id "My Team" --yes
Configuration file "config.json" successfully created
$> cat foo.json
{
"Auth": {
"Username": "testuser",
"Password": "secret!",
"TeamID": "My Team",
"KeyID": "",
"IssuerID": "",
"PrivateKey": ""
}
}
Configuring App Store Connect API keys
First, get your App Store Connect API keys
Then, run the setup process:
### Using App Store Connect API keys
$> notarizer setup
Configuration file "/Users/backstaff/.installbuilder/notarizer/etc/config.json" does not exist. Do you want to create it? [Y/n]:
Do you have an Apple app-specific password? [y/n]: n
Do you have a App Store Connect API key? [y/n]: y
Enter your App Store Connect API key ID: SFAZX65J1N
Enter your App Store Connect API issuer ID: 24d44e78-9e92-a808-2cf779d390d3
Enter your App Store Connect API key Text (in base64): ***********************...***********
Configuration file "custom.json" successfully created
Providing the API key in this way is a little annoying, so it is better to provide it via command line flags:
$> notarizer setup --config-file config.json --key-id SFAZX65J1N --issuer-id 24d44e78-9e92-a808-2cf779d390d3 --private-key-file AuthKey_SFAZX65J1N.p8 --yes
Configuration file "config.json" successfully created
$> cat config.json
{
"Auth": {
"Username": "",
"Password": "",
"TeamID": "",
"KeyID": "SFAZX65J1N",
"IssuerID": "24d44e78-9e92-a808-2cf779d390d3",
"PrivateKey": "bPFsu6VcN8gFt+GBJfw...F2AsM7k81ZIcnhjOMsFC1EcUnfL/aqCLYu/x0QM7XKJcWcY8ClhSc3RsQeOukK"
}
}
Usage
The tool supports different operations to perform:
Send DMG or APP files to be notarized
This subcommand allows sending signed DMGs or .app bundles to the Apple notarization servers:
$> notarizer upload --help
upload sends a signed package for notarization
Usage:
notarizer upload [file] [flags]
Aliases:
upload, submit
Flags:
-h, --help help for upload
--interval int Wait interval between checks in seconds (default 10)
--staple Staple the resulting asset (implies wait)
--summary-file string Summary File in JSON format
--timeout int Wait timeout (default 1200)
--wait Wait for the upload to be processed
Global Flags:
--config-file string Configuration file
--log-level string Log Level: (trace, debug, info, warn, error, fatal, panic) (default "info")
--no-color Do not use colors in the output messages
--plain-output Do not use colors nor animations in the output messages
The process sends the provided DMG file to be notarized and prints the associated request id, that can be used to later query for the status (make sure the DMG and its bundled app is signed):
$> notarizer upload sample-1.0-osx-installer.dmg
» Notarizing "./sample-1.0-osx-installer.dmg"
» Uploading "./sample-1.0-osx-installer.dmg" for notarization
✔ Uploaded with id "9da7b0ce-b23d-4661-a6c6-bbccbac63c72" [In Progress]
Success!
Using this id, you will be able to check for its status (see the next subcommand section). Alternatively, you can also instruct the notarizer tool to wait for the analysis process to finish:
$> notarizer upload sample-1.0-osx-installer.dmg --wait
» Notarizing "./sample-1.0-osx-installer.dmg"
» Uploading "./sample-1.0-osx-installer.dmg" for notarization
✔ Uploaded with id "9da7b0ce-b23d-4661-a6c6-bbccbac63c72" [In Progress]
✔ Finished waiting for id "9da7b0ce-b23d-4661-a6c6-bbccbac63c72": Accepted
Success!
Waiting for a notarization submission status
If you submitted an asset to be notarized wihout specifying the --wait
flag, you can wait for it using its submission id:
$> notarized wait
./bin/notarizer wait 9da7b0ce-b23d-4661-a6c6-bbccbac63c72
Notarization sucessful for "9da7b0ce-b23d-4661-a6c6-bbccbac63c72" [Accepted]
If you did not take note of the submission id, you can list your latest submission and get the id from there:
$> notarizer list --output-format json --last 1| jq '.[].id'
"9da7b0ce-b23d-4661-a6c6-bbccbac63c72"
Query a notarization submission status
After a DMG file is sent for notatization, Apple server will take care of analyzing it and if it passes its validation, it will publish a notarization ticket so Gatekeeper can verify it later on.
You can refer to your notarization tickets by the id
returned by the upload command.
A ticket being processed would look like:
$> notarizer status 934941aa-c8ab-40dc-af39-f68e84e1d3d1
{
"id": "934941aa-c8ab-40dc-af39-f68e84e1d3d1",
"type": "submissions",
"attributes": {
"status": "Accepted",
"name": "sample-1.0-osx-installer.dmg",
"createdDate": "2024-06-17T07:21:30.891Z"
}
}
In the above log, we managed to successfully notarize the DMG, but it is also possible to get a rejection:
$> notarizer status 52371698-c890-42cc-84c6-1a75170deca0
{
"id": "52371698-c890-42cc-84c6-1a75170deca0",
"type": "submissions",
"attributes": {
"status": "Invalid",
"name": "sample-1.0-osx-installer.dmg",
"createdDate": "2024-09-27T11:46:06.621Z"
}
}
And you can get more info through the log command:
$> notarizer log -output-format json 00ebf941-af71-4720-bf38-58f818f37847
{
"logFormatVersion": 1,
"jobId": "00ebf941-af71-4720-bf38-58f818f37847",
"status": "Invalid",
"statusSummary": "Archive contains critical validation errors",
"statusCode": 4000,
"archiveFilename": "sample-1.0-osx-installer.dmg",
"uploadDate": "2024-09-27T11:46:02.981Z",
"sha256": "ea8e0b633a4fea0de41b91f0495bc2bf3d233f5443e02cca36f02d97f30c7a53",
"ticketContents": null,
"issues": [
{
"severity": "error",
"code": null,
"path": "sample-1.0-osx-installer.dmg/sample-1.0-osx-installer.app/Contents/MacOS/osx-x86_64",
"message": "The binary is not signed with a valid Developer ID certificate.",
"docUrl": null,
"architecture": "x86_64"
}
]
}
It is also possible to list the submissions. By default it will show all the history, but you can control the amount of tickets using the --last
flag:
$> notarizer list --last 5
┌───┬─────────────┬──────────────────────────────────────┬──────────────┬───────────────┐
│ # │ STATUS │ ID │ NAME │ CREATEDDATE │
├───┼─────────────┼──────────────────────────────────────┼──────────────┼───────────────┤
│ 0 │ Accepted │ 69f4c22f-679e-4f56-80e9-30b78d366ae6 │ sample-5-dmg │ 2024-06-17T07 │
│ 1 │ In Progress │ 53758b06-a445-451c-bca2-9a43dda82256 │ sample-4.dmg │ 2024-06-17T3Z │
│ 2 │ Accepted │ 51b61552-95e0-433b-bc86-1d18d3a43268 │ sample-3.dmg │ 2024-06-15T2Z │
│ 3 │ Accepted │ 73c0e332-fdef-4e74-97d0-c59972924333 │ sample-2.dmg │ 2024-06-15T7Z │
│ 4 │ Accepted │ 547f605b-4035-4996-9ede-a8bb1d219771 │ sample-1.dmg │ 2024-06-15T6Z │
└───┴─────────────┴──────────────────────────────────────┴──────────────┴───────────────┘
Or in json
format:
$> notarizer list --output-format json --last 5
[
{
"id": "69f4c22f-679e-4f56-80e9-30b78d366ae6",
"type": "submissions",
"attributes": {
"status": "Accepted",
"name": "sample-1.0-osx-installer.dmg",
"createdDate": "2024-06-17T07:21:30.891Z"
}
},
{
"id": "53758b06-a445-451c-bca2-9a43dda82256",
"type": "submissions",
"attributes": {
"status": "In Progress",
"name": "sample-1.0-osx-installer.dmg",
"createdDate": "2024-06-17T07:21:22.803Z"
}
},
{
"id": "51b61552-95e0-433b-bc86-1d18d3a43268",
"type": "submissions",
"attributes": {
"status": "Accepted",
"name": "sample-1.0-osx-installer.dmg",
"createdDate": "2024-06-15T09:26:53.152Z"
}
},
{
"id": "73c0e332-fdef-4e74-97d0-c59972924333",
"type": "submissions",
"attributes": {
"status": "Accepted",
"name": "sample-1.0-osx-installer.dmg",
"createdDate": "2024-06-15T09:10:46.177Z"
}
},
{
"id": "547f605b-4035-4996-9ede-a8bb1d219771",
"type": "submissions",
"attributes": {
"status": "Accepted",
"name": "sample-1.0-osx-installer.dmg",
"createdDate": "2024-06-15T09:08:12.986Z"
}
}
]
Stappling the ticket back to the asset
Once an asset (DMG or .app) is notarized, when it gets downloaded to a user computer and opened, its Gatekeeper will check Apple servers to query for it notarization status. It will then find the associated ticket, and confirm it was notarized. This process however requires Internet connection (the ticket is cached locally, so this only happens the first time).
To make the process work withpout Internet, it is also possible to attach the notarization ticket to the asset:
$> notarizer staple attach sample.dmg
┌───┬────────────┬──────────────────────────────────────────────┬───────────┬──────────┬───────┐
│ # │ FILE │ TICKET │ NOTARIZED │ STAPPLED │ ERROR │
├───┼────────────┼──────────────────────────────────────────────┼───────────┼──────────┼───────┤
│ 0 │ sample.dmg │ 2/2/e011f3dc33ccc66f97f52303c5d988ff82debb4a │ true │ true │ │
└───┴────────────┴──────────────────────────────────────────────┴───────────┴──────────┴───────┘
It will modify the asset with the notarization ticket. You can verify it on macOS using the stappler command:
$> stapler validate sample.dmg
Processing: sample.dmg
The validate action worked!
If you are not running the tool on macOS, you can also use the validate
command to check if the asset is stappled:
$> notarizer staple validate ./sample.dmg
┌───┬────────────┬──────────────────────────────────────────────┬───────────┬─────────┬───────┐
│ # │ FILE │ TICKET │ NOTARIZED │ STAPLED │ ERROR │
├───┼────────────┼──────────────────────────────────────────────┼───────────┼─────────┼───────┤
│ 0 │ sample.dmg │ 2/2/e011f3dc33ccc66f97f52303c5d988ff82debb4a │ true │ true │ │
└───┴────────────┴──────────────────────────────────────────────┴───────────┴─────────┴───────┘