Skip to content

Instantly share code, notes, and snippets.

@waschmittel
Last active August 3, 2026 12:53
Show Gist options
  • Select an option

  • Save waschmittel/00df8a5f46861f234fb1e426c479a449 to your computer and use it in GitHub Desktop.

Select an option

Save waschmittel/00df8a5f46861f234fb1e426c479a449 to your computer and use it in GitHub Desktop.
script to setup git/ssh auth and git signing based on https://gist.github.com/arianvp/5f59f1783e3eaf1a2d4cd8e952bb4acf -- git-ssh (push/pull/...) needs fingerprint, commit-signing does not, but both keys are in the secure enclave
#!/usr/bin/env zx
//
// resolveIdentity() falls back from exact-label match to "the sole identity
// with this touch policy" so a key created by hand before this script existed
// gets adopted rather than duplicated; ambiguous cases die rather than guess.
// --cleanup deletes by resolved identity one at a time, never via
// `sc_auth delete-all-ctk-identities`, so unrelated FileVault/PIV/login
// smartcard identities are never touched.
import os from 'node:os';
import path from 'node:path';
$.verbose = false;
const SCRIPT_NAME = path.basename(new URL(import.meta.url).pathname);
const SSH_DIR = path.join(os.homedir(), '.ssh');
const PROVIDER = '/usr/lib/ssh-keychain.dylib';
const PUSH_LABEL = 'ssh-push';
const SIGN_LABEL = 'ssh-signing';
const PUSH_PRIV = path.join(SSH_DIR, 'id_ecdsa_sk_rk');
const PUSH_PUB = `${PUSH_PRIV}.pub`;
const SIGN_PRIV = path.join(SSH_DIR, 'id_ecdsa_sk_rk_signing');
const SIGN_PUB = `${SIGN_PRIV}.pub`;
const ALLOWED_SIGNERS = path.join(SSH_DIR, 'allowed_signers');
const CONFIG_FILE = path.join(SSH_DIR, 'config');
const MARK_BEGIN = '# >>> se-ssh-setup managed block >>>';
const MARK_END = '# <<< se-ssh-setup managed block <<<';
const ALLOWED_ARGS = new Set(['setup', 'cleanup', 'copy-public-key', 'h', 'help', '_', '$0']);
for (const key of Object.keys(argv)) {
if (!ALLOWED_ARGS.has(key)) die(`Unknown argument: --${key}`);
}
const WANTS_SETUP = Boolean(argv.setup);
const WANTS_CLEANUP = Boolean(argv.cleanup);
const WANTS_COPY = Boolean(argv['copy-public-key']);
const WANTS_HELP = Boolean(argv.h || argv.help);
if ([WANTS_SETUP, WANTS_CLEANUP, WANTS_COPY].filter(Boolean).length > 1) {
die('pass only one of --setup, --cleanup, --copy-public-key');
}
const ACTION = WANTS_HELP ? 'help' : WANTS_CLEANUP ? 'cleanup' : WANTS_SETUP ? 'setup' : WANTS_COPY ? 'copy' : 'help';
function warn(msg) {
console.log(chalk.yellow('⚠'), msg);
}
function die(msg) {
console.error(chalk.red.bold('ERROR:'), msg);
process.exit(1);
}
function printHelp() {
console.log(`${chalk.bold(SCRIPT_NAME)} — macOS Secure Enclave SSH keys for git push + commit signing`);
console.log();
console.log(` ${chalk.cyan('--setup')} set up a push key (Touch ID) and a signing key (no touch)`);
console.log(` ${chalk.cyan('--cleanup')} remove everything --setup created`);
console.log(` ${chalk.cyan('--copy-public-key')} copy a public key to the clipboard`);
console.log(` ${chalk.cyan('-h, --help')} show this help (default)`);
}
async function commandExists(cmd) {
return (await $`command -v ${cmd}`.nothrow()).exitCode === 0;
}
async function requireMacosTools() {
if (os.platform() !== 'darwin') die('this script only runs on macOS');
if (!(await commandExists('sc_auth'))) die('sc_auth not found');
if (!(await commandExists('ssh-keygen'))) die('ssh-keygen not found');
if (!(await fs.pathExists(PROVIDER))) die(`${PROVIDER} not found (no Secure Enclave SSH support on this system)`);
}
async function listIdentities(sshFormat) {
const flags = sshFormat ? ['-t', 'ssh'] : [];
const out = await $`sc_auth list-ctk-identities ${flags}`.nothrow();
return out.stdout
.split('\n')
.slice(1)
.map((line) => line.trim())
.filter(Boolean)
.map((line) => line.split(/\s+/))
.map(([keyType, hash, prot, label]) => ({ keyType, hash, prot, label }));
}
async function sha1HashForLabel(label) {
return (await listIdentities(false)).find((i) => i.label === label)?.hash;
}
async function sshFingerprintForLabel(label) {
return (await listIdentities(true)).find((i) => i.label === label)?.hash;
}
async function fingerprintOfFile(file) {
const out = await $`ssh-keygen -l -f ${file}`.nothrow();
return out.stdout.trim().split(/\s+/)[1];
}
async function resolveIdentity(canonicalLabel, touchPolicy) {
const identities = await listIdentities(false);
const exact = identities.find((i) => i.label === canonicalLabel);
if (exact) return exact;
const candidates = identities.filter((i) => i.prot === touchPolicy);
if (candidates.length > 1) {
die(
`multiple Secure Enclave identities have touch policy '${touchPolicy}' and none is labeled '${canonicalLabel}' ` +
`(${candidates.map((c) => c.label).join(', ')}) — rename/delete the extras with sc_auth so only one remains, then re-run`
);
}
return candidates[0] ?? null;
}
async function resolveOrCreateIdentity(canonicalLabel, touchPolicy) {
const existing = await resolveIdentity(canonicalLabel, touchPolicy);
if (existing) return existing.label;
await $`sc_auth create-ctk-identity -l ${canonicalLabel} -k p-256-ne -t ${touchPolicy} -N ${canonicalLabel}`.nothrow();
if (!(await sha1HashForLabel(canonicalLabel))) die(`failed to create identity '${canonicalLabel}'`);
return canonicalLabel;
}
async function runExportPass(stdin) {
await within(async () => {
cd(SSH_DIR);
await $`printf ${stdin} | ssh-keygen -w ${PROVIDER} -K -N ""`.nothrow();
});
}
async function exportBothKeys(pushLabel, signLabel) {
const pushFp = await sshFingerprintForLabel(pushLabel);
const signFp = await sshFingerprintForLabel(signLabel);
if (!pushFp) die(`can't find fingerprint for ${pushLabel}`);
if (!signFp) die(`can't find fingerprint for ${signLabel}`);
if (
(await fs.pathExists(PUSH_PUB)) &&
(await fs.pathExists(SIGN_PUB)) &&
(await fingerprintOfFile(PUSH_PUB)) === pushFp &&
(await fingerprintOfFile(SIGN_PUB)) === signFp
) {
return;
}
await fs.remove(PUSH_PRIV);
await fs.remove(PUSH_PUB);
await runExportPass('\\nn\\n');
if (!(await fs.pathExists(PUSH_PUB))) die('first export pass produced no key file');
const firstFp = await fingerprintOfFile(PUSH_PUB);
const tmpPriv = path.join(os.tmpdir(), `se-ssh-priv-${process.pid}-${Date.now()}`);
const tmpPub = `${tmpPriv}.pub`;
if (firstFp === pushFp) {
await fs.copy(PUSH_PRIV, tmpPriv);
await fs.copy(PUSH_PUB, tmpPub);
} else if (firstFp === signFp) {
await fs.copy(PUSH_PRIV, SIGN_PRIV);
await fs.copy(PUSH_PUB, SIGN_PUB);
} else {
die(`first exported key fingerprint (${firstFp}) matches neither identity`);
}
await runExportPass('\\ny\\ny\\n');
const secondFp = await fingerprintOfFile(PUSH_PUB);
if (firstFp === pushFp) {
if (secondFp !== signFp) die(`second exported key fingerprint (${secondFp}) != expected signing key`);
await fs.copy(PUSH_PUB, SIGN_PUB, { overwrite: true });
await fs.copy(PUSH_PRIV, SIGN_PRIV, { overwrite: true });
await fs.copy(tmpPriv, PUSH_PRIV, { overwrite: true });
await fs.copy(tmpPub, PUSH_PUB, { overwrite: true });
await fs.remove(tmpPriv);
await fs.remove(tmpPub);
} else {
if (secondFp !== pushFp) die(`second exported key fingerprint (${secondFp}) != expected push key`);
}
await fs.chmod(PUSH_PRIV, 0o600);
await fs.chmod(SIGN_PRIV, 0o600);
await fs.chmod(PUSH_PUB, 0o644);
await fs.chmod(SIGN_PUB, 0o644);
if ((await fingerprintOfFile(PUSH_PUB)) !== pushFp) die("final push key file doesn't match enclave identity");
if ((await fingerprintOfFile(SIGN_PUB)) !== signFp) die("final signing key file doesn't match enclave identity");
}
async function writeSshConfig() {
await fs.ensureDir(SSH_DIR);
await fs.chmod(SSH_DIR, 0o700);
if (!(await fs.pathExists(CONFIG_FILE))) await fs.writeFile(CONFIG_FILE, '');
await fs.chmod(CONFIG_FILE, 0o600);
const existing = await fs.readFile(CONFIG_FILE, 'utf8');
if (existing.includes(MARK_BEGIN)) return;
const block = [
MARK_BEGIN,
'Host *',
' UseKeychain yes',
' AddKeysToAgent yes',
` IdentityFile ${PUSH_PRIV}`,
`SecurityKeyProvider ${PROVIDER}`,
MARK_END,
'',
].join('\n');
await fs.writeFile(CONFIG_FILE, `${block}\n${existing}`);
}
async function loadAgent() {
await $`ssh-add -S ${PROVIDER} ${PUSH_PRIV}`.nothrow().quiet();
await $`ssh-add -S ${PROVIDER} ${SIGN_PRIV}`.nothrow().quiet();
}
async function configureGitSigning() {
const email = (await $`git config --global --get user.email`.nothrow()).stdout.trim();
if (!email) {
warn('git config --global user.email is not set — skipping allowed_signers entry.');
warn('Set it, then re-run this script to finish signing setup.');
}
await $`git config --global gpg.format ssh`;
await $`git config --global user.signingkey ${SIGN_PUB}`;
await $`git config --global commit.gpgsign true`;
await $`git config --global tag.gpgsign true`;
await $`git config --global gpg.ssh.allowedSignersFile ${ALLOWED_SIGNERS}`;
if (email) {
const signPubContent = (await fs.readFile(SIGN_PUB, 'utf8')).trim();
const existingLines = (await fs.pathExists(ALLOWED_SIGNERS))
? (await fs.readFile(ALLOWED_SIGNERS, 'utf8')).split('\n').filter(Boolean)
: [];
const keptLines = existingLines.filter((l) => !l.endsWith('se-ssh-setup'));
keptLines.push(`${email} ${signPubContent} se-ssh-setup`);
await fs.writeFile(ALLOWED_SIGNERS, `${keptLines.join('\n')}\n`);
await fs.chmod(ALLOWED_SIGNERS, 0o600);
}
}
async function doCopyPublicKey() {
if (os.platform() !== 'darwin') die('this script only runs on macOS');
if (!(await commandExists('pbcopy'))) die('pbcopy not found');
const target = (await question('Which key to copy? [push/signing]: ')).trim().toLowerCase();
if (target !== 'push' && target !== 'signing') {
die(`expected 'push' or 'signing', got '${target}'`);
}
const file = target === 'push' ? PUSH_PUB : SIGN_PUB;
if (!(await fs.pathExists(file))) die(`${file} not found — run --setup first`);
const content = (await fs.readFile(file, 'utf8')).trim();
await $`printf '%s' ${content} | pbcopy`;
console.log(chalk.green(`Copied ${target} public key to clipboard.`));
}
async function doSetup() {
console.log(chalk.cyan('Setting up Secure Enclave SSH keys...'));
await requireMacosTools();
await fs.ensureDir(SSH_DIR);
await fs.chmod(SSH_DIR, 0o700);
const pushLabel = await resolveOrCreateIdentity(PUSH_LABEL, 'bio');
const signLabel = await resolveOrCreateIdentity(SIGN_LABEL, 'none');
await exportBothKeys(pushLabel, signLabel);
await writeSshConfig();
await loadAgent();
await configureGitSigning();
const pushPubContent = (await fs.readFile(PUSH_PUB, 'utf8')).trim();
const signPubContent = (await fs.readFile(SIGN_PUB, 'utf8')).trim();
console.log();
console.log(chalk.green.bold('Done.'));
console.log(` push key (Touch ID): ${chalk.cyan(PUSH_PUB)}`);
console.log(` signing key (no touch): ${chalk.cyan(SIGN_PUB)}`);
console.log();
console.log(chalk.bold('Add to GitLab (User Settings -> SSH Keys, or <your-gitlab-host>/-/user_settings/ssh_keys):'));
console.log(` 1. Usage type ${chalk.bold('Authentication')}:`);
console.log(` ${chalk.inverse(pushPubContent)}`);
console.log();
console.log(` 2. Usage type ${chalk.bold.red('Signing')} — ${chalk.red.bold("do NOT pick 'Authentication & Signing'")}:`);
console.log(` ${chalk.inverse(signPubContent)}`);
console.log();
console.log(chalk.yellow(' This key has no Touch ID prompt. If GitLab is allowed to use it for'));
console.log(chalk.yellow(' authentication too, anyone with access to this Mac can push as you'));
console.log(chalk.yellow(' with no fingerprint required. It must only ever be used for signing.'));
console.log(chalk.dim(" If your GitLab version has no separate 'Signing' usage type, don't add"));
console.log(chalk.dim(' this key at all — local commit signing still works, you just lose the'));
console.log(chalk.dim(' Verified badge on GitLab.'));
console.log();
console.log(chalk.dim(`Run './${SCRIPT_NAME} --copy-public-key' to copy either key to the clipboard.`));
}
async function doCleanup() {
await requireMacosTools();
const pushIdentity = await resolveIdentity(PUSH_LABEL, 'bio');
const signIdentity = await resolveIdentity(SIGN_LABEL, 'none');
const pushDesc = pushIdentity ? `'${pushIdentity.label}'` : `'${PUSH_LABEL}' (not found)`;
const signDesc = signIdentity ? `'${signIdentity.label}'` : `'${SIGN_LABEL}' (not found)`;
console.log(chalk.bold('This will:'));
console.log(` - delete Secure Enclave identities ${chalk.magenta(pushDesc)} and ${chalk.magenta(signDesc)}`);
console.log(` - remove ${chalk.cyan(PUSH_PRIV)}, ${chalk.cyan(PUSH_PUB)}, ${chalk.cyan(SIGN_PRIV)}, ${chalk.cyan(SIGN_PUB)}`);
console.log(` - remove this script's block from ${chalk.cyan(CONFIG_FILE)}`);
console.log(` - remove this script's line from ${chalk.cyan(ALLOWED_SIGNERS)}`);
console.log(" - unset git global signing config if it points at these keys");
console.log();
console.log(chalk.red('Any existing SSH/GitHub authorization or verified commit history tied to'));
console.log(chalk.red.bold('these keys will stop working. This cannot be undone.'));
const reply = await question("Type 'yes' to proceed: ");
if (reply !== 'yes') {
console.log(chalk.yellow('Aborted.'));
process.exit(1);
}
for (const identity of [pushIdentity, signIdentity]) {
if (identity) await $`sc_auth delete-ctk-identity -h ${identity.hash}`.nothrow();
}
for (const f of [PUSH_PUB, SIGN_PUB]) {
if (await fs.pathExists(f)) await $`ssh-add -d ${f}`.nothrow();
}
await fs.remove(PUSH_PRIV);
await fs.remove(PUSH_PUB);
await fs.remove(SIGN_PRIV);
await fs.remove(SIGN_PUB);
if (await fs.pathExists(CONFIG_FILE)) {
const content = await fs.readFile(CONFIG_FILE, 'utf8');
if (content.includes(MARK_BEGIN)) {
let skip = false;
const kept = content.split('\n').filter((line) => {
if (line === MARK_BEGIN) { skip = true; return false; }
if (line === MARK_END) { skip = false; return false; }
return !skip;
});
await fs.writeFile(CONFIG_FILE, kept.join('\n'));
}
}
if (await fs.pathExists(ALLOWED_SIGNERS)) {
const kept = (await fs.readFile(ALLOWED_SIGNERS, 'utf8'))
.split('\n')
.filter(Boolean)
.filter((l) => !l.endsWith('se-ssh-setup'));
if (kept.length) {
await fs.writeFile(ALLOWED_SIGNERS, `${kept.join('\n')}\n`);
} else {
await fs.remove(ALLOWED_SIGNERS);
}
}
const currentSigningKey = (await $`git config --global --get user.signingkey`.nothrow()).stdout.trim();
if (currentSigningKey === SIGN_PUB) {
for (const key of ['user.signingkey', 'gpg.format', 'commit.gpgsign', 'tag.gpgsign', 'gpg.ssh.allowedSignersFile']) {
await $`git config --global --unset ${key}`.nothrow();
}
}
console.log(chalk.green.bold('Cleanup done.'));
}
switch (ACTION) {
case 'help':
printHelp();
break;
case 'setup':
await doSetup();
break;
case 'cleanup':
await doCleanup();
break;
case 'copy':
await doCopyPublicKey();
break;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment