sign.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. /**
  2. * @module sign
  3. */
  4. 'use strict'
  5. const path = require('path')
  6. const Promise = require('bluebird')
  7. const compareVersion = require('compare-version')
  8. const pkg = require('./package.json')
  9. const util = require('./util')
  10. const debuglog = util.debuglog
  11. const debugwarn = util.debugwarn
  12. const getAppContentsPath = util.getAppContentsPath
  13. const execFileAsync = util.execFileAsync
  14. const validateOptsAppAsync = util.validateOptsAppAsync
  15. const validateOptsPlatformAsync = util.validateOptsPlatformAsync
  16. const walkAsync = util.walkAsync
  17. const Identity = require('./util-identities').Identity
  18. const findIdentitiesAsync = require('./util-identities').findIdentitiesAsync
  19. const ProvisioningProfile = require('./util-provisioning-profiles').ProvisioningProfile
  20. const preEmbedProvisioningProfile = require('./util-provisioning-profiles').preEmbedProvisioningProfile
  21. const preAutoEntitlements = require('./util-entitlements').preAutoEntitlements
  22. /**
  23. * This function returns a promise validating opts.binaries, the additional binaries to be signed along with the discovered enclosed components.
  24. * @function
  25. * @param {Object} opts - Options.
  26. * @returns {Promise} Promise.
  27. */
  28. function validateOptsBinariesAsync (opts) {
  29. return new Promise(function (resolve, reject) {
  30. if (opts.binaries) {
  31. if (!Array.isArray(opts.binaries)) {
  32. reject(new Error('Additional binaries should be an Array.'))
  33. return
  34. }
  35. // TODO: Presence check for binary files, reject if any does not exist
  36. }
  37. resolve()
  38. })
  39. }
  40. /**
  41. * This function returns a promise validating all options passed in opts.
  42. * @function
  43. * @param {Object} opts - Options.
  44. * @returns {Promise} Promise.
  45. */
  46. function validateSignOptsAsync (opts) {
  47. if (opts.ignore && !(opts.ignore instanceof Array)) {
  48. opts.ignore = [opts.ignore]
  49. }
  50. if (opts['provisioning-profile']) {
  51. if (typeof opts['provisioning-profile'] !== 'string' && !(opts['provisioning-profile'] instanceof ProvisioningProfile)) return Promise.reject(new Error('Path to provisioning profile should be a string or a ProvisioningProfile object.'))
  52. }
  53. if (opts['type']) {
  54. if (opts['type'] !== 'development' && opts['type'] !== 'distribution') return Promise.reject(new Error('Type must be either `development` or `distribution`.'))
  55. } else {
  56. opts['type'] = 'distribution'
  57. }
  58. return Promise.map([
  59. validateOptsAppAsync,
  60. validateOptsPlatformAsync,
  61. validateOptsBinariesAsync
  62. ], function (validate) {
  63. return validate(opts)
  64. })
  65. }
  66. /**
  67. * This function returns a promise verifying the code sign of application bundle.
  68. * @function
  69. * @param {Object} opts - Options.
  70. * @returns {Promise} Promise resolving output.
  71. */
  72. function verifySignApplicationAsync (opts) {
  73. // Verify with codesign
  74. var compareVersion = require('compare-version')
  75. var osRelease = require('os').release()
  76. debuglog('Verifying application bundle with codesign...')
  77. var promise = execFileAsync('codesign', [
  78. '--verify',
  79. '--deep'
  80. ]
  81. .concat(
  82. opts['strict-verify'] !== false &&
  83. compareVersion(osRelease, '15.0.0') >= 0 // Only pass strict flag in El Capitan and later
  84. ? ['--strict' +
  85. (opts['strict-verify']
  86. ? '=' + opts['strict-verify'] // Array should be converted to a comma separated string
  87. : '')]
  88. : [],
  89. ['--verbose=2', opts.app]))
  90. // Additionally test Gatekeeper acceptance for darwin platform
  91. if (opts.platform === 'darwin' && opts['gatekeeper-assess'] !== false) {
  92. promise = promise
  93. .then(function () {
  94. debuglog('Verifying Gatekeeper acceptance for darwin platform...')
  95. return execFileAsync('spctl', [
  96. '--assess',
  97. '--type', 'execute',
  98. '--verbose',
  99. '--ignore-cache',
  100. '--no-cache',
  101. opts.app
  102. ])
  103. })
  104. }
  105. return promise
  106. .thenReturn()
  107. }
  108. /**
  109. * This function returns a promise codesigning only.
  110. * @function
  111. * @param {Object} opts - Options.
  112. * @returns {Promise} Promise.
  113. */
  114. function signApplicationAsync (opts) {
  115. return walkAsync(getAppContentsPath(opts))
  116. .then(function (childPaths) {
  117. function ignoreFilePath (opts, filePath) {
  118. if (opts.ignore) {
  119. return opts.ignore.some(function (ignore) {
  120. if (typeof ignore === 'function') {
  121. return ignore(filePath)
  122. }
  123. return filePath.match(ignore)
  124. })
  125. }
  126. return false
  127. }
  128. if (opts.binaries) childPaths = childPaths.concat(opts.binaries)
  129. var args = [
  130. '--sign', opts.identity.hash || opts.identity.name,
  131. '--force'
  132. ]
  133. if (opts.keychain) {
  134. args.push('--keychain', opts.keychain)
  135. }
  136. if (opts.requirements) {
  137. args.push('--requirements', opts.requirements)
  138. }
  139. if (opts.timestamp) {
  140. args.push('--timestamp=' + opts.timestamp)
  141. }
  142. var promise
  143. if (opts.entitlements) {
  144. // Sign with entitlements
  145. promise = Promise.mapSeries(childPaths, function (filePath) {
  146. if (ignoreFilePath(opts, filePath)) {
  147. debuglog('Skipped... ' + filePath)
  148. return
  149. }
  150. debuglog('Signing... ' + filePath)
  151. return execFileAsync('codesign', args.concat('--entitlements', opts['entitlements-inherit'], filePath))
  152. })
  153. .then(function () {
  154. debuglog('Signing... ' + opts.app)
  155. return execFileAsync('codesign', args.concat('--entitlements', opts.entitlements, opts.app))
  156. })
  157. } else {
  158. // Otherwise normally
  159. promise = Promise.mapSeries(childPaths, function (filePath) {
  160. if (ignoreFilePath(opts, filePath)) {
  161. debuglog('Skipped... ' + filePath)
  162. return
  163. }
  164. debuglog('Signing... ' + filePath)
  165. return execFileAsync('codesign', args.concat(filePath))
  166. })
  167. .then(function () {
  168. debuglog('Signing... ' + opts.app)
  169. return execFileAsync('codesign', args.concat(opts.app))
  170. })
  171. }
  172. return promise
  173. .then(function () {
  174. // Verify code sign
  175. debuglog('Verifying...')
  176. var promise = verifySignApplicationAsync(opts)
  177. .then(function (result) {
  178. debuglog('Verified.')
  179. })
  180. // Check entitlements if applicable
  181. if (opts.entitlements) {
  182. promise = promise
  183. .then(function () {
  184. debuglog('Displaying entitlements...')
  185. return execFileAsync('codesign', [
  186. '--display',
  187. '--entitlements', ':-', // Write to standard output and strip off the blob header
  188. opts.app
  189. ])
  190. })
  191. .then(function (result) {
  192. debuglog('Entitlements:', '\n',
  193. result)
  194. })
  195. }
  196. return promise
  197. })
  198. })
  199. }
  200. /**
  201. * This function returns a promise signing the application.
  202. * @function
  203. * @param {mixed} opts - Options.
  204. * @returns {Promise} Promise.
  205. */
  206. var signAsync = module.exports.signAsync = function (opts) {
  207. debuglog('electron-osx-sign@%s', pkg.version)
  208. return validateSignOptsAsync(opts)
  209. .then(function () {
  210. // Determine identity for signing
  211. var promise
  212. if (opts.identity) {
  213. debuglog('`identity` passed in arguments.')
  214. if (opts['identity-validation'] === false) {
  215. if (!(opts.identity instanceof Identity)) {
  216. opts.identity = new Identity(opts.identity)
  217. }
  218. return Promise.resolve()
  219. }
  220. promise = findIdentitiesAsync(opts, opts.identity)
  221. } else {
  222. debugwarn('No `identity` passed in arguments...')
  223. if (opts.platform === 'mas') {
  224. if (opts.type === 'distribution') {
  225. debuglog('Finding `3rd Party Mac Developer Application` certificate for signing app distribution in the Mac App Store...')
  226. promise = findIdentitiesAsync(opts, '3rd Party Mac Developer Application:')
  227. } else {
  228. debuglog('Finding `Mac Developer` certificate for signing app in development for the Mac App Store signing...')
  229. promise = findIdentitiesAsync(opts, 'Mac Developer:')
  230. }
  231. } else {
  232. debuglog('Finding `Developer ID Application` certificate for distribution outside the Mac App Store...')
  233. promise = findIdentitiesAsync(opts, 'Developer ID Application:')
  234. }
  235. }
  236. return promise
  237. .then(function (identities) {
  238. if (identities.length > 0) {
  239. // Identity(/ies) found
  240. if (identities.length > 1) {
  241. debugwarn('Multiple identities found, will use the first discovered.')
  242. } else {
  243. debuglog('Found 1 identity.')
  244. }
  245. opts.identity = identities[0]
  246. } else {
  247. // No identity found
  248. return Promise.reject(new Error('No identity found for signing.'))
  249. }
  250. })
  251. })
  252. .then(function () {
  253. // Determine entitlements for code signing
  254. var filePath
  255. if (opts.platform === 'mas') {
  256. // To sign apps for Mac App Store, an entitlements file is required, especially for app sandboxing (as well some other services).
  257. // Fallback entitlements for sandboxing by default: Note this may cause troubles while running an signed app due to missing keys special to the project.
  258. // Further reading: https://developer.apple.com/library/mac/documentation/Miscellaneous/Reference/EntitlementKeyReference/Chapters/EnablingAppSandbox.html
  259. if (!opts.entitlements) {
  260. filePath = path.join(__dirname, 'default.entitlements.mas.plist')
  261. debugwarn('No `entitlements` passed in arguments:', '\n',
  262. '* Sandbox entitlements are required for Mac App Store distribution, your codesign entitlements file is default to:', filePath)
  263. opts.entitlements = filePath
  264. }
  265. if (!opts['entitlements-inherit']) {
  266. filePath = path.join(__dirname, 'default.entitlements.mas.inherit.plist')
  267. debugwarn('No `entitlements-inherit` passed in arguments:', '\n',
  268. '* Sandbox entitlements file for enclosing app files is default to:', filePath)
  269. opts['entitlements-inherit'] = filePath
  270. }
  271. } else {
  272. // Not necessary to have entitlements for non Mac App Store distribution
  273. if (!opts.entitlements) {
  274. debugwarn('No `entitlements` passed in arguments:', '\n',
  275. '* Provide `entitlements` to specify entitlements file for codesign.')
  276. } else {
  277. // If entitlements is provided as a flag, fallback to default
  278. if (opts.entitlements === true) {
  279. filePath = path.join(__dirname, 'default.entitlements.darwin.plist')
  280. debugwarn('`entitlements` not specified in arguments:', '\n',
  281. '* Provide `entitlements` to specify entitlements file for codesign.', '\n',
  282. '* Sandbox entitlements file for enclosing app files is default to:', filePath)
  283. opts.entitlements = filePath
  284. }
  285. if (!opts['entitlements-inherit']) {
  286. filePath = path.join(__dirname, 'default.entitlements.darwin.inherit.plist')
  287. debugwarn('No `entitlements-inherit` passed in arguments:', '\n',
  288. '* Sandbox entitlements file for enclosing app files is default to:', filePath)
  289. opts['entitlements-inherit'] = filePath
  290. }
  291. }
  292. }
  293. })
  294. .then(function () {
  295. // Pre-sign operations
  296. var preSignOperations = []
  297. if (opts['pre-embed-provisioning-profile'] === false) {
  298. debugwarn('Pre-sign operation disabled for provisioning profile embedding:', '\n',
  299. '* Enable by setting `pre-embed-provisioning-profile` to `true`.')
  300. } else {
  301. debuglog('Pre-sign operation enabled for provisioning profile:', '\n',
  302. '* Disable by setting `pre-embed-previsioning-profile` to `false`.')
  303. preSignOperations.push(preEmbedProvisioningProfile)
  304. }
  305. if (opts['pre-auto-entitlements'] === false) {
  306. debugwarn('Pre-sign operation disabled for entitlements automation.')
  307. } else {
  308. debuglog('Pre-sign operation enabled for entitlements automation with versions >= `1.1.1`:', '\n',
  309. '* Disable by setting `pre-auto-entitlements` to `false`.')
  310. if (opts.entitlements && (!opts.version || compareVersion(opts.version, '1.1.1') >= 0)) {
  311. // Enable Mac App Store sandboxing without using temporary-exception, introduced in Electron v1.1.1. Relates to electron#5601
  312. preSignOperations.push(preAutoEntitlements)
  313. }
  314. }
  315. return Promise.mapSeries(preSignOperations, function (preSignOperation) {
  316. return preSignOperation(opts)
  317. })
  318. })
  319. .then(function () {
  320. debuglog('Signing application...', '\n',
  321. '> Application:', opts.app, '\n',
  322. '> Platform:', opts.platform, '\n',
  323. '> Entitlements:', opts.entitlements, '\n',
  324. '> Child entitlements:', opts['entitlements-inherit'], '\n',
  325. '> Additional binaries:', opts.binaries, '\n',
  326. '> Identity:', opts.identity)
  327. return signApplicationAsync(opts)
  328. })
  329. .then(function () {
  330. // Post-sign operations
  331. debuglog('Application signed.')
  332. })
  333. }
  334. /**
  335. * This function is a normal callback implementation.
  336. * @function
  337. * @param {Object} opts - Options.
  338. * @param {RequestCallback} cb - Callback.
  339. */
  340. module.exports.sign = function (opts, cb) {
  341. signAsync(opts)
  342. .then(function () {
  343. debuglog('Application signed: ' + opts.app)
  344. if (cb) cb()
  345. })
  346. .catch(function (err) {
  347. debuglog('Sign failed:')
  348. if (err.message) debuglog(err.message)
  349. else if (err.stack) debuglog(err.stack)
  350. else debuglog(err)
  351. if (cb) cb(err)
  352. })
  353. }