Repository: electron-userland/electron-builder
File: packages/app-builder-lib/src/codeSign/macCodeSign.ts (TypeScript)

Code before:

  await Promise.all([
    // we do not clear downloaded files - will be removed on tmpDir cleanup automatically. not a security issue since in any case data is available as env variables and protected by password.
    BluebirdPromise.map(certLinks, (link, i) => importCertificate(link, tmpDir, currentDir).then(it => (certPaths[i] = it))),
    BluebirdPromise.mapSeries(securityCommands, it => exec("/usr/bin/security", it)),
  ])
  return await importCerts(keychainFile, certPaths, [cscKeyPassword, cscIKeyPassword].filter(it => it != null) as Array<string>)
}

async function importCerts(keychainFile: string, paths: Array<string>, keyPasswords: Array<string>): Promise<CodeSigningInfo> {
  for (let i = 0; i < paths.length; i++) {
    const password = keyPasswords[i]
    await exec("/usr/bin/security", ["import", paths[i], "-k", keychainFile, "-T", "/usr/bin/codesign", "-T", "/usr/bin/productbuild", "-P", password])

    // https://stackoverflow.com/questions/39868578/security-codesign-in-sierra-keychain-ignores-access-control-settings-and-ui-p
    // https://github.com/electron-userland/electron-packager/issues/701#issuecomment-322315996
    await exec("/usr/bin/security", ["set-key-partition-list", "-S", "apple-tool:,apple:", "-s", "-k", password, keychainFile])
  }

  return {
    keychainFile,
  }
}

export async function sign(opts: SignOptions): Promise<void> {
  const timeout = parseInt(process.env.SIGNTOOL_TIMEOUT as any, 10) || 10 * 60 * 1000
  let retryCount = 0
  while (retryCount < 3) {
    try {
      await Promise.race([signAsync(opts), new Promise((_, reject) => setTimeout(() => reject(new Error("sign Timeout")), timeout))])
      return
    } catch (e: any) {
      if (retryCount > 3) {
        throw e
      }
      retryCount += 1
      log.warn(`Attempt ${retryCount} to code sign failed, another attempt will be made in 15 seconds: ${e.message}`)
      await new Promise(resolve => setTimeout(resolve, 15000))
    }
  }
}

export let findIdentityRawResult: Promise<Array<string>> | null = null

async function getValidIdentities(keychain?: string | null): Promise<Array<string>> {
  function addKeychain(args: Array<string>) {
    if (keychain != null) {
      args.push(keychain)
    }
    return args
  }

Review comment (performance):
Is it safe to add a timeout? I'm worried it won't account for slow network connections and I haven't seen any issues reported of the codesign process hanging.

Kind of in favor of just using `builder-util`'s `retry` mechanism
https://github.com/electron-userland/electron-builder/blob/a13a539fe7aab97de6567c99fe2fadcfa0a4e9ee/packages/builder-util/src/util.ts#L407-L419

Would simplify this logic to:
```
retry(() => signAsync(opts), 3, 5000, 5000)
```
3 retries, 5sec interval, with 5sec multiplier backoff delay

Code after:

  await Promise.all([
    // we do not clear downloaded files - will be removed on tmpDir cleanup automatically. not a security issue since in any case data is available as env variables and protected by password.
    BluebirdPromise.map(certLinks, (link, i) => importCertificate(link, tmpDir, currentDir).then(it => (certPaths[i] = it))),
    BluebirdPromise.mapSeries(securityCommands, it => exec("/usr/bin/security", it)),
  ])
  return await importCerts(keychainFile, certPaths, [cscKeyPassword, cscIKeyPassword].filter(it => it != null) as Array<string>)
}

async function importCerts(keychainFile: string, paths: Array<string>, keyPasswords: Array<string>): Promise<CodeSigningInfo> {
  for (let i = 0; i < paths.length; i++) {
    const password = keyPasswords[i]
    await exec("/usr/bin/security", ["import", paths[i], "-k", keychainFile, "-T", "/usr/bin/codesign", "-T", "/usr/bin/productbuild", "-P", password])

    // https://stackoverflow.com/questions/39868578/security-codesign-in-sierra-keychain-ignores-access-control-settings-and-ui-p
    // https://github.com/electron-userland/electron-packager/issues/701#issuecomment-322315996
    await exec("/usr/bin/security", ["set-key-partition-list", "-S", "apple-tool:,apple:", "-s", "-k", password, keychainFile])
  }

  return {
    keychainFile,
  }
}

export async function sign(opts: SignOptions): Promise<void> {
  return retry(() => signAsync(opts), 3, 5000, 5000)
}

export let findIdentityRawResult: Promise<Array<string>> | null = null

async function getValidIdentities(keychain?: string | null): Promise<Array<string>> {
  function addKeychain(args: Array<string>) {
    if (keychain != null) {
      args.push(keychain)
    }
    return args
  }

  let result = findIdentityRawResult
  if (result == null || keychain != null) {
    // https://github.com/electron-userland/electron-builder/issues/481
    // https://github.com/electron-userland/electron-builder/issues/535
    result = Promise.all<Array<string>>([
      exec("/usr/bin/security", addKeychain(["find-identity", "-v"])).then(it =>
        it
          .trim()
          .split("\n")
          .filter(it => {
            for (const prefix of appleCertificatePrefixes) {
              if (it.includes(prefix)) {
                return true