diff --git a/.circleci/config.yml b/.circleci/config.yml index 24fe422..4b4c379 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -28,21 +28,21 @@ version: 2 jobs: test-mac-16: macos: - xcode: "11.0.0" + xcode: "11.6.0" environment: NODE_VERSION: "16" <<: *steps-test test-mac-14: macos: - xcode: "11.0.0" + xcode: "11.6.0" environment: NODE_VERSION: "14" <<: *steps-test test-mac-12: macos: - xcode: "11.0.0" + xcode: "11.6.0" environment: NODE_VERSION: "12" <<: *steps-test diff --git a/README.md b/README.md index 12e945f..bc4d203 100644 --- a/README.md +++ b/README.md @@ -77,10 +77,7 @@ The examples below assume that `--pre-auto-entitlements` is enabled. electron-osx-sign path/to/my.app --provisioning-profile=path/to/my.provisionprofile ``` -- To specify the entitlements file: - ```sh - electron-osx-sign path/to/my.app --entitlements=path/to/my.entitlements - ``` +- To specify custom entitlements files you have to use the JS API. - It is recommended to make use of `--version` while signing legacy versions of Electron: ```sh @@ -151,34 +148,21 @@ Needs file extension `.app`. Path to additional binaries that will be signed along with built-ins of Electron. Default to `undefined`. -`entitlements` - *String* +`optionsForFile` - *Function* -Path to entitlements file for signing the app. -Default to built-in entitlements file, Sandbox enabled for Mac App Store platform. -See [default.entitlements.mas.plist](https://github.com/electron-userland/electron-osx-sign/blob/master/default.entitlements.mas.plist) or [default.entitlements.darwin.plist](https://github.com/electron-userland/electron-osx-sign/blob/master/default.entitlements.darwin.plist) with respect to your platform. +Function that receives the path to a file and can return the entitlements to use for that file to override the default behavior. The +object this function returns can include any of the following optional keys. -`entitlements-inherit` - *String* - -Path to child entitlements which inherit the security settings for signing frameworks and bundles of a distribution. *This option only applies when signing with entitlements.* -See [default.entitlements.mas.inherit.plist](https://github.com/electron-userland/electron-osx-sign/blob/master/default.entitlements.mas.inherit.plist) or [default.entitlements.darwin.inherit.plist](https://github.com/electron-userland/electron-osx-sign/blob/master/default.entitlements.darwin.inherit.plist) with respect to your platform. - -`entitlements-loginhelper` - *String* - -Path to login helper entitlement file. When using App Sandbox, the inherited entitlement should not be used since this is a standalone executable. *This option only applies when signing with entitlements.* -Default to the same entitlements file used for signing the app bundle. - -`entitlementsForFile` - *Function* - -Function that receives the path to a file and the current codesign arguments as parameters. If you wish to override the entitlements used for this file path this function should return the absolute path to a different entitlements file. +| Option | Description | Usage Example | +|-------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------| +| `entitlements` | String specifying the path to an `entitlements.plist` file. Will default to built-in entitlements files. Can also be an array of entitlement keys that osx-sign will write to an entitlements file for you. | `'path/to/entitlements'` | +| `hardenedRuntime` | Boolean flag to enable the Hardened Runtime when signing the app. Enabled by default. | `false` | +| `requirements` | String specifying the [requirements](https://developer.apple.com/library/mac/documentation/Security/Conceptual/CodeSigningGuide/RequirementLang/RequirementLang.html) that you recommend to be used to evaluate the code signature. | `'anchor apple or anchor = "/var/db/yourcorporateanchor.cert"'` | +| `signatureFlags` | List of [code signature flags](https://developer.apple.com/documentation/security/seccodesignatureflags?language=objc). Accepts an array of strings or a comma-separated string. | `['kSecCodeSignatureRestrict']` | +| `timestamp` | String specifying the URL of the timestamp authority server. Defaults to the server provided by Apple. Please note that this default server may not support signatures not furnished by Apple. Disable the timestamp service with `none`. | `'https://different.timeserver'` | **Note:** Only available via the JS API -`gatekeeper-assess` - *Boolean* - -Flag to enable/disable Gatekeeper assessment after signing the app. Disabling it is useful for signing with self-signed certificates. -Gatekeeper assessment is enabled by default on `darwin` platform. -Default to `true`. - `hardenedRuntime` or `hardened-runtime` - *Boolean* Flag to enable the Mojave hardened runtime when signing the app. Disabled by default, requires Xcode >= 10 and @@ -191,7 +175,7 @@ Default to be selected with respect to `provisioning-profile` and `platform` fro Signing platform `mas` will look for `3rd Party Mac Developer Application: * (*)`, and platform `darwin` will look for `Developer ID Application: * (*)` by default. -`identity-validation` - *Boolean* +`identityValidation` - *Boolean* Flag to enable/disable validation for the signing identity. If enabled, the `identity` provided will be validated in the `keychain` specified. Default to `true`. @@ -213,49 +197,27 @@ Build platform of Electron. Allowed values: `darwin`, `mas`. Default to auto detect by presence of `Squirrel.framework` within the application bundle. -`pre-auto-entitlements` - *Boolean* +`preAutoEntitlements` - *Boolean* Flag to enable/disable automation of `com.apple.security.application-groups` in entitlements file and update `Info.plist` with `ElectronTeamID`. Default to `true`. -`pre-embed-provisioning-profile` - *Boolean* +`preEmbedProvisioningProfile` - *Boolean* Flag to enable/disable embedding of provisioning profile in the current working directory. Default to `true`. -`provisioning-profile` - *String* +`provisioningProfile` - *String* Path to provisioning profile. -`requirements` - *String* - -Specify the criteria that you recommend to be used to evaluate the code signature. -See more info from https://developer.apple.com/library/mac/documentation/Security/Conceptual/CodeSigningGuide/RequirementLang/RequirementLang.html -Default to `undefined`. - -`restrict` - *Boolean* - -**To be deprecated, see `signature-flags`.** -Restrict dyld loading. See doc about this [code signature flag](https://developer.apple.com/documentation/security/seccodesignatureflags/kseccodesignaturerestrict?language=objc) for more details. Disabled by default. - -`signature-flags` - *String* -Comma separated string or array for [code signature flag](https://developer.apple.com/documentation/security/seccodesignatureflags?language=objc). Default to `undefined`. - -`signature-size` - *Number* -Provide a value to be passed to `codesign` along with the `--signature-size` flag, to work around the *signature too large to embed* issue. A value of `12000` should do it - see the [FAQ](https://github.com/electron/electron-osx-sign/wiki/FAQ) for details. Default to `undefined`. - -`strict-verify` - *Boolean|String|Array.* +`strictVerify` - *Boolean|String|Array.* Flag to enable/disable `--strict` flag when verifying the signed application bundle. If provided as a string, each component should be separated with comma (`,`). If provided as an array, each item should be a string corresponding to a component. Default to `true`. -`timestamp` - *String* - -Specify the URL of the timestamp authority server, default to server provided by Apple. Please note that this default server may not support signatures not furnished by Apple. -Disable the timestamp service with `none`. - `type` - *String* Specify whether to sign app for development or for distribution. @@ -354,7 +316,7 @@ Default to be selected with respect to `platform` from `keychain` or keychain by Flattening platform `mas` will look for `3rd Party Mac Developer Installer: * (*)`, and platform `darwin` will look for `Developer ID Installer: * (*)` by default. -`identity-validation` - *Boolean* +`identityValidation` - *Boolean* Flag to enable/disable validation for signing identity. If enabled, the `identity` provided will be validated in the `keychain` specified. Default to `true`. diff --git a/bin/electron-osx-flat-usage.txt b/bin/electron-osx-flat-usage.txt index e9071ff..368ce2c 100644 --- a/bin/electron-osx-flat-usage.txt +++ b/bin/electron-osx-flat-usage.txt @@ -17,7 +17,7 @@ DESCRIPTION Name of certificate to use when signing. Default to selected with respect to --platform from --keychain specified or keychain by system default. - --identity-validation, --no-identity-validation + --identityValidation, --no-identityValidation Flag to enable/disable validation for the signing identity. --install=install-path diff --git a/bin/electron-osx-sign-usage.txt b/bin/electron-osx-sign-usage.txt index 1d61018..55bd8cc 100644 --- a/bin/electron-osx-sign-usage.txt +++ b/bin/electron-osx-sign-usage.txt @@ -13,26 +13,6 @@ DESCRIPTION embedded-binary ... Path to additional binaries that will be signed along with built-ins of Electron, spaced. - --entitlements=file - Path to entitlements file for signing the app. - Default to built-in entitlements file, Sandbox enabled for Mac App Store platform. - - --entitlements-inherit=file - Path to child entitlements which inherit the security settings for signing frameworks and bundles of a distribution. - This option only applies when signing with entitlements. - - --entitlements-loginhelper=file - Path to login helper entitlement file. When using App Sandbox, the inherited entitlement should not be used since this is a standalone executable. - This option only applies when signing with entitlements. - - --gatekeeper-assess, --no-gatekeeper-assess - Flag to enable/disable Gatekeeper assessment after signing the app. Disabling it is useful for signing with self-signed certificates. - Gatekeeper assessment is enabled by default on ``darwin'' platform. - - --hardened-runtime - Flag to enable the Mojave hardened runtime when signing the app. Disabled by default, requires Xcode >= 10 and macOS - >= 10.13.6. - --help Flag to display all commands. @@ -40,7 +20,7 @@ DESCRIPTION Name of certificate to use when signing. Default to selected with respect to --provisioning-profile and --platform from --keychain specified or keychain by system default. - --identity-validation, --no-identity-validation + --identityValidation, --no-identityValidation Flag to enable/disable validation for the signing identity. --ignore=path @@ -64,28 +44,11 @@ DESCRIPTION --provisioning-profile=file Path to provisioning profile. - --requirements=requirements - Specify the criteria that you recommend to be used to evaluate the code signature. - - --restrict - (This will be deprecated soon, see --sign-flags.) - Flag to enable restrict mode. Disabled by default. - - --signature-flags=flags - Code signature flags. Default to none. - - --signature-size=size - Signature size. Default to none. - - --strict-verify, --strict-verify=options, --no-strict-verify + --strictVerify, --strictVerify=options, --no-strictVerify Flag to enable/disable ``--strict'' flag when verifying the signed application bundle. Each component should be separated in ``options'' with comma (``,''). Enabled by default. - --timestamp=timestamp - Specify the URL of the timestamp authority server, default to server provided by Apple. - Disable the timestamp service with ``none''. - --type=type Specify whether to sign app for development or for distribution. Allowed values: ``development'', ``distribution''. diff --git a/bin/electron-osx-sign.js b/bin/electron-osx-sign.js index 247d7d3..95234db 100755 --- a/bin/electron-osx-sign.js +++ b/bin/electron-osx-sign.js @@ -6,21 +6,16 @@ const args = require('minimist')(process.argv.slice(2), { string: [ 'signature-flags' ], - number: [ - 'signature-size' - ], boolean: [ 'help', 'pre-auto-entitlements', 'pre-embed-provisioning-profile', - 'gatekeeper-assess', 'hardened-runtime', 'restrict' ], default: { 'pre-auto-entitlements': true, - 'pre-embed-provisioning-profile': true, - 'gatekeeper-assess': true + 'pre-embed-provisioning-profile': true } }); const usage = fs.readFileSync(path.join(__dirname, 'electron-osx-sign-usage.txt')).toString(); diff --git a/entitlements/default.entitlements.mas.plist b/entitlements/default.darwin.gpu.plist old mode 100755 new mode 100644 similarity index 81% rename from entitlements/default.entitlements.mas.plist rename to entitlements/default.darwin.gpu.plist index 8e31f75..446fe17 --- a/entitlements/default.entitlements.mas.plist +++ b/entitlements/default.darwin.gpu.plist @@ -2,7 +2,7 @@ - com.apple.security.app-sandbox + com.apple.security.cs.allow-jit diff --git a/entitlements/default.darwin.plist b/entitlements/default.darwin.plist new file mode 100644 index 0000000..c762cfb --- /dev/null +++ b/entitlements/default.darwin.plist @@ -0,0 +1,20 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.device.audio-input + + com.apple.security.device.bluetooth + + com.apple.security.device.camera + + com.apple.security.device.print + + com.apple.security.device.usb + + com.apple.security.personal-information.location + + + diff --git a/entitlements/default.entitlements.darwin.plist b/entitlements/default.darwin.plugin.plist similarity index 54% rename from entitlements/default.entitlements.darwin.plist rename to entitlements/default.darwin.plugin.plist index 1da3936..67ebe4e 100644 --- a/entitlements/default.entitlements.darwin.plist +++ b/entitlements/default.darwin.plugin.plist @@ -2,5 +2,9 @@ + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + diff --git a/entitlements/default.entitlements.darwin.inherit.plist b/entitlements/default.darwin.renderer.plist similarity index 76% rename from entitlements/default.entitlements.darwin.inherit.plist rename to entitlements/default.darwin.renderer.plist index 1da3936..446fe17 100644 --- a/entitlements/default.entitlements.darwin.inherit.plist +++ b/entitlements/default.darwin.renderer.plist @@ -2,5 +2,7 @@ + com.apple.security.cs.allow-jit + diff --git a/entitlements/default.entitlements.mas.inherit.plist b/entitlements/default.mas.child.plist similarity index 100% rename from entitlements/default.entitlements.mas.inherit.plist rename to entitlements/default.mas.child.plist diff --git a/entitlements/default.mas.plist b/entitlements/default.mas.plist new file mode 100755 index 0000000..025064d --- /dev/null +++ b/entitlements/default.mas.plist @@ -0,0 +1,22 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-write + + com.apple.security.files.bookmarks.app-scope + + com.apple.security.network.client + + com.apple.security.print + + com.apple.security.device.camera + + com.apple.security.device.microphone + + com.apple.security.device.usb + + + diff --git a/index.d.ts b/index.d.ts deleted file mode 100644 index ad8c5d6..0000000 --- a/index.d.ts +++ /dev/null @@ -1,43 +0,0 @@ -declare module "electron-osx-sign" { - interface BaseSignOptions { - app: string; - identity?: string; - platform?: string; - keychain?: string; - } - - interface SignOptions extends BaseSignOptions { - binaries?: string[]; - entitlements?: string; - 'entitlements-inherit'?: string; - 'entitlements-loginhelper'?: string; - 'gatekeeper-assess'?: boolean; - hardenedRuntime?: boolean; - 'identity-validation'?: boolean; - ignore?: string | ((file: string) => boolean); - 'pre-auto-entitlements'?: boolean; - 'pre-embed-provisioning-profile'?: boolean; - 'provisioning-profile'?: string; - 'requirements'?: string; - 'signature-flags'?: string | ((file: string) => string[]); - 'signature-size'?: number; - 'type'?: string; - version?: string; - entitlementsForFile?: (file: string, codeSignArgs: string[]) => string | null; - } - - export function sign(opts: SignOptions, callback: (error: Error) => void): void; - - export function signAsync(opts: SignOptions): Promise; - - interface FlatOptions extends BaseSignOptions { - 'identity-validation'?: boolean; - install?: string; - pkg?: string; - scripts?: string; - } - - export function flat(opts: FlatOptions, callback: (error: Error) => void): void; - - export function flatAsync(opts: FlatOptions): Promise; -} diff --git a/package.json b/package.json index 254b84b..90e655d 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,6 @@ ] }, "engines": { - "node": ">=4.0.0" + "node": ">=12.0.0" } } diff --git a/src/flat.ts b/src/flat.ts index 6a84ce6..0dfe373 100644 --- a/src/flat.ts +++ b/src/flat.ts @@ -77,7 +77,7 @@ export async function buildPkg (_opts: FlatOptions) { if (validatedOptions.identity) { debugLog('`identity` passed in arguments.'); - if (validatedOptions['identity-validation'] === false) { + if (validatedOptions.identityValidation === false) { // Do nothing } else { identities = await findIdentities(validatedOptions.keychain || null, validatedOptions.identity); diff --git a/src/sign.ts b/src/sign.ts index 47addb1..b30777f 100644 --- a/src/sign.ts +++ b/src/sign.ts @@ -1,5 +1,7 @@ +import * as fs from 'fs-extra'; import * as os from 'os'; import * as path from 'path'; +import * as plist from 'plist'; import compareVersion from 'compare-version'; import { @@ -12,12 +14,9 @@ import { walkAsync } from './util'; import { Identity, findIdentities } from './util-identities'; -import { - preEmbedProvisioningProfile, - getProvisioningProfile -} from './util-provisioning-profiles'; +import { preEmbedProvisioningProfile, getProvisioningProfile } from './util-provisioning-profiles'; import { preAutoEntitlements } from './util-entitlements'; -import { ElectronMacPlatform, SignOptions, ValidatedSignOptions } from './types'; +import { ElectronMacPlatform, PerFileSignOptions, SignOptions, ValidatedSignOptions } from './types'; const pkgVersion: string = require('../../package.json').version; @@ -41,85 +40,6 @@ function validateOptsIgnore (ignore: SignOptions['ignore']): ValidatedSignOption } } -function validateOptsEntitlements (opts: SignOptions, platform: ElectronMacPlatform) { - const entitlementOptions = { - entitlements: opts.entitlements, - 'entitlements-inherit': opts['entitlements-inherit'] - }; - - const entitlementsFolder = path.resolve(__dirname, '..', '..', 'entitlements'); - - if (platform === 'mas') { - // To sign apps for Mac App Store, an entitlements file is required, especially for app sandboxing (as well some other services). - // Fallback entitlements for sandboxing by default: Note this may cause troubles while running an signed app due to missing keys special to the project. - // Further reading: https://developer.apple.com/library/mac/documentation/Miscellaneous/Reference/EntitlementKeyReference/Chapters/EnablingAppSandbox.html - if (!entitlementOptions.entitlements) { - const entitlementsPath = path.resolve(entitlementsFolder, 'default.entitlements.mas.plist'); - debugWarn( - 'No `entitlements` passed in arguments:', - '\n', - '* Sandbox entitlements are required for Mac App Store distribution, your codesign entitlements file is default to:', - entitlementsPath - ); - entitlementOptions.entitlements = entitlementsPath; - } - if (!entitlementOptions['entitlements-inherit']) { - const entitlementsPath = path.join( - entitlementsFolder, - 'default.entitlements.mas.inherit.plist' - ); - debugWarn( - 'No `entitlements-inherit` passed in arguments:', - '\n', - '* Sandbox entitlements file for enclosed app files is default to:', - entitlementsPath - ); - entitlementOptions['entitlements-inherit'] = entitlementsPath; - } - } else { - // Not necessary to have entitlements for non Mac App Store distribution - if (!opts.entitlements) { - debugWarn( - 'No `entitlements` passed in arguments:', - '\n', - '* Provide `entitlements` to specify entitlements file for codesign.' - ); - } else { - // If entitlements is provided as a boolean flag, fallback to default - if ((entitlementOptions.entitlements as any) === true) { - const entitlementsPath = path.join(entitlementsFolder, 'default.entitlements.darwin.plist'); - debugWarn( - '`entitlements` not specified in arguments:', - '\n', - '* Provide `entitlements` to specify entitlements file for codesign.', - '\n', - '* Entitlements file is default to:', - entitlementsPath - ); - entitlementOptions.entitlements = entitlementsPath; - } - if (!opts['entitlements-inherit']) { - const entitlementsPath = path.join( - entitlementsFolder, - 'default.entitlements.darwin.inherit.plist' - ); - debugWarn( - 'No `entitlements-inherit` passed in arguments:', - '\n', - '* Entitlements file for enclosed app files is default to:', - entitlementsPath - ); - entitlementOptions['entitlements-inherit'] = entitlementsPath; - } - } - } - - return entitlementOptions as { - entitlements: string; - 'entitlements-inherit': string; - }; -} - /** * This function returns a promise validating all options passed in opts. */ @@ -127,7 +47,7 @@ async function validateSignOpts (opts: SignOptions): Promise= 0 // Strict flag since darwin 15.0.0 --> OS X 10.11.0 El Capitan + opts.strictVerify !== false && compareVersion(osRelease, '15.0.0') >= 0 // Strict flag since darwin 15.0.0 --> OS X 10.11.0 El Capitan ? [ '--strict' + - (opts['strict-verify'] - ? '=' + opts['strict-verify'] // Array should be converted to a comma separated string + (opts.strictVerify + ? '=' + opts.strictVerify // Array should be converted to a comma separated string : '') ] : [], ['--verbose=2', opts.app] ) ); +} - // Additionally test Gatekeeper acceptance for darwin platform - if (opts.platform === 'darwin' && opts['gatekeeper-assess'] !== false) { - debugLog('Verifying Gatekeeper acceptance for darwin platform...'); - await execFileAsync('spctl', [ - '--assess', - '--type', - 'execute', - '--verbose', - '--ignore-cache', - '--no-cache', - opts.app - ]); +function defaultOptionsForFile (filePath: string, platform: ElectronMacPlatform) { + const entitlementsFolder = path.resolve(__dirname, '..', '..', 'entitlements'); + + let entitlementsFile: string; + if (platform === 'darwin') { + // Default Entitlements + // c.f. https://source.chromium.org/chromium/chromium/src/+/main:chrome/app/app-entitlements.plist + // Also include JIT for main process V8 + entitlementsFile = path.resolve(entitlementsFolder, 'default.darwin.plist'); + // Plugin helper + // c.f. https://source.chromium.org/chromium/chromium/src/+/main:chrome/app/helper-plugin-entitlements.plist + if (filePath.includes('(Plugin).app')) { + entitlementsFile = path.resolve(entitlementsFolder, 'default.darwin.plugin.plist'); + // GPU Helper + // c.f. https://source.chromium.org/chromium/chromium/src/+/main:chrome/app/helper-gpu-entitlements.plist + } else if (filePath.includes('(GPU).app')) { + entitlementsFile = path.resolve(entitlementsFolder, 'default.darwin.gpu.plist'); + // Renderer Helper + // c.f. https://source.chromium.org/chromium/chromium/src/+/main:chrome/app/helper-renderer-entitlements.plist + } else if (filePath.includes('(Renderer).app')) { + entitlementsFile = path.resolve(entitlementsFolder, 'default.darwin.renderer.plist'); + } + } else { + // Default entitlements + // TODO: Can these be more scoped like the non-mas variant? + entitlementsFile = path.resolve(entitlementsFolder, 'default.mas.plist'); + + // If it is not the top level app bundle, we sign with inherit + if (filePath.includes('.app/')) { + entitlementsFile = path.resolve(entitlementsFolder, 'default.mas.child.plist'); + } } + + return { + entitlements: entitlementsFile, + hardenedRuntime: true, + requirements: undefined as string | undefined, + signatureFlags: undefined as string | string[] | undefined, + timestamp: undefined as string | undefined + }; +} + +async function mergeOptionsForFile ( + opts: PerFileSignOptions | null, + defaults: ReturnType +) { + const mergedPerFileOptions = { ...defaults }; + if (opts) { + if (opts.entitlements !== undefined) { + if (Array.isArray(opts.entitlements)) { + const entitlements = opts.entitlements.reduce>((dict, entitlementKey) => ({ + ...dict, + [entitlementKey]: true + }), {}); + const dir = await fs.mkdtemp(path.resolve(os.tmpdir(), 'tmp-entitlements-')); + const entitlementsPath = path.join(dir, 'entitlements.plist'); + await fs.writeFile(entitlementsPath, plist.build(entitlements), 'utf8'); + opts.entitlements = entitlementsPath; + } + mergedPerFileOptions.entitlements = opts.entitlements; + } + if (opts.hardenedRuntime !== undefined) { + mergedPerFileOptions.hardenedRuntime = opts.hardenedRuntime; + } + if (opts.requirements !== undefined) mergedPerFileOptions.requirements = opts.requirements; + if (opts.signatureFlags !== undefined) { + mergedPerFileOptions.signatureFlags = opts.signatureFlags; + } + if (opts.timestamp !== undefined) mergedPerFileOptions.timestamp = opts.timestamp; + } + return mergedPerFileOptions; } /** @@ -207,23 +185,6 @@ async function signApplication (opts: ValidatedSignOptions, identity: Identity) if (opts.keychain) { args.push('--keychain', opts.keychain); } - if (opts.requirements) { - args.push('--requirements', opts.requirements); - } - if (opts.timestamp) { - args.push('--timestamp=' + opts.timestamp); - } else { - args.push('--timestamp'); - } - if (opts['signature-size']) { - if (Number.isInteger(opts['signature-size']) && opts['signature-size'] > 0) { - args.push('--signature-size', `${opts['signature-size']}`); - } else { - debugWarn( - `Invalid value provided for --signature-size (${opts['signature-size']}). Must be a positive integer.` - ); - } - } /** * Sort the child paths by how deep they are in the file tree. Some arcane apple @@ -235,105 +196,90 @@ async function signApplication (opts: ValidatedSignOptions, identity: Identity) const bDepth = b.split(path.sep).length; return bDepth - aDepth; }); - if (opts.entitlements) { - // Sign with entitlements - // promise = Promise.mapSeries(childPaths, function (filePath) { - for (const filePath of children) { - if (shouldIgnoreFilePath(filePath)) { - debugLog('Skipped... ' + filePath); - continue; - } - debugLog('Signing... ' + filePath); - - let optionsArguments: string[] = []; - - if (opts['signature-flags']) { - if (Array.isArray(opts['signature-flags'])) { - optionsArguments.push(...opts['signature-flags']); - } else if (typeof opts['signature-flags'] === 'function') { - const flags = opts['signature-flags'](filePath); - optionsArguments.push(...flags); - } else { - const flags = opts['signature-flags'].split(',').map(function (flag) { - return flag.trim(); - }); - optionsArguments.push(...flags); - } - } - - if ( - opts.hardenedRuntime || - opts['hardened-runtime'] || - optionsArguments.includes('runtime') - ) { - // Hardened runtime since darwin 17.7.0 --> macOS 10.13.6 - if (compareVersion(osRelease, '17.7.0') >= 0) { - optionsArguments.push('runtime'); - } else { - // Remove runtime if passed in with --signature-flags - debugLog( - 'Not enabling hardened runtime, current macOS version too low, requires 10.13.6 and higher' - ); - optionsArguments = optionsArguments.filter((arg) => { - return arg !== 'runtime'; - }); - } - } - - if (opts.restrict) { - optionsArguments.push('restrict'); - debugWarn( - 'This flag is to be deprecated, consider using --signature-flags=restrict instead' - ); - } - - if (optionsArguments.length) { - args.push('--options', [...new Set(optionsArguments)].join(',')); - } - - let entitlementsFile = opts['entitlements-inherit']; - if (filePath.includes('Library/LoginItems')) { - entitlementsFile = opts['entitlements-loginhelper']!; - } - - const clonedArgs = args.concat([]); - if (opts.entitlementsForFile) { - entitlementsFile = opts.entitlementsForFile(filePath, clonedArgs) || entitlementsFile; - } - - await execFileAsync( - 'codesign', - clonedArgs.concat('--entitlements', entitlementsFile, filePath) - ); + for (const filePath of [...children, opts.app]) { + if (shouldIgnoreFilePath(filePath)) { + debugLog('Skipped... ' + filePath); + continue; } - // Sign the actual app now - debugLog('Signing... ' + opts.app); + const perFileOptions = await mergeOptionsForFile( + opts.optionsForFile ? opts.optionsForFile(filePath) : null, + defaultOptionsForFile(filePath, opts.platform) + ); - const clonedArgs = args.concat([]); - let entitlementsFile = opts.entitlements; - if (opts.entitlementsForFile) { - entitlementsFile = opts.entitlementsForFile(opts.app, clonedArgs) || entitlementsFile; + if (opts.preAutoEntitlements === false) { + debugWarn('Pre-sign operation disabled for entitlements automation.'); + } else { + debugLog( + 'Pre-sign operation enabled for entitlements automation with versions >= `1.1.1`:', + '\n', + '* Disable by setting `pre-auto-entitlements` to `false`.' + ); + if (!opts.version || compareVersion(opts.version, '1.1.1') >= 0) { + // Enable Mac App Store sandboxing without using temporary-exception, introduced in Electron v1.1.1. Relates to electron#5601 + const newEntitlements = await preAutoEntitlements(opts, perFileOptions, { + identity, + provisioningProfile: opts.provisioningProfile + ? await getProvisioningProfile(opts.provisioningProfile, opts.keychain) + : undefined + }); + + // preAutoEntitlements may provide us new entitlements, if so we update our options + // and ensure that entitlements-loginhelper has a correct default value + if (newEntitlements) { + perFileOptions.entitlements = newEntitlements; + } + } + } + + debugLog('Signing... ' + filePath); + + if (perFileOptions.requirements) { + args.push('--requirements', perFileOptions.requirements); + } + if (perFileOptions.timestamp) { + args.push('--timestamp=' + perFileOptions.timestamp); + } else { + args.push('--timestamp'); + } + + let optionsArguments: string[] = []; + + if (perFileOptions.signatureFlags) { + if (Array.isArray(perFileOptions.signatureFlags)) { + optionsArguments.push(...perFileOptions.signatureFlags); + } else { + const flags = perFileOptions.signatureFlags.split(',').map(function (flag) { + return flag.trim(); + }); + optionsArguments.push(...flags); + } + } + + if (perFileOptions.hardenedRuntime || optionsArguments.includes('runtime')) { + // Hardened runtime since darwin 17.7.0 --> macOS 10.13.6 + if (compareVersion(osRelease, '17.7.0') >= 0) { + optionsArguments.push('runtime'); + } else { + // Remove runtime if passed in with --signature-flags + debugLog( + 'Not enabling hardened runtime, current macOS version too low, requires 10.13.6 and higher' + ); + optionsArguments = optionsArguments.filter((arg) => { + return arg !== 'runtime'; + }); + } + } + + if (optionsArguments.length) { + args.push('--options', [...new Set(optionsArguments)].join(',')); } await execFileAsync( 'codesign', - clonedArgs.concat('--entitlements', entitlementsFile, opts.app) + args.concat('--entitlements', perFileOptions.entitlements, filePath) ); - } else { - // Otherwise normally - for (const filePath of children) { - if (shouldIgnoreFilePath(filePath)) { - debugLog('Skipped... ' + filePath); - continue; - } - - await execFileAsync('codesign', args.concat(filePath)); - } - - debugLog('Signing... ' + opts.app); - await execFileAsync('codesign', args.concat(opts.app)); } // Verify code sign @@ -342,17 +288,15 @@ async function signApplication (opts: ValidatedSignOptions, identity: Identity) debugLog('Verified.'); // Check entitlements if applicable - if (opts.entitlements) { - debugLog('Displaying entitlements...'); - const result = await execFileAsync('codesign', [ - '--display', - '--entitlements', - ':-', // Write to standard output and strip off the blob header - opts.app - ]); + debugLog('Displaying entitlements...'); + const result = await execFileAsync('codesign', [ + '--display', + '--entitlements', + ':-', // Write to standard output and strip off the blob header + opts.app + ]); - debugLog('Entitlements:', '\n', result); - } + debugLog('Entitlements:', '\n', result); } /** @@ -360,14 +304,14 @@ async function signApplication (opts: ValidatedSignOptions, identity: Identity) */ export async function signApp (_opts: SignOptions) { debugLog('electron-osx-sign@%s', pkgVersion); - let validatedOpts = await validateSignOpts(_opts); + const validatedOpts = await validateSignOpts(_opts); let identities: Identity[] = []; let identityInUse: Identity | null = null; // Determine identity for signing if (validatedOpts.identity) { debugLog('`identity` passed in arguments.'); - if (validatedOpts['identity-validation'] === false) { + if (validatedOpts.identityValidation === false) { identityInUse = new Identity(validatedOpts.identity); } else { identities = await findIdentities(validatedOpts.keychain || null, validatedOpts.identity); @@ -416,7 +360,7 @@ export async function signApp (_opts: SignOptions) { } // Pre-sign operations - if (validatedOpts['pre-embed-provisioning-profile'] === false) { + if (validatedOpts.preEmbedProvisioningProfile === false) { debugWarn( 'Pre-sign operation disabled for provisioning profile embedding:', '\n', @@ -430,50 +374,12 @@ export async function signApp (_opts: SignOptions) { ); await preEmbedProvisioningProfile( validatedOpts, - validatedOpts['provisioning-profile'] - ? await getProvisioningProfile( - validatedOpts['provisioning-profile'], - validatedOpts.keychain - ) + validatedOpts.provisioningProfile + ? await getProvisioningProfile(validatedOpts.provisioningProfile, validatedOpts.keychain) : null ); } - if (validatedOpts['pre-auto-entitlements'] === false) { - debugWarn('Pre-sign operation disabled for entitlements automation.'); - } else { - debugLog( - 'Pre-sign operation enabled for entitlements automation with versions >= `1.1.1`:', - '\n', - '* Disable by setting `pre-auto-entitlements` to `false`.' - ); - if ( - validatedOpts.entitlements && - (!validatedOpts.version || compareVersion(validatedOpts.version, '1.1.1') >= 0) - ) { - // Enable Mac App Store sandboxing without using temporary-exception, introduced in Electron v1.1.1. Relates to electron#5601 - const newEntitlements = await preAutoEntitlements(validatedOpts, { - identity: identityInUse, - provisioningProfile: validatedOpts['provisioning-profile'] - ? await getProvisioningProfile( - validatedOpts['provisioning-profile'], - validatedOpts.keychain - ) - : undefined - }); - - // preAutoEntitlements may provide us new entitlements, if so we update our options - // and ensure that entitlements-loginhelper has a correct default value - if (newEntitlements) { - validatedOpts = { - ...validatedOpts, - entitlements: newEntitlements, - 'entitlements-loginhelper': validatedOpts['entitlements-loginhelper'] || newEntitlements - }; - } - } - } - debugLog( 'Signing application...', '\n', @@ -483,15 +389,6 @@ export async function signApp (_opts: SignOptions) { '> Platform:', validatedOpts.platform, '\n', - '> Entitlements:', - validatedOpts.entitlements, - '\n', - '> Child entitlements:', - validatedOpts['entitlements-inherit'], - '\n', - '> Login helper entitlements:', - validatedOpts['entitlements-loginhelper'], - '\n', '> Additional binaries:', validatedOpts.binaries, '\n', diff --git a/src/types.ts b/src/types.ts index 03ce964..808ea1f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -12,42 +12,56 @@ type OnlyValidatedBaseSignOptions = { platform: ElectronMacPlatform; }; -type OnlySignOptions = { - binaries?: string[]; - entitlements?: string; - 'entitlements-inherit'?: string; - 'entitlements-loginhelper'?: string; - 'gatekeeper-assess'?: boolean; +/** + * Any missing options will use the default values, providing a partial + * structure will shallow merge with the default values. + */ +export type PerFileSignOptions = { + /** + * The entitlements file to use when signing this file + */ + entitlements?: string | string[]; + /** + * Whether to enable hardened runtime for this file. Enabled by default. + */ hardenedRuntime?: boolean; /** - * @deprecated use hardenedRuntime instead + * The designated requirements to embed when signing this file */ - ['hardened-runtime']?: boolean; - 'identity-validation'?: boolean; - ignore?: string | string[] | ((file: string) => boolean); - 'pre-auto-entitlements'?: boolean; - 'pre-embed-provisioning-profile'?: boolean; - 'provisioning-profile'?: string; requirements?: string; - restrict?: boolean; - 'signature-flags'?: string | ((file: string) => string[]); - 'signature-size'?: number; - 'strict-verify'?: boolean; + /** + * See --options of the "codesign" command. + * + * https://www.manpagez.com/man/1/codesign + */ + signatureFlags?: string | string[]; + /** + * The timestamp server to use when signing, by default uses the Apple provided + * timestamp server. + */ timestamp?: string; +} + +type OnlySignOptions = { + binaries?: string[]; + optionsForFile?: (filePath: string) => PerFileSignOptions; + identityValidation?: boolean; + ignore?: string | string[] | ((file: string) => boolean); + preAutoEntitlements?: boolean; + preEmbedProvisioningProfile?: boolean; + provisioningProfile?: string; + strictVerify?: boolean; type?: SigningDistributionType; version?: string; - entitlementsForFile?: (file: string, codeSignArgs: string[]) => string | null; }; type OnlyValidatedSignOptions = { - entitlements: string; - 'entitlements-inherit': string; ignore?: (string | ((file: string) => boolean))[]; type: SigningDistributionType; }; type OnlyFlatOptions = { - 'identity-validation'?: boolean; + identityValidation?: boolean; install?: string; pkg?: string; scripts?: string; diff --git a/src/util-entitlements.ts b/src/util-entitlements.ts index e074003..9a48613 100644 --- a/src/util-entitlements.ts +++ b/src/util-entitlements.ts @@ -2,7 +2,7 @@ import * as fs from 'fs-extra'; import * as os from 'os'; import * as path from 'path'; import * as plist from 'plist'; -import { ValidatedSignOptions } from './types'; +import { PerFileSignOptions, ValidatedSignOptions } from './types'; import { debugLog, getAppContentsPath } from './util'; import { Identity } from './util-identities'; import { ProvisioningProfile } from './util-provisioning-profiles'; @@ -12,6 +12,8 @@ type ComputedOptions = { provisioningProfile?: ProvisioningProfile; }; +const preAuthMemo = new Map(); + /** * This function returns a promise completing the entitlements automation: The * process includes checking in `Info.plist` for `ElectronTeamID` or setting @@ -22,9 +24,13 @@ type ComputedOptions = { */ export async function preAutoEntitlements ( opts: ValidatedSignOptions, + perFileOpts: PerFileSignOptions, computed: ComputedOptions ): Promise { - if (!opts.entitlements) return; + if (!perFileOpts.entitlements) return; + + const memoKey = [opts.app, perFileOpts.entitlements].join('---'); + if (preAuthMemo.has(memoKey)) return preAuthMemo.get(memoKey); // If entitlements file not provided, default will be used. Fixes #41 const appInfoPath = path.join(getAppContentsPath(opts), 'Info.plist'); @@ -34,12 +40,18 @@ export async function preAutoEntitlements ( '\n', '> Info.plist:', appInfoPath, - '\n', - '> Entitlements:', - opts.entitlements + '\n' ); - const entitlementsContents = await fs.readFile(opts.entitlements, 'utf8'); - const entitlements = plist.parse(entitlementsContents) as Record; + let entitlements: Record; + if (typeof perFileOpts.entitlements === 'string') { + const entitlementsContents = await fs.readFile(perFileOpts.entitlements, 'utf8'); + entitlements = plist.parse(entitlementsContents) as Record; + } else { + entitlements = perFileOpts.entitlements.reduce>((dict, entitlementKey) => ({ + ...dict, + [entitlementKey]: true + }), {}); + } if (!entitlements['com.apple.security.app-sandbox']) { // Only automate when app sandbox enabled by user return; @@ -126,5 +138,6 @@ export async function preAutoEntitlements ( await fs.writeFile(entitlementsPath, plist.build(entitlements), 'utf8'); debugLog('Entitlements file updated:', '\n', '> Entitlements:', entitlementsPath); + preAuthMemo.set(memoKey, entitlementsPath); return entitlementsPath; } diff --git a/test/basic.js b/test/basic.js index e7ec36d..1512774 100644 --- a/test/basic.js +++ b/test/basic.js @@ -11,8 +11,7 @@ function createDefaultsTest (release) { const opts = { app: util.generateAppPath(release), - identity: 'codesign.electronjs.org', - 'gatekeeper-assess': false + identity: 'codesign.electronjs.org' }; // test with no other options for self discovery waterfall(