Skip to content

Commit

Permalink
Merge branch 'main' into electron#193
Browse files Browse the repository at this point in the history
  • Loading branch information
erickzhao authored Nov 14, 2024
2 parents c16dc7c + 031072f commit ccb3a69
Show file tree
Hide file tree
Showing 16 changed files with 499 additions and 301 deletions.
4 changes: 2 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
version: 2.1

orbs:
cfa: continuousauth/[email protected].0
node: electronjs/node@2.2.1
cfa: continuousauth/[email protected].1
node: electronjs/node@2.3.1

workflows:
test_and_release:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/add-to-project.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
creds: ${{ secrets.ECOSYSTEM_ISSUE_TRIAGE_GH_APP_CREDS }}
org: electron
- name: Add to Project
uses: dsanders11/project-actions/add-item@eb760c48894b5702398529cbb8f6e98378e315d0 # v1.3.0
uses: dsanders11/project-actions/add-item@438b25e007c2f4efec324497fadc6402e7cc61a6 # v1.4.0
with:
field: Opened
field-value: ${{ github.event.pull_request.created_at || github.event.issue.created_at }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/semantic.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: semantic-pull-request
uses: amannn/action-semantic-pull-request@cfb60706e18bc85e8aec535e3c577abe8f70378e # v5.5.2
uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # v5.5.3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ node_modules
lib
*.log
src/example.ts
docs
.vscode
10 changes: 9 additions & 1 deletion .prettierrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,13 @@
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"parser": "typescript"
"parser": "typescript",
"overrides": [
{
"files": ["*.json", "*.jsonc", "*.json5"],
"options": {
"parser": "json"
}
}
]
}
218 changes: 132 additions & 86 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,135 +9,181 @@ Electron Notarize
## Installation

```bash
# npm
npm install @electron/notarize --save-dev

# yarn
yarn add @electron/notarize --dev
```

## What is app "notarization"?

From Apple's docs in XCode:

> A notarized app is a macOS app that was uploaded to Apple for processing before it was distributed. When you export a notarized app from Xcode, it code signs the app with a Developer ID certificate and staples a ticket from Apple to the app. The ticket confirms that you previously uploaded the app to Apple.
> A notarized app is a macOS app that was uploaded to Apple for processing before it was distributed.
> When you export a notarized app from Xcode, it code signs the app with a Developer ID certificate
> and staples a ticket from Apple to the app. The ticket confirms that you previously uploaded the app to Apple.
> On macOS 10.14 and later, the user can launch notarized apps when Gatekeeper is enabled. When the user first launches a notarized app, Gatekeeper looks for the app’s ticket online. If the user is offline, Gatekeeper looks for the ticket that was stapled to the app.
> On macOS 10.14 and later, the user can launch notarized apps when Gatekeeper is enabled.
> When the user first launches a notarized app, Gatekeeper looks for the app’s ticket online.
> If the user is offline, Gatekeeper looks for the ticket that was stapled to the app.
Apple has made this a hard requirement as of 10.15 (Catalina).
As macOS 10.15 (Catalina), Apple has made notarization a hard requirement for all applications
distributed outside of the Mac App Store. App Store applications do not need to be notarized.

## Prerequisites

For notarization, you need the following things:

