Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: remove now-unusable "legacy" notarization #187

Merged
merged 3 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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