1 |
|
2 |
|
3 |
|
4 |
|
5 | 'use strict'
|
6 |
|
7 | const path = require('path')
|
8 |
|
9 | const semver = require('semver')
|
10 |
|
11 | const util = require('./util')
|
12 | const debuglog = util.debuglog
|
13 | const debugwarn = util.debugwarn
|
14 | const getAppContentsPath = util.getAppContentsPath
|
15 | const execFileAsync = util.execFileAsync
|
16 | const validateOptsAppAsync = util.validateOptsAppAsync
|
17 | const validateOptsPlatformAsync = util.validateOptsPlatformAsync
|
18 | const walkAsync = util.walkAsync
|
19 | const Identity = require('./util-identities').Identity
|
20 | const findIdentitiesAsync = require('./util-identities').findIdentitiesAsync
|
21 | const ProvisioningProfile = require('./util-provisioning-profiles').ProvisioningProfile
|
22 | const preEmbedProvisioningProfile = require('./util-provisioning-profiles').preEmbedProvisioningProfile
|
23 | const preAutoEntitlements = require('./util-entitlements').preAutoEntitlements
|
24 |
|
25 | const osRelease = require('os').release()
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 | function validateOptsBinariesAsync (opts) {
|
34 | return new Promise(function (resolve, reject) {
|
35 | if (opts.binaries) {
|
36 | if (!Array.isArray(opts.binaries)) {
|
37 | reject(new Error('Additional binaries should be an Array.'))
|
38 | return
|
39 | }
|
40 |
|
41 | }
|
42 | resolve()
|
43 | })
|
44 | }
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 | function validateSignOptsAsync (opts) {
|
53 | if (opts.ignore && !(opts.ignore instanceof Array)) {
|
54 | opts.ignore = [opts.ignore]
|
55 | }
|
56 |
|
57 | if (opts['provisioning-profile']) {
|
58 | 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.'))
|
59 | }
|
60 |
|
61 | if (opts['type']) {
|
62 | if (opts['type'] !== 'development' && opts['type'] !== 'distribution') return Promise.reject(new Error('Type must be either `development` or `distribution`.'))
|
63 | } else {
|
64 | opts['type'] = 'distribution'
|
65 | }
|
66 |
|
67 | return Promise.all([
|
68 | validateOptsAppAsync(opts),
|
69 | validateOptsPlatformAsync(opts),
|
70 | validateOptsBinariesAsync(opts),
|
71 | ])
|
72 | }
|
73 |
|
74 |
|
75 |
|
76 |
|
77 |
|
78 |
|
79 |
|
80 | async function verifySignApplicationAsync (opts) {
|
81 |
|
82 | const semver = require('semver')
|
83 | debuglog('Verifying application bundle with codesign...')
|
84 |
|
85 | await execFileAsync('codesign', [
|
86 | '--verify',
|
87 | '--deep'
|
88 | ]
|
89 | .concat(
|
90 | opts['strict-verify'] !== false &&
|
91 | semver.gte(osRelease, '15.0.0') >= 0
|
92 | ? ['--strict' +
|
93 | (opts['strict-verify']
|
94 | ? '=' + opts['strict-verify']
|
95 | : '')]
|
96 | : [],
|
97 | ['--verbose=2', opts.app]))
|
98 |
|
99 |
|
100 | if (opts.platform === 'darwin' && opts['gatekeeper-assess'] !== false) {
|
101 | debuglog('Verifying Gatekeeper acceptance for darwin platform...')
|
102 | await execFileAsync('spctl', [
|
103 | '--assess',
|
104 | '--type', 'execute',
|
105 | '--verbose',
|
106 | '--ignore-cache',
|
107 | '--no-cache',
|
108 | opts.app
|
109 | ])
|
110 | }
|
111 | }
|
112 |
|
113 |
|
114 |
|
115 |
|
116 |
|
117 |
|
118 |
|
119 | function signApplicationAsync (opts) {
|
120 | return walkAsync(getAppContentsPath(opts))
|
121 | .then(async function (childPaths) {
|
122 | |
123 |
|
124 |
|
125 |
|
126 |
|
127 | childPaths = childPaths.sort((a, b) => {
|
128 | const aDepth = a.split(path.sep).length
|
129 | const bDepth = b.split(path.sep).length
|
130 | return bDepth - aDepth
|
131 | })
|
132 |
|
133 | function ignoreFilePath (opts, filePath) {
|
134 | if (opts.ignore) {
|
135 | return opts.ignore.some(function (ignore) {
|
136 | if (typeof ignore === 'function') {
|
137 | return ignore(filePath)
|
138 | }
|
139 | return filePath.match(ignore)
|
140 | })
|
141 | }
|
142 | return false
|
143 | }
|
144 |
|
145 | if (opts.binaries) childPaths = childPaths.concat(opts.binaries)
|
146 |
|
147 | const args = [
|
148 | '--sign', opts.identity.hash || opts.identity.name,
|
149 | '--force'
|
150 | ]
|
151 | if (opts.keychain) {
|
152 | args.push('--keychain', opts.keychain)
|
153 | }
|
154 | if (opts.requirements) {
|
155 | args.push('--requirements', opts.requirements)
|
156 | }
|
157 | if (opts.timestamp) {
|
158 | args.push('--timestamp=' + opts.timestamp)
|
159 | } else {
|
160 | args.push('--timestamp')
|
161 | }
|
162 | if (opts['signature-size']) {
|
163 | if (Number.isInteger(opts['signature-size']) && opts['signature-size'] > 0) {
|
164 | args.push('--signature-size', opts['signature-size'])
|
165 | } else {
|
166 | debugwarn(`Invalid value provided for --signature-size (${opts['signature-size']}). Must be a positive integer.`)
|
167 | }
|
168 | }
|
169 |
|
170 | let optionsArguments = []
|
171 |
|
172 | if (opts['signature-flags']) {
|
173 | if (Array.isArray(opts['signature-flags'])) {
|
174 | optionsArguments = [...opts['signature-flags']]
|
175 | } else {
|
176 | const flags = opts['signature-flags'].split(',').map(function (flag) { return flag.trim() })
|
177 | optionsArguments = [...flags]
|
178 | }
|
179 | }
|
180 |
|
181 | if (opts.hardenedRuntime || opts['hardened-runtime'] || optionsArguments.includes('runtime')) {
|
182 |
|
183 | if (semver.gte(osRelease, '17.7.0') >= 0) {
|
184 | optionsArguments.push('runtime')
|
185 | } else {
|
186 |
|
187 | debuglog('Not enabling hardened runtime, current macOS version too low, requires 10.13.6 and higher')
|
188 | optionsArguments = optionsArguments.filter(function (element, index) { return element !== 'runtime' })
|
189 | }
|
190 | }
|
191 |
|
192 | if (opts['restrict']) {
|
193 | optionsArguments.push('restrict')
|
194 | debugwarn('This flag is to be deprecated, consider using --signature-flags=restrict instead')
|
195 | }
|
196 |
|
197 | if (optionsArguments.length) {
|
198 | args.push('--options', [...new Set(optionsArguments)].join(','))
|
199 | }
|
200 |
|
201 | if (opts.entitlements) {
|
202 |
|
203 | for (const filePath of childPaths) {
|
204 | if (ignoreFilePath(opts, filePath)) {
|
205 | debuglog('Skipped... ' + filePath)
|
206 | continue
|
207 | }
|
208 | debuglog('Signing... ' + filePath)
|
209 | let entitlementsFile = opts['entitlements-inherit']
|
210 | if (filePath.includes('Library/LoginItems')) {
|
211 | entitlementsFile = opts['entitlements-loginhelper']
|
212 | }
|
213 |
|
214 | await execFileAsync('codesign', args.concat('--entitlements', entitlementsFile, filePath))
|
215 | }
|
216 | debuglog('Signing... ' + opts.app)
|
217 | await execFileAsync('codesign', args.concat('--entitlements', opts.entitlements, opts.app))
|
218 | } else {
|
219 | for (const filePath of childPaths) {
|
220 | if (ignoreFilePath(opts, filePath)) {
|
221 | debuglog('Skipped... ' + filePath)
|
222 | continue
|
223 | }
|
224 |
|
225 | debuglog('Signing... ' + filePath)
|
226 | await execFileAsync('codesign', args.concat(filePath))
|
227 | }
|
228 |
|
229 | debuglog('Signing... ' + opts.app)
|
230 | await execFileAsync('codesign', args.concat(opts.app))
|
231 | }
|
232 |
|
233 |
|
234 | debuglog('Verifying...')
|
235 | await verifySignApplicationAsync(opts)
|
236 | debuglog('Verified.')
|
237 |
|
238 |
|
239 | if (opts.entitlements) {
|
240 | debuglog('Displaying entitlements...')
|
241 | const result = await execFileAsync('codesign', [
|
242 | '--display',
|
243 | '--entitlements', ':-',
|
244 | opts.app
|
245 | ])
|
246 | debuglog('Entitlements:', '\n', result)
|
247 | }
|
248 | })
|
249 | }
|
250 |
|
251 |
|
252 |
|
253 |
|
254 |
|
255 |
|
256 |
|
257 | const signAsync = module.exports.signAsync = function (opts) {
|
258 | return validateSignOptsAsync(opts)
|
259 | .then(function () {
|
260 |
|
261 | let promise
|
262 | if (opts.identity) {
|
263 | debuglog('`identity` passed in arguments.')
|
264 | if (opts['identity-validation'] === false) {
|
265 | if (!(opts.identity instanceof Identity)) {
|
266 | opts.identity = new Identity(opts.identity)
|
267 | }
|
268 | return Promise.resolve()
|
269 | }
|
270 | promise = findIdentitiesAsync(opts, opts.identity)
|
271 | } else {
|
272 | debugwarn('No `identity` passed in arguments...')
|
273 | if (opts.platform === 'mas') {
|
274 | if (opts.type === 'distribution') {
|
275 | debuglog('Finding `3rd Party Mac Developer Application` certificate for signing app distribution in the Mac App Store...')
|
276 | promise = findIdentitiesAsync(opts, '3rd Party Mac Developer Application:')
|
277 | } else {
|
278 | debuglog('Finding `Mac Developer` certificate for signing app in development for the Mac App Store signing...')
|
279 | promise = findIdentitiesAsync(opts, 'Mac Developer:')
|
280 | }
|
281 | } else {
|
282 | debuglog('Finding `Developer ID Application` certificate for distribution outside the Mac App Store...')
|
283 | promise = findIdentitiesAsync(opts, 'Developer ID Application:')
|
284 | }
|
285 | }
|
286 | return promise
|
287 | .then(function (identities) {
|
288 | if (identities.length > 0) {
|
289 |
|
290 | if (identities.length > 1) {
|
291 | debugwarn('Multiple identities found, will use the first discovered.')
|
292 | } else {
|
293 | debuglog('Found 1 identity.')
|
294 | }
|
295 | opts.identity = identities[0]
|
296 | } else {
|
297 |
|
298 | return Promise.reject(new Error('No identity found for signing.'))
|
299 | }
|
300 | })
|
301 | })
|
302 | .then(function () {
|
303 |
|
304 | let filePath
|
305 | if (opts.platform === 'mas') {
|
306 |
|
307 |
|
308 |
|
309 | if (!opts.entitlements) {
|
310 | filePath = path.join(__dirname, 'default.entitlements.mas.plist')
|
311 | debugwarn('No `entitlements` passed in arguments:', '\n',
|
312 | '* Sandbox entitlements are required for Mac App Store distribution, your codesign entitlements file is default to:', filePath)
|
313 | opts.entitlements = filePath
|
314 | }
|
315 | if (!opts['entitlements-inherit']) {
|
316 | filePath = path.join(__dirname, 'default.entitlements.mas.inherit.plist')
|
317 | debugwarn('No `entitlements-inherit` passed in arguments:', '\n',
|
318 | '* Sandbox entitlements file for enclosing app files is default to:', filePath)
|
319 | opts['entitlements-inherit'] = filePath
|
320 | }
|
321 | } else {
|
322 |
|
323 | if (!opts.entitlements) {
|
324 | debugwarn('No `entitlements` passed in arguments:', '\n',
|
325 | '* Provide `entitlements` to specify entitlements file for codesign.')
|
326 | } else {
|
327 |
|
328 | if (opts.entitlements === true) {
|
329 | filePath = path.join(__dirname, 'default.entitlements.darwin.plist')
|
330 | debugwarn('`entitlements` not specified in arguments:', '\n',
|
331 | '* Provide `entitlements` to specify entitlements file for codesign.', '\n',
|
332 | '* Sandbox entitlements file for enclosing app files is default to:', filePath)
|
333 | opts.entitlements = filePath
|
334 | }
|
335 | if (!opts['entitlements-inherit']) {
|
336 | filePath = path.join(__dirname, 'default.entitlements.darwin.inherit.plist')
|
337 | debugwarn('No `entitlements-inherit` passed in arguments:', '\n',
|
338 | '* Sandbox entitlements file for enclosing app files is default to:', filePath)
|
339 | opts['entitlements-inherit'] = filePath
|
340 | }
|
341 | }
|
342 | }
|
343 | if (!opts['entitlements-loginhelper']) {
|
344 | filePath = opts.entitlements
|
345 | debugwarn('No `entitlements-loginhelper` passed in arguments:', '\n',
|
346 | '* Sandbox entitlements file for login helper is default to:', filePath)
|
347 | opts['entitlements-loginhelper'] = filePath
|
348 | }
|
349 | })
|
350 | .then(async function () {
|
351 |
|
352 | const preSignOperations = []
|
353 |
|
354 | if (opts['pre-embed-provisioning-profile'] === false) {
|
355 | debugwarn('Pre-sign operation disabled for provisioning profile embedding:', '\n',
|
356 | '* Enable by setting `pre-embed-provisioning-profile` to `true`.')
|
357 | } else {
|
358 | debuglog('Pre-sign operation enabled for provisioning profile:', '\n',
|
359 | '* Disable by setting `pre-embed-provisioning-profile` to `false`.')
|
360 | preSignOperations.push(preEmbedProvisioningProfile)
|
361 | }
|
362 |
|
363 | if (opts['pre-auto-entitlements'] === false) {
|
364 | debugwarn('Pre-sign operation disabled for entitlements automation.')
|
365 | } else {
|
366 | debuglog('Pre-sign operation enabled for entitlements automation with versions >= `1.1.1`:', '\n',
|
367 | '* Disable by setting `pre-auto-entitlements` to `false`.')
|
368 | if (opts.entitlements && (!opts.version || semver.gte(opts.version, '1.1.1') >= 0)) {
|
369 |
|
370 | preSignOperations.push(preAutoEntitlements)
|
371 | }
|
372 | }
|
373 |
|
374 | for (const preSignOperation of preSignOperations) {
|
375 | await preSignOperation(opts)
|
376 | }
|
377 | })
|
378 | .then(function () {
|
379 | debuglog('Signing application...', '\n',
|
380 | '> Application:', opts.app, '\n',
|
381 | '> Platform:', opts.platform, '\n',
|
382 | '> Entitlements:', opts.entitlements, '\n',
|
383 | '> Child entitlements:', opts['entitlements-inherit'], '\n',
|
384 | '> Additional binaries:', opts.binaries, '\n',
|
385 | '> Identity:', opts.identity)
|
386 | return signApplicationAsync(opts)
|
387 | })
|
388 | .then(function () {
|
389 |
|
390 | debuglog('Application signed.')
|
391 | })
|
392 | }
|
393 |
|
394 |
|
395 |
|
396 |
|
397 |
|
398 |
|
399 |
|
400 | module.exports.sign = function (opts, cb) {
|
401 | signAsync(opts)
|
402 | .then(function () {
|
403 | debuglog('Application signed: ' + opts.app)
|
404 | if (cb) cb()
|
405 | })
|
406 | .catch(function (err) {
|
407 | debuglog('Sign failed:')
|
408 | if (err.message) debuglog(err.message)
|
409 | else if (err.stack) debuglog(err.stack)
|
410 | else debuglog(err)
|
411 | if (cb) cb(err)
|
412 | })
|
413 | }
|