1. Xcode 10 or later installed on your Mac.
2. An [Apple Developer](https://developer.apple.com/) account.
3. [An app-specific password for your ADC account’s Apple ID](https://support.apple.com/HT204397).
4. Your app may need to be signed with `hardened-runtime`, including the following entitlement:
1. `com.apple.security.cs.allow-jit`
1. Xcode 13 or later installed on your Mac.
1. An [Apple Developer](https://developer.apple.com/) account.
1. [An app-specific password for your ADC account’s Apple ID](https://support.apple.com/HT204397).
1. Your app may need to be signed with `hardenedRuntime: true` option, with the `com.apple.security.cs.allow-jit` entitlement.

> [!NOTE]
> If you are using Electron 11 or below, you must add the `com.apple.security.cs.allow-unsigned-executable-memory` entitlement too.
> When using version 12+, this entitlement should not be applied as it increases your app's attack surface.
If you are using Electron 11 or below, you must add the `com.apple.security.cs.allow-unsigned-executable-memory` entitlement too.
When using version 12+, this entitlement should not be applied as it increases your app's attack surface.

## API

### Method: `notarize(opts): Promise<void>`

* `options` Object
* `tool` String - The notarization tool to use, default is `notarytool`. Can be `legacy` or `notarytool`. `notarytool` is substantially (10x) faster and `legacy` is deprecated and will **stop working** on November 1st 2023.
* `appPath` String - The absolute path to your `.app` file
* There are different options for each tool: Notarytool
* There are three authentication methods available: user name with password:
* `appleId` String - The username of your apple developer account
* `appleIdPassword` String - The [app-specific password](https://support.apple.com/HT204397) (not your Apple ID password).
* `teamId` String - The team ID you want to notarize under.
* ... or apiKey with apiIssuer:
* `appleApiKey` String - Absolute path to the `.p8` file containing the key. Required for JWT authentication. See Note on JWT authentication below.
* `appleApiKeyId` String - App Store Connect API key ID, for example, `T9GPZ92M7K`. Required for JWT authentication. See Note on JWT authentication below.
* `appleApiIssuer` String - Your App Store Connect API key issuer, for example, `c055ca8c-e5a8-4836-b61d-aa5794eeb3f4`. Required if `appleApiKey` is specified.
* ... or keychain with keychainProfile:
* `keychain` String (optional) - The name of the keychain or path to the keychain you stored notarization credentials in. If omitted, iCloud keychain is used by default.
* `keychainProfile` String - The name of the profile you provided when storing notarization credentials.
* ... or Legacy
* `appBundleId` String - The app bundle identifier your Electron app is using. E.g. `com.github.electron`
* `ascProvider` String (optional) - Your [Team Short Name](#notes-on-your-team-short-name).
* There are two authentication methods available: user name with password:
* `appleId` String - The username of your apple developer account
* `appleIdPassword` String - The [app-specific password](https://support.apple.com/HT204397) (not your Apple ID password).
* ... or apiKey with apiIssuer:
* `appleApiKey` String - Required for JWT authentication. See Note on JWT authentication below.
* `appleApiIssuer` String - Issuer ID. Required if `appleApiKey` is specified.

## Safety when using `appleIdPassword`

1. Never hard code your password into your packaging scripts, use an environment
variable at a minimum.
2. It is possible to provide a keychain reference instead of your actual password (assuming that you have already logged into
the Application Loader from Xcode). For example:
`@electron/notarize` exposes a single `notarize` function that accepts the following parameters:
* `appPath` — the absolute path to your codesigned and packaged Electron application.
* `notarytoolPath` - String (optional) - Path to a custom notarytool binary ([more details](#custom-notarytool))
* additional options required for authenticating your Apple ID (see below)

```javascript
const password = `@keychain:"Application Loader: ${appleId}"`;
```
The method returns a void Promise once app notarization is complete. Please note that notarization may take
many minutes.

Another option is that you can add a new keychain item using either the Keychain Access app or from the command line using the `security` utility:
If the notarization process is unusually log for your application, see Apple Developer's docs to
[Avoid long notarization response times and size limits](https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution/customizing_the_notarization_workflow#3561440).

```bash
security add-generic-password -a "AC_USERNAME" -w <app_specific_password> -s "AC_PASSWORD"
```
where `AC_USERNAME` should be replaced with your Apple ID, and then in your code you can use:
### Usage with app-specific password

You can generate an [app-specific password](https://support.apple.com/en-us/102654) for your Apple ID
to notarize your Electron applications.

This method also requires you to specify the [Team ID](https://developer.apple.com/help/account/manage-your-team/locate-your-team-id/)
of the Developer Team you want to notarize under. An Apple ID may be part of multiple Teams.

```javascript
const password = `@keychain:AC_PASSWORD`;
import { notarize } from '@electron/notarize';

await notarize({
appPath,
appleId, // Login name of your Apple Developer account
appleIdPassword, // App-specific password
teamId, // Team ID for your developer team
});
```

## Notes on JWT authentication
> [!IMPORTANT]
> **Never hard code your app-specific password into your packaging scripts.** Use an environment
> variable at a minimum.
You can obtain an API key from [Appstore Connect](https://appstoreconnect.apple.com/access/api). Create a key with _App Manager_ access. Note down the Issuer ID and download the `.p8` file. This file is your API key and comes with the name of `AuthKey_<appleApiKeyId>.p8`. This is the string you have to supply when calling `notarize`.
### Usage with App Store Connect API key

Based on the `ApiKey`, the legacy `altool` will look in the following places for that file:
Alternatively, you can also authenticate via JSON Web Token (JWT) with App Store Connect.

* `./private_keys`
* `~/private_keys`
* `~/.private_keys`
* `~/.appstoreconnect/private_keys`
You can obtain an API key from [App Store Connect](https://appstoreconnect.apple.com/access/integrations/api).
Create a **Team Key** (not an _Individual Key_) with **App Manager** access.

`notarytool` will not look for the key, and you must instead provide its path as the `appleApiKey` argument.
Note down the Issuer ID (UUID format) and Key ID (10-character alphanumeric string),
and download the `.p8` API key file (`AuthKey_<appleApiKeyId>.p8`).
For security purposes, the private key can only be downloaded once.

## Notes on your Team Short Name
Provide the absolute path to your API key as the `appleApiKey` argument.

If you are a member of multiple teams or organizations, you have to tell Apple on behalf of which organization you're uploading. To find your [team's short name](https://forums.developer.apple.com/thread/113798)), you can ask `iTMSTransporter`, which is part of the now deprecated `Application Loader` as well as the newer [`Transporter`](https://apps.apple.com/us/app/transporter/id1450874784?mt=12).
```javascript
import { notarize } from '@electron/notarize';

With `Transporter` installed, run:
```sh
/Applications/Transporter.app/Contents/itms/bin/iTMSTransporter -m provider -u APPLE_DEV_ACCOUNT -p APP_PASSWORD
await notarize({
appPath,
appleApiKey, // Absolute path to API key (e.g. `/path/to/AuthKey_X0X0X0X0X0.p8`)
appleApiIssuer, // Issuer ID (e.g. `d5631714-a680-4b4b-8156-b4ed624c0845`)
});
```

Alternatively, with older versions of Xcode, run:
### Usage with Keychain credentials

As an alternative to passing authentication options, you can also store your authentication
credentials (for both API key and app-specific password strategies) in the macOS Keychain
via the `xcrun notarytool` command-line utility.

This method has the advantage of validating your notarization credentials before submitting
your application for notarization.

For example:

```sh
/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/itms/bin/iTMSTransporter -m provider -u APPLE_DEV_ACCOUNT -p APP_PASSWORD
# App-specific password strategy
xcrun notarytool store-credentials "my-app-password-profile"
--apple-id "<AppleID>"
--team-id <DeveloperTeamID>
--password <app_specific_password>
```

## Notes on your teamId
```sh
# App Store Connect API key strategy
xcrun notarytool store-credentials "my-api-key-profile"
--key "<PathToAPIKey>"
--key-id <KeyID>
--issuer <IssuerID>
```

If you use the new Notary Tool method with `appleId`/`appleIdPassword` you will need to set the `teamId` option. To get this ID, go to your [Apple Developer Account](https://developer.apple.com/account), then click on "Membership details", and there you will find your Team ID. This link should get you there directly: https://developer.apple.com/account#MembershipDetailsCard
Successful storage of your credentials will look like this:

## Debug
```
This process stores your credentials securely in the Keychain. You reference these credentials later using a profile name.
[`debug`](https://www.npmjs.com/package/debug) is used to display logs and messages. You can use `export DEBUG=electron-notarize*` to log additional debug information from this module.
Validating your credentials...
Success. Credentials validated.
Credentials saved to Keychain.
To use them, specify `--keychain-profile "my-api-key-profile"`
```

## Example Usage
After successfully storing your credentials, pass the keychain profile name into
the `keychainProfile` parameter.

```javascript
import { notarize } from '@electron/notarize';

async function packageTask () {
// Package your app here, and code sign with hardened runtime
await notarize({
appBundleId,
appPath,
appleId,
appleIdPassword,
ascProvider, // This parameter is optional
});
}
await notarize({
appPath,
keychainProfile,
});
```

### Custom notarytool

You can provide a path to a custom `notarytool`. This module allows this option to enable unique edge cases - but this use case is _explicitly unsupported_.

## Troubleshooting

### Debug logging

[`debug`](https://www.npmjs.com/package/debug) is used to display logs and messages.
Run your notarization scripts with the `DEBUG=electron-notarize*` environment variable to log additional
debug information from this module.

### Validating credentials

When notarizing your application, you may run into issues with validating your notarization
credentials.

```
Error: HTTP status code: 401. Invalid credentials. Username or password is incorrect.
Use the app-specific password generated at appleid.apple.com. Ensure that all authentication arguments are correct.
```

[Storing your credentials in Keychain](#usage-with-keychain-credentials) will validate your credentials before
even GitHub.

### Validating app notarization

To validate that notarization worked, you can use the `stapler` command-line utility:

```sh
stapler validate path/to/notarized.app
```

### Apple documentation

Apple also provides additional debugging documentation on
[Resolving common notarization issues](https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution/resolving_common_notarization_issues).
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"homepage": "https://github.com/electron/notarize#readme",
"repository": {
"type": "git",
"url": "https://github.com/electron/notarize.git"
"url": "git+https://github.com/electron/notarize.git"
},
"bugs": {
"url": "https://github.com/electron/notarize/issues"
Expand All @@ -35,7 +35,9 @@
"jest": "^29.0.0",
"prettier": "^1.18.2",
"ts-jest": "^29.0.0",
"typescript": "^4.8.4"
"typedoc": "~0.25.13",
"typedoc-plugin-missing-exports": "^2.2.0",
"typescript": "4.9.3"
},
"dependencies": {
"debug": "^4.1.1",
Expand Down
8 changes: 4 additions & 4 deletions src/check-signature.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import * as path from 'path';

import { spawn } from './spawn';
import { NotarizeStapleOptions } from './types';
import { NotaryToolNotarizeAppOptions } from './types';
import debug from 'debug';
const d = debug('electron-notarize');

const codesignDisplay = async (opts: NotarizeStapleOptions) => {
const codesignDisplay = async (opts: NotaryToolNotarizeAppOptions) => {
const result = await spawn('codesign', ['-dv', '-vvvv', '--deep', opts.appPath]);
return result;
};

const codesign = async (opts: NotarizeStapleOptions) => {
const codesign = async (opts: NotaryToolNotarizeAppOptions) => {
d('attempting to check codesign of app:', opts.appPath);
const result = await spawn('codesign', ['-vvv', '--deep', '--strict', opts.appPath]);
return result;
};
export async function checkSignatures(opts: NotarizeStapleOptions): Promise<void> {
export async function checkSignatures(opts: NotaryToolNotarizeAppOptions): Promise<void> {
const fileExt = path.extname(opts.appPath);
if (fileExt === '.dmg' || fileExt === '.pkg') {
d('skipping codesign check for dmg or pkg file');
Expand Down
Loading

0 comments on commit ccb3a69

Please sign in to comment.