45 lines
3.7 KiB
TypeScript
45 lines
3.7 KiB
TypeScript
// Code signing keychain for `storebound apple build` outside the Mac's logged-in GUI session. codesign in an SSH
|
|
// session (or any session without the login keychain unlocked) fails late with errSecInternalComponent. The fixes:
|
|
// - run from the GUI session (a .command file opened with `open -a Terminal`)
|
|
// - a CI runner / Jenkins agent started in that session, or with an unlocked keychain
|
|
// - a build keychain: --keychain <path>, unlocked with --keychain-unlock (password from STOREBOUND_KEYCHAIN_PASSWORD,
|
|
// never printed) and its key partition list set so codesign may use the keys without a prompt.
|
|
// Pure planning here; cli/apple/build.ts runs it.
|
|
export const KEYCHAIN_PASSWORD_ENV = 'STOREBOUND_KEYCHAIN_PASSWORD';
|
|
|
|
/** The refusal for a build in an SSH / non-GUI session, naming every fix. */
|
|
export function sshBuildRefusal(reason: string, o: { unlockFlag?: boolean } = {}): string {
|
|
const unlock = o.unlockFlag !== false;
|
|
return `apple build: ${reason}; codesign there fails with errSecInternalComponent (locked keychain). Fixes, pick one:\n`
|
|
+ ` 1. Run it in the Mac's logged-in GUI session: write the command into a .command file and \`open -a Terminal <file>\` (unset SSH_CONNECTION SSH_CLIENT SSH_TTY inside).\n`
|
|
+ ` 2. A CI runner / Jenkins agent started in the GUI session, or one whose keychain is unlocked first (\`security unlock-keychain -p "$${KEYCHAIN_PASSWORD_ENV}" <keychain>\`, then \`security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$${KEYCHAIN_PASSWORD_ENV}" <keychain>\`).\n`
|
|
+ (unlock ? ` 3. Let storebound do step 2: export ${KEYCHAIN_PASSWORD_ENV}=... (the keychain password; never printed) and add --keychain-unlock (the login keychain), or --keychain <path> --keychain-unlock for a build keychain that holds the Apple Distribution key + certificate (storebound apple signing ensure --keychain <path> imports them).\n`
|
|
+ ` --allow-ssh skips this check when the keychain is already unlocked in this session.`
|
|
: ' --allow-ssh skips this check when the keychain is already unlocked in this session.');
|
|
}
|
|
|
|
export interface KeychainPlan { keychain: string; steps: string[][] }
|
|
|
|
/** The `security` calls --keychain-unlock runs, with PASSWORD as a placeholder (substituted by the caller, never logged). */
|
|
export function unlockSteps(keychain: string, searchList: string[]): string[][] {
|
|
const steps: string[][] = [['security', 'unlock-keychain', '-p', 'PASSWORD', keychain],
|
|
['security', 'set-key-partition-list', '-S', 'apple-tool:,apple:,codesign:', '-s', '-k', 'PASSWORD', keychain]];
|
|
if (!searchList.includes(keychain)) steps.push(['security', 'list-keychains', '-d', 'user', '-s', keychain, ...searchList]);
|
|
return steps;
|
|
}
|
|
|
|
/** `security list-keychains` output -> paths (quotes stripped). */
|
|
export const parseKeychainList = (out: string): string[] => out.split('\n').map(l => l.trim().replace(/^"|"$/g, '')).filter(Boolean);
|
|
|
|
/** The password from the environment, or an error that names the variable (the value is never in a message). */
|
|
export function keychainPassword(env: Record<string, string | undefined>): string {
|
|
const p = env[KEYCHAIN_PASSWORD_ENV];
|
|
if (!p) throw new Error(`--keychain-unlock needs the keychain password in the environment variable ${KEYCHAIN_PASSWORD_ENV} (not an argument: arguments end up in shell history)`);
|
|
return p;
|
|
}
|
|
|
|
/** A command line for logs with the password replaced. */
|
|
export const showSecurity = (argv: string[]): string => argv.map(a => (a === 'PASSWORD' ? '$' + KEYCHAIN_PASSWORD_ENV : a)).join(' ');
|
|
|
|
/** Settings that make xcodebuild sign with a given keychain. */
|
|
export const keychainSigningArgs = (keychain: string): string[] => [`OTHER_CODE_SIGN_FLAGS=--keychain ${keychain}`];
|