diff --git a/packages/hadron-build/lib/mac-notary-service.js b/packages/hadron-build/lib/mac-notary-service.js new file mode 100644 index 00000000000..d45d0bb4464 --- /dev/null +++ b/packages/hadron-build/lib/mac-notary-service.js @@ -0,0 +1,90 @@ +// eslint-disable-next-line strict +'use strict'; +const download = require('download'); +const path = require('path'); +const { promises: fs } = require('fs'); +const debug = require('debug')('hadron-build:macos-notarization'); +const { promisify } = require('util'); +const childProcess = require('child_process'); +const execFile = promisify(childProcess.execFile); + +async function setupMacosNotary() { + try { + await fs.access('macnotary/macnotary'); + debug('macnotary already downloaded'); + } catch (err) { + debug('downloading macnotary'); + await download(process.env.MACOS_NOTARY_CLIENT_URL, 'macnotary', { + extract: true, + strip: 1 // remove leading platform + arch directory + }); + await fs.chmod('macnotary/macnotary', 0o755); // ensure +x is set + } +} + +/** + * Notarize a resource with the macOS notary service. + * https://wiki.corp.mongodb.com/display/BUILD/How+to+use+MacOS+notary+service + * + * Notarization is a three step process: + * 1. All the files to be notarized are zipped up into a single file. + * 2. The zip file is uploaded to the notary service. It signs all the + * files in the zip file and returns a new zip file with the signed files. + * 3. The orginal files are removed and the signed files are unzipped into + * their place. + * + * @param {string} src The path to the resource to notarize. It can be a directory or a file. + * @param {object} notarizeOptions + * @param {string} notarizeOptions.bundleId + * @param {string} [notarizeOptions.macosEntitlements] + */ +async function notarize(src, notarizeOptions) { + debug(`Signing and notarizing "${src}"`); + + await setupMacosNotary(); + + const fileName = path.basename(src); + const unsignedArchive = `${fileName}.zip`; + const signedArchive = `${fileName}.signed.zip`; + + const execOpts = { + cwd: path.dirname(src), + encoding: 'utf8', + }; + + // Step:1 - zip up the file/folder to unsignedArchive + debug(`running "zip -y -r '${unsignedArchive}' '${fileName}'"`); + await execFile('zip', ['-y', '-r', unsignedArchive, fileName], execOpts); + + try { + // Step:2 - send the zip to notary service and save the result to signedArchive + debug(`sending file to notary service (bundle id = ${notarizeOptions.bundleId})`); + const macnotaryResult = await execFile(path.resolve('macnotary/macnotary'), [ + '-t', 'app', + '-m', 'notarizeAndSign', + '-u', process.env.MACOS_NOTARY_API_URL, + '-b', notarizeOptions.bundleId, + '-f', unsignedArchive, + '-o', signedArchive, + '--verify', + ...(notarizeOptions.macosEntitlements ? ['-e', notarizeOptions.macosEntitlements] : []) + ], execOpts); + debug('macnotary result:', macnotaryResult.stdout, macnotaryResult.stderr); + debug('ls', (await execFile('ls', ['-lh'], execOpts)).stdout); + + // Step:3 - remove existing src, unzip signedArchive to src + debug('removing existing directory contents'); + await execFile('rm', ['-r', fileName], execOpts); + debug(`unzipping with "unzip -u ${signedArchive}"`); + await execFile('unzip', ['-u', signedArchive], execOpts); + } finally { + // cleanup - remove signedArchive and unsignedArchive + debug('ls', (await execFile('ls', ['-lh'], execOpts)).stdout); + debug(`removing ${signedArchive} and ${unsignedArchive}`); + await execFile('rm', ['-r', signedArchive, unsignedArchive], execOpts).catch(err => { + debug('error cleaning up', err); + }); + } +} + +module.exports = { notarize }; diff --git a/packages/hadron-build/lib/target.js b/packages/hadron-build/lib/target.js index 1ec0adff3ca..2ecc3746e7c 100644 --- a/packages/hadron-build/lib/target.js +++ b/packages/hadron-build/lib/target.js @@ -1,23 +1,20 @@ // eslint-disable-next-line strict 'use strict'; const chalk = require('chalk'); -const childProcess = require('child_process'); -const download = require('download'); const fs = require('fs'); const _ = require('lodash'); const semver = require('semver'); const path = require('path'); -const { promisify } = require('util'); const normalizePkg = require('normalize-package-data'); const parseGitHubRepoURL = require('parse-github-repo-url'); const ffmpegAfterExtract = require('electron-packager-plugin-non-proprietary-codecs-ffmpeg').default; const windowsInstallerVersion = require('./windows-installer-version'); const debug = require('debug')('hadron-build:target'); -const execFile = promisify(childProcess.execFile); const which = require('which'); const plist = require('plist'); const { sign, getSignedFilename } = require('./signtool'); const tarGz = require('./tar-gz'); +const { notarize } = require('./mac-notary-service'); function _canBuildInstaller(ext) { var bin = null; @@ -562,7 +559,6 @@ class Target { }; this.createInstaller = async() => { - const appDirectoryName = `${this.productName}.app`; const appPath = this.appPath; { @@ -580,58 +576,32 @@ class Target { await fs.promises.writeFile(plistFilePath, plist.build(plistContents)); } - if (process.env.MACOS_NOTARY_KEY && - process.env.MACOS_NOTARY_SECRET && - process.env.MACOS_NOTARY_CLIENT_URL && - process.env.MACOS_NOTARY_API_URL) { - debug(`Signing and notarizing "${appPath}"`); - // https://wiki.corp.mongodb.com/display/BUILD/How+to+use+MacOS+notary+service - debug(`Downloading the notary client from ${process.env.MACOS_NOTARY_CLIENT_URL} to ${path.resolve('macnotary')}`); - await download(process.env.MACOS_NOTARY_CLIENT_URL, 'macnotary', { - extract: true, - strip: 1 // remove leading platform + arch directory - }); - await fs.promises.chmod('macnotary/macnotary', 0o755); // ensure +x is set + const isNotarizationPossible = process.env.MACOS_NOTARY_KEY && + process.env.MACOS_NOTARY_SECRET && + process.env.MACOS_NOTARY_CLIENT_URL && + process.env.MACOS_NOTARY_API_URL; - debug(`running "zip -y -r '${appDirectoryName}.zip' '${appDirectoryName}'"`); - await execFile('zip', ['-y', '-r', `${appDirectoryName}.zip`, appDirectoryName], { - cwd: path.dirname(appPath) - }); - debug(`sending file to notary service (bundle id = ${this.bundleId})`); - const macnotaryResult = await execFile(path.resolve('macnotary/macnotary'), [ - '-t', 'app', - '-m', 'notarizeAndSign', - '-u', process.env.MACOS_NOTARY_API_URL, - '-b', this.bundleId, - '-f', `${appDirectoryName}.zip`, - '-o', `${appDirectoryName}.signed.zip`, - '--verify', - ...(this.macosEntitlements ? ['-e', this.macosEntitlements] : []) - ], { - cwd: path.dirname(appPath), - encoding: 'utf8' - }); - debug('macnotary result:', macnotaryResult.stdout, macnotaryResult.stderr); - debug('ls', (await execFile('ls', ['-lh'], { cwd: path.dirname(appPath), encoding: 'utf8' })).stdout); - debug('removing existing directory contents'); - await execFile('rm', ['-r', appDirectoryName], { - cwd: path.dirname(appPath) - }); - debug(`unzipping with "unzip -u '${appDirectoryName}.signed.zip'"`); - await execFile('unzip', ['-u', `${appDirectoryName}.signed.zip`], { - cwd: path.dirname(appPath), - encoding: 'utf8' - }); - debug('ls', (await execFile('ls', ['-lh'], { cwd: path.dirname(appPath), encoding: 'utf8' })).stdout); - debug(`removing '${appDirectoryName}.signed.zip' and '${appDirectoryName}.zip'`); - await fs.promises.unlink(`${appPath}.signed.zip`); - await fs.promises.unlink(`${appPath}.zip`); + const notarizationOptions = { + bundleId: this.bundleId, + macosEntitlements: this.macosEntitlements + }; + + if (isNotarizationPossible) { + await notarize(appPath, notarizationOptions); } else { console.error(chalk.yellow.bold( - 'WARNING: macos notary service credentials not set -- skipping signing and notarization!')); + 'WARNING: macos notary service credentials not set -- skipping signing and notarization of .app!')); } + const createDMG = require('electron-installer-dmg'); await createDMG(this.installerOptions); + + if (isNotarizationPossible) { + await notarize(this.installerOptions.dmgPath, notarizationOptions); + } else { + console.error(chalk.yellow.bold( + 'WARNING: macos notary service credentials not set -- skipping signing and notarization of .dmg!')); + } }; }