electron-builder version: 26.15.3
Environment:
- macOS 27.0 (build 26A5388g) - reported via sw_vers; this is a very new/beta build, worth noting in case it's version-specific, though the root cause below looks like a straightforward logic bug independent of macOS version.
- Darwin ... 27.0.0 ... arm64 (Apple Silicon)
- Node v26.5.0, pnpm 11.17.0
- Package manager: pnpm workspace (monorepo)
Description:
When signing a macOS app via CSC_LINK/CSC_KEY_PASSWORD (pointing at a local .p12 file), the build fails during keychain setup with:
⨯ /usr/bin/security process failed 1
Exit code:
1
Output:
Exit code: 1. Command failed: /usr/bin/security set-key-partition-list -S apple-tool:,apple: -s -k <REDACTED> /var/folders/.../<hash>.keychain
security: SecKeychainUnlock: The user name or passphrase you entered is not correct.
This reproduces identically with both an empty CSC_KEY_PASSWORD and a real, non-empty one - same failure, same error, just a different (still wrong) string after -k. That rules out "just set/fix the .p12 password" as a workaround, since the password being passed to set-key-partition-list -k is never the correct one regardless of what it is.
Root cause:
In packages/app-builder-lib/src/codeSign/macCodeSign.ts (compiled: app-builder-lib/out/codeSign/macCodeSign.js), createKeychain() generates a random password to create and unlock the temporary keychain it builds for signing:
// macCodeSign.js, line 137
const keychainPassword = (0, crypto_1.randomBytes)(32).toString("base64");
const securityCommands = [
["create-keychain", "-p", keychainPassword, keychainFile],
["unlock-keychain", "-p", keychainPassword, keychainFile],
["set-keychain-settings", keychainFile],
];
But keychainPassword is never passed down into importCerts() - only the .p12's own import password(s) are (cscPasswords, line 155/159). importCerts() then reuses that .p12 import password for the -k flag of set-key-partition-list, which is documented (and behaves, per this bug) as needing the keychain's own password, not the imported item's password:
// macCodeSign.js, importCerts(), lines 161-169
async function importCerts(keychainFile, paths, keyPasswords) {
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]);
// BUG: `password` here is the .p12's own import password (correct for `security import -P`
// just above), but set-key-partition-list's `-k` flag needs the *keychain's* unlock
// password (`keychainPassword` from createKeychain(), never passed through here) instead.
await exec("/usr/bin/security", ["set-key-partition-list", "-S", "apple-tool:,apple:", "-s", "-k", password, keychainFile]);
}
return { keychainFile };
}
security import -k -P is correct - that -P is genuinely the item's import password. But security set-key-partition-list -k needs the keychain's unlock password to succeed, per security's own man page and every other caller of this command I could find (including electron-builder's own create-keychain/unlock-keychain calls two lines above, which correctly use keychainPassword).
Steps to reproduce:
- Create a Developer ID Application (or any) signing certificate, export as .p12 (with or without a password - both fail).
- Set CSC_LINK=/path/to/cert.p12 and CSC_KEY_PASSWORD=.
- Run electron-builder --mac with any mac.identity unset (so it goes through the normal CSC_LINK import flow).
- Observe the SecKeychainUnlock failure above.
Suggested fix:
Pass keychainPassword from createKeychain() into importCerts() and use it for the set-key-partition-list -k argument, keeping the existing password (import password) only for the security import -P call:
await exec("/usr/bin/security", ["set-key-partition-list", "-S", "apple-tool:,apple:", "-s", "-k", keychainPassword, keychainFile]);
Workaround I used:
don't set CSC_LINK/CSC_KEY_PASSWORD at all. If the certificate is already present in an existing keychain (e.g. the login keychain, from generating the CSR via Keychain Access), electron-builder's identity auto-discovery (CSC_IDENTITY_AUTO_DISCOVERY, on by default) finds and uses it via security find-identity - a code path that never calls createKeychain/importCerts, so the bug never triggers. This isn't viable for CI, though, where there's no pre-populated keychain to discover from - that's presumably the exact scenario CSC_LINK exists for.
electron-builder version: 26.15.3
Environment:
Description:
When signing a macOS app via CSC_LINK/CSC_KEY_PASSWORD (pointing at a local .p12 file), the build fails during keychain setup with:
⨯ /usr/bin/security process failed 1
Exit code:
1
Output:
This reproduces identically with both an empty CSC_KEY_PASSWORD and a real, non-empty one - same failure, same error, just a different (still wrong) string after -k. That rules out "just set/fix the .p12 password" as a workaround, since the password being passed to set-key-partition-list -k is never the correct one regardless of what it is.
Root cause:
In packages/app-builder-lib/src/codeSign/macCodeSign.ts (compiled: app-builder-lib/out/codeSign/macCodeSign.js), createKeychain() generates a random password to create and unlock the temporary keychain it builds for signing:
But keychainPassword is never passed down into importCerts() - only the .p12's own import password(s) are (cscPasswords, line 155/159). importCerts() then reuses that .p12 import password for the -k flag of set-key-partition-list, which is documented (and behaves, per this bug) as needing the keychain's own password, not the imported item's password:
security import -k -P is correct - that -P is genuinely the item's import password. But security set-key-partition-list -k needs the keychain's unlock password to succeed, per security's own man page and every other caller of this command I could find (including electron-builder's own create-keychain/unlock-keychain calls two lines above, which correctly use keychainPassword).
Steps to reproduce:
Suggested fix:
Pass keychainPassword from createKeychain() into importCerts() and use it for the set-key-partition-list -k argument, keeping the existing password (import password) only for the security import -P call:
await exec("/usr/bin/security", ["set-key-partition-list", "-S", "apple-tool:,apple:", "-s", "-k", keychainPassword, keychainFile]);Workaround I used:
don't set CSC_LINK/CSC_KEY_PASSWORD at all. If the certificate is already present in an existing keychain (e.g. the login keychain, from generating the CSR via Keychain Access), electron-builder's identity auto-discovery (CSC_IDENTITY_AUTO_DISCOVERY, on by default) finds and uses it via security find-identity - a code path that never calls createKeychain/importCerts, so the bug never triggers. This isn't viable for CI, though, where there's no pre-populated keychain to discover from - that's presumably the exact scenario CSC_LINK exists for.