Skip to content

Commit

Permalink
fix: remove now-unusable "legacy" notarization (#187)
Browse files Browse the repository at this point in the history
* Remove now-unusable "legacy" notarization

* Make `notarize` function overloaded

* Update docs more
  • Loading branch information
rotu authored May 14, 2024
1 parent ba33a3d commit f48a181
Show file tree
Hide file tree
Showing 5 changed files with 59 additions and 203 deletions.
51 changes: 9 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Apple has made this a hard requirement as of 10.15 (Catalina).

For notarization, you need the following things:

1. Xcode 10 or later installed on your Mac.
1. Xcode 13 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:
Expand All @@ -44,29 +44,20 @@ For notarization, you need the following things:
### 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.
* `tool` String - The notarization tool to use, default is `notarytool`. Previously, the value `legacy` used `altool`, which [**stopped working** on November 1st 2023](https://developer.apple.com/news/?id=y5mjxqmn).
* `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
* 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.
* `teamId` String - The [team ID](https://developer.apple.com/help/account/manage-your-team/locate-your-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`

Expand All @@ -92,34 +83,11 @@ const password = `@keychain:AC_PASSWORD`;

## Notes on JWT authentication

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`.

Based on the `ApiKey`, the legacy `altool` will look in the following places for that file:

* `./private_keys`
* `~/private_keys`
* `~/.private_keys`
* `~/.appstoreconnect/private_keys`

`notarytool` will not look for the key, and you must instead provide its path as the `appleApiKey` argument.

## Notes on your Team Short Name

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).

With `Transporter` installed, run:
```sh
/Applications/Transporter.app/Contents/itms/bin/iTMSTransporter -m provider -u APPLE_DEV_ACCOUNT -p APP_PASSWORD
```

Alternatively, with older versions of Xcode, run:
```sh
/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/itms/bin/iTMSTransporter -m provider -u APPLE_DEV_ACCOUNT -p APP_PASSWORD
```
You can obtain an API key from [App Store Connect](https://appstoreconnect.apple.com/access/api). Create a _Team Key_ (not an _Individual 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`. Provide the path to this file as the `appleApiKey` argument.

## Notes on your teamId

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
To get your `teamId` value, go to your [Apple Developer Account](https://developer.apple.com/account), then click on "Membership details", and there you will find your Team ID.

## Debug

Expand All @@ -133,11 +101,10 @@ 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
teamId,
});
}
```
65 changes: 27 additions & 38 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,58 +2,47 @@ import debug from 'debug';
import retry from 'promise-retry';

import { checkSignatures } from './check-signature';
import { delay } from './helpers';
import { startLegacyNotarize, waitForLegacyNotarize } from './legacy';
import { isNotaryToolAvailable, notarizeAndWaitForNotaryTool } from './notarytool';
import { stapleApp } from './staple';
import { NotarizeOptions, NotaryToolStartOptions } from './types';
import {
NotarizeOptions,
NotaryToolStartOptions,
NotarizeOptionsLegacy,
NotarizeOptionsNotaryTool,
} from './types';

const d = debug('electron-notarize');

export { NotarizeOptions };

export { validateLegacyAuthorizationArgs as validateAuthorizationArgs } from './validate-args';
export { validateNotaryToolAuthorizationArgs as validateAuthorizationArgs } from './validate-args';

export async function notarize({ appPath, ...otherOptions }: NotarizeOptions) {
await checkSignatures({ appPath });
async function notarize(args: NotarizeOptionsNotaryTool): Promise<void>;
/** @deprecated */
async function notarize(args: NotarizeOptionsLegacy): Promise<void>;

async function notarize({ appPath, ...otherOptions }: NotarizeOptions) {
if (otherOptions.tool === 'legacy') {
console.warn(
'Notarizing using the legacy altool system. The altool system will be disabled on November 1 2023. Please switch to the notarytool system before then.',
);
console.warn(
'You can do this by setting "tool: notarytool" in your "@electron/notarize" options. Please note that the credentials options may be slightly different between tools.',
throw new Error(
'Notarization with the legacy altool system was decommisioned as of November 2023',
);
d('notarizing using the legacy notarization system, this will be slow');
const { uuid } = await startLegacyNotarize({
appPath,
...otherOptions,
});
/**
* Wait for Apples API to initialize the status UUID
*
* If we start checking too quickly the UUID is not ready yet
* and this step will fail. It takes Apple a number of minutes
* to actually complete the job so an extra 10 second delay here
* is not a big deal
*/
d('notarization started, waiting for 10 seconds before pinging Apple for status');
await delay(10000);
d('starting to poll for notarization status');
await waitForLegacyNotarize({ uuid, ...otherOptions });
} else {
d('notarizing using the new notarytool system');
if (!(await isNotaryToolAvailable())) {
throw new Error('notarytool is not available, you must be on at least Xcode 13');
}

await notarizeAndWaitForNotaryTool({
appPath,
...otherOptions,
} as NotaryToolStartOptions);
}

await checkSignatures({ appPath });

d('notarizing using notarytool');
if (!(await isNotaryToolAvailable())) {
throw new Error('notarytool is not available, you must be on at least Xcode 13');
}

await notarizeAndWaitForNotaryTool({
appPath,
...otherOptions,
} as NotaryToolStartOptions);

await retry(() => stapleApp({ appPath }), {
retries: 3,
});
}

export { notarize };
129 changes: 9 additions & 120 deletions src/legacy.ts
Original file line number Diff line number Diff line change
@@ -1,129 +1,18 @@
import debug from 'debug';
import * as path from 'path';

import { spawn } from './spawn';
import { withTempDir, makeSecret, parseNotarizationInfo, delay } from './helpers';
import { validateLegacyAuthorizationArgs, isLegacyPasswordCredentials } from './validate-args';
import {
NotarizeResult,
LegacyNotarizeStartOptions,
LegacyNotarizeWaitOptions,
LegacyNotarizeCredentials,
} from './types';
import { LegacyNotarizeStartOptions, LegacyNotarizeWaitOptions } from './types';

const d = debug('electron-notarize:legacy');

function authorizationArgs(rawOpts: LegacyNotarizeCredentials): string[] {
const opts = validateLegacyAuthorizationArgs(rawOpts);
if (isLegacyPasswordCredentials(opts)) {
return ['-u', makeSecret(opts.appleId), '-p', makeSecret(opts.appleIdPassword)];
} else {
return [
'--apiKey',
makeSecret(opts.appleApiKey),
'--apiIssuer',
makeSecret(opts.appleApiIssuer),
];
}
}

export async function startLegacyNotarize(
opts: LegacyNotarizeStartOptions,
): Promise<NotarizeResult> {
/** @deprecated */
export async function startLegacyNotarize(opts: LegacyNotarizeStartOptions): Promise<never> {
d('starting notarize process for app:', opts.appPath);
return await withTempDir<NotarizeResult>(async dir => {
const zipPath = path.resolve(dir, `${path.basename(opts.appPath, '.app')}.zip`);
d('zipping application to:', zipPath);
const zipResult = await spawn(
'ditto',
['-c', '-k', '--sequesterRsrc', '--keepParent', path.basename(opts.appPath), zipPath],
{
cwd: path.dirname(opts.appPath),
},
);
if (zipResult.code !== 0) {
throw new Error(
`Failed to zip application, exited with code: ${zipResult.code}\n\n${zipResult.output}`,
);
}
d('zip succeeded, attempting to upload to Apple');

const notarizeArgs = [
'altool',
'--notarize-app',
'-f',
zipPath,
'--primary-bundle-id',
opts.appBundleId,
...authorizationArgs(opts),
];

if (opts.ascProvider) {
notarizeArgs.push('-itc_provider', opts.ascProvider);
}

const result = await spawn('xcrun', notarizeArgs);
if (result.code !== 0) {
throw new Error(`Failed to upload app to Apple's notarization servers\n\n${result.output}`);
}
d('upload success');

const uuidMatch = /\nRequestUUID = (.+?)\n/g.exec(result.output);
if (!uuidMatch) {
throw new Error(`Failed to find request UUID in output:\n\n${result.output}`);
}

d('found UUID:', uuidMatch[1]);

return {
uuid: uuidMatch[1],
};
});
throw new Error('Cannot start notarization. Legacy notarization (altool) is no longer available');
}

export async function waitForLegacyNotarize(opts: LegacyNotarizeWaitOptions): Promise<void> {
d('checking notarization status:', opts.uuid);
const result = await spawn('xcrun', [
'altool',
'--notarization-info',
opts.uuid,
...authorizationArgs(opts),
]);
if (result.code !== 0) {
// These checks could fail for all sorts of reasons, including:
// * The status of a request isn't available as soon as the upload request returns, so
// it may just not be ready yet.
// * If using keychain password, user's mac went to sleep and keychain locked.
// * Regular old connectivity failure.
d(
`Failed to check status of notarization request, retrying in 30 seconds: ${opts.uuid}\n\n${result.output}`,
);
await delay(30000);
return waitForLegacyNotarize(opts);
}
const notarizationInfo = parseNotarizationInfo(result.output);

if (notarizationInfo.status === 'in progress') {
d('still in progress, waiting 30 seconds');
await delay(30000);
return waitForLegacyNotarize(opts);
}

d('notarzation done with info:', notarizationInfo);

if (notarizationInfo.status === 'invalid') {
d('notarization failed');
throw new Error(`Apple failed to notarize your application, check the logs for more info
Status Code: ${notarizationInfo.statusCode || 'No Code'}
Message: ${notarizationInfo.statusMessage || 'No Message'}
Logs: ${notarizationInfo.logFileUrl}`);
}

if (notarizationInfo.status !== 'success') {
throw new Error(`Unrecognized notarization status: "${notarizationInfo.status}"`);
}

d('notarization was successful');
return;
/** @deprecated */
export async function waitForLegacyNotarize(opts: LegacyNotarizeWaitOptions): Promise<never> {
throw new Error(
'Cannot wait for notarization. Legacy notarization (altool) is no longer available',
);
}
14 changes: 11 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/** @deprecated */
export interface LegacyNotarizePasswordCredentials {
appleId: string;
appleIdPassword: string;
Expand All @@ -9,6 +10,7 @@ export interface NotaryToolPasswordCredentials {
teamId: string;
}

/** @deprecated */
export interface LegacyNotarizeApiKeyCredentials {
appleApiKey: string;
appleApiIssuer: string;
Expand All @@ -25,6 +27,7 @@ export interface NotaryToolKeychainCredentials {
keychain?: string;
}

/** @deprecated */
export type LegacyNotarizeCredentials =
| LegacyNotarizePasswordCredentials
| LegacyNotarizeApiKeyCredentials;
Expand All @@ -34,6 +37,7 @@ export type NotaryToolCredentials =
| NotaryToolKeychainCredentials;
export type NotarizeCredentials = LegacyNotarizeCredentials | NotaryToolCredentials;

/** @deprecated */
export interface LegacyNotarizeAppOptions {
appPath: string;
appBundleId: string;
Expand All @@ -51,12 +55,16 @@ export interface NotarizeResult {
uuid: string;
}

/** @deprecated */
export type LegacyNotarizeStartOptions = LegacyNotarizeAppOptions &
LegacyNotarizeCredentials &
TransporterOptions;
export type NotaryToolStartOptions = NotaryToolNotarizeAppOptions & NotaryToolCredentials;
/** @deprecated */
export type LegacyNotarizeWaitOptions = NotarizeResult & LegacyNotarizeCredentials;
export type NotarizeStapleOptions = Pick<LegacyNotarizeAppOptions, 'appPath'>;
export type NotarizeOptions =
| ({ tool?: 'legacy' } & LegacyNotarizeStartOptions)
| ({ tool: 'notarytool' } & NotaryToolStartOptions);

/** @deprecated */
export type NotarizeOptionsLegacy = { tool: 'legacy' } & LegacyNotarizeStartOptions;
export type NotarizeOptionsNotaryTool = { tool?: 'notarytool' } & NotaryToolStartOptions;
export type NotarizeOptions = NotarizeOptionsLegacy | NotarizeOptionsNotaryTool;
3 changes: 3 additions & 0 deletions src/validate-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,23 @@ import {
NotaryToolPasswordCredentials,
} from './types';

/** @deprecated */
export function isLegacyPasswordCredentials(
opts: LegacyNotarizeCredentials,
): opts is LegacyNotarizePasswordCredentials {
const creds = opts as LegacyNotarizePasswordCredentials;
return creds.appleId !== undefined || creds.appleIdPassword !== undefined;
}

/** @deprecated */
export function isLegacyApiKeyCredentials(
opts: LegacyNotarizeCredentials,
): opts is LegacyNotarizeApiKeyCredentials {
const creds = opts as LegacyNotarizeApiKeyCredentials;
return creds.appleApiKey !== undefined || creds.appleApiIssuer !== undefined;
}

/** @deprecated */
export function validateLegacyAuthorizationArgs(
opts: LegacyNotarizeCredentials,
): LegacyNotarizeCredentials {
Expand Down

0 comments on commit f48a181

Please sign in to comment.