macOS-only, Nix-first tooling for Secure Enclave-backed SSH authentication and Git SSH signing.
nix-secure-enclave-key manages macOS CryptoTokenKit identities backed by the
Secure Enclave and exposes them as SSH authentication and Git SSH signing keys. The
private key never leaves the Secure Enclave. The repository contains only the
Nushell CLI, Nix integration, and configuration examples; it does not contain a
private key or an exported identity.
The project is inspired by Secretive and its Secure Enclave-backed SSH workflow. It talks to macOS CryptoTokenKit and Apple's SSH provider directly, so Secretive is not a runtime dependency.
- 🍎 macOS only: Built on Apple CryptoTokenKit, Secure Enclave, and the Apple SSH provider; Linux and other platforms are unsupported.
- 🔐 Secure Enclave identities: Create and idempotently manage a macOS CryptoTokenKit identity and its non-secret SSH stub or public-key reference.
- 🔑 SSH login: Use the key with any SSH server where the public key is authorized.
- ✍️ Git SSH signing: Sign Git commits with the same Secure Enclave-backed key.
- 🐙 GitHub registration: Register the public key for authentication, signing, or both, while skipping keys that are already registered.
- 📜 CTK certificates: Create certificate signing requests and import CTK certificates.
- ❄️ Nix first: Use the package with nix-darwin and Home Manager without running Secure Enclave or GitHub operations as root during activation.
I used Secretive for a long time and was happy with its Secure Enclave-backed SSH workflow. The friction was its GUI-only control path: configuring and operating the key from shell-based workflows, especially coding agents, was awkward. When the Mac was asleep, a coding agent could not complete a signing request, which was especially frustrating for an otherwise declarative and automated workflow.
nix-secure-enclave-key keeps the Secure Enclave protection while making the
identity, SSH stub/reference, Git signing, and GitHub registration workflows available as
an explicit CLI and Nix configuration. Secretive remains an inspiration, not a
runtime dependency, and this project never reuses or deletes its identities.
- macOS for
sc_auth, Secure Enclave operations, and Apple’s SSH provider - Nix on macOS for the packaged
nix-secure-enclave-keybinary ghis only needed for GitHub registration; the Nix modules add it automatically when opt-in registration is enabled
This is a Darwin-only Nix package. Linux and other platforms are unsupported; the project does not generate fallback SSH keys outside the Secure Enclave.
Install the package with Nix, then perform the first identity creation as the logged-in user on the Mac:
nix profile install github:ryoppippi/nix-secure-enclave-key
nix-secure-enclave-key doctor
nix-secure-enclave-key setup
nix-secure-enclave-key pub
For a one-off invocation without installing the package, pass the command to
nix run:
nix run github:ryoppippi/nix-secure-enclave-key -- doctor
The defaults are:
key file: ~/.ssh/id_enclave_key
label: nix-secure-enclave-key
protection: none
--protection none creates the Secure Enclave identity without biometric
protection. --protection bio requests biometric protection; macOS may require
Touch ID when that identity is created or used. These values are passed to
sc_auth(8).
identity ensure is idempotent: it creates a missing CTK identity and SSH
stub/reference, leaves existing pairs alone, and rejects an incomplete
key/public pair. It never deletes an identity. When Apple’s resident-key
download cannot select one identity among several, the CLI writes a public-key
reference instead; the Secure Enclave private key is still never written to
disk. The Git signer loads that reference through an isolated temporary
ssh-agent when needed; the agent receives Secure Enclave handles and only
exposes public keys.
The generated SSH key is not GitHub-specific. It can authenticate to any SSH
server where its public key is present in authorized_keys; GitHub registration
is only needed when using the key with GitHub.
Configure SSH authentication for all hosts and Git signing manually if you are not using nix-darwin or Home Manager:
Host *
IdentityFile ~/.ssh/id_enclave_key
SecurityKeyProvider /usr/lib/ssh-keychain.dylib
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_enclave_key
git config --global gpg.ssh.program "$(command -v nix-secure-enclave-key-git-sign)"
The generated key is a general SSH authentication key, not a GitHub-only key.
Connect to another SSH server with Touch ID
Create an identity dedicated to interactive SSH login and request biometric protection when it is created:
nix-secure-enclave-key setup \
--key-file ~/.ssh/id_enclave_login \
--label nix-secure-enclave-key-login \
--protection bio
nix-secure-enclave-key pub --key-file ~/.ssh/id_enclave_login
Add the line printed by pub to the remote account's
~/.ssh/authorized_keys, then configure the local Mac:
Host my-server
HostName ssh.example.com
User your-user
IdentityFile ~/.ssh/id_enclave_login
IdentitiesOnly yes
SecurityKeyProvider /usr/lib/ssh-keychain.dylibConnect normally:
ssh my-server
bio asks macOS to protect this identity with biometrics and may prompt for
Touch ID when the key is created or used. The file named in IdentityFile is a
stub or public-key reference, not the private key. Only the public key belongs
on the server; the Secure Enclave private key cannot be viewed or exported.
Use separate identities for Git signing and SSH servers
Create one identity for unattended Git signing and another for an interactive server connection. Each identity needs its own label and SSH stub/reference:
nix-secure-enclave-key setup \
--key-file ~/.ssh/id_git_signing \
--label git-signing-nix-secure-enclave-key \
--protection none
nix-secure-enclave-key setup \
--key-file ~/.ssh/id_remote_server_x \
--label remote-server-x-ssh-login-key-nix-secure-enclave-key \
--protection bio
nix-secure-enclave-key pub --key-file ~/.ssh/id_git_signing
nix-secure-enclave-key pub --key-file ~/.ssh/id_remote_server_x
Point Git at the signing stub/reference and the SSH client at the server stub/reference:
git config --global user.signingkey ~/.ssh/id_git_signing
Host remote-server-x
HostName ssh.example.com
User your-user
IdentityFile ~/.ssh/id_remote_server_x
IdentitiesOnly yes
SecurityKeyProvider /usr/lib/ssh-keychain.dylibUse the desired --key-file when registering a specific identity with GitHub.
The names above are examples; create additional labels, stub/reference paths,
and Host blocks for other servers. If several identities are present, the
CLI selects the requested public key before writing the local reference, so no
Secure Enclave private key is exported.
Register the same public key for one or both purposes:
nix-secure-enclave-key github add --type signing
nix-secure-enclave-key github add --type authentication
nix-secure-enclave-key github add --type both
Before calling gh ssh-key add, the CLI compares the key algorithm and base64
key body with the authenticated account’s existing keys. Matching keys are
skipped regardless of their title. If gh is not on PATH, unauthenticated, or
cannot access the endpoint, the CLI prints a prompt containing only the public
key path and registration command. It never asks for, prints, or copies the
private key.
Use --prompt-only to print those commands without contacting GitHub.
Configure automatic GitHub key registration
When github.autoAdd = true, gh must be authenticated with permission to
read and add SSH authentication and signing keys. Refresh the GitHub CLI
scopes before running darwin-rebuild switch or Home Manager activation:
gh auth refresh --hostname github.com --scopes admin:ssh_signing_key,admin:public_key
GitHub treats authentication and signing registrations separately. Therefore,
--type both may add the same public key twice, once for each purpose. If the
scope refresh or endpoint access is unavailable, activation does not fail or
request a secret; it prints the equivalent gh ssh-key add commands for
manual execution.
When --title is omitted, the CLI generates a machine-specific title from the
identity prefix, macOS LocalHostName, and the first part of the public-key
fingerprint. The default identity is titled like
nix-secure-enclave-key-macbook-pro-j1lHgxmxJT5g; a named Nix identity such as
git-signing is titled like
git-signing-macbook-pro-j1lHgxmxJT5g-nix-secure-enclave-key. This keeps shared
dotfiles readable when multiple Macs register different keys. Set --title or
github.title when a fixed title is required.
The nix-darwin module is the primary integration and does not require Home Manager. It installs the package, configures the system SSH client, configures Git SSH signing, and runs local identity setup as the primary user during activation.
The flake exposes packages.<system>.default, darwinModules.default,
homeManagerModules.default, and overlays.default.
Set system.primaryUser, then configure the module in the system configuration:
{
inputs.nix-secure-enclave-key.url = "github:ryoppippi/nix-secure-enclave-key";
modules = [
inputs.nix-secure-enclave-key.darwinModules.default
({ ... }: {
system.primaryUser = "your-macOS-user";
programs.nix-secure-enclave-key = {
enable = true; # Install the package and configure SSH/Git integration.
identities = {
git-signing = {
keyFile = "~/.ssh/id_enclave_key"; # Non-secret SSH stub/reference for login and signing.
label = "nix-secure-enclave-key"; # Reuse this existing CryptoTokenKit identity.
protection = "none"; # "bio" requests biometric protection and may require Touch ID.
autoEnsure = true; # Create the identity and SSH stub/reference during activation.
github = {
autoAdd = false; # Set true to register the public key during activation.
type = "both"; # Register SSH authentication, Git signing, or both.
# title defaults to a machine- and public-key-specific value; set it to override that title.
};
};
};
signingIdentity = "git-signing"; # Select the identity used by Git SSH signing.
signByDefault = true; # Sign Git commits and tags with the Secure Enclave-backed key.
};
})
];
}autoEnsure invokes identity ensure as the configured primary user, not as
root. github.autoAdd is disabled by default. When enabled, nix-darwin adds
gh to the system package set and invokes github add as the primary user;
the CLI compares the public key first and skips keys already registered with
GitHub. This means darwin-rebuild switch can complete the local setup and,
optionally, both GitHub registrations without Home Manager.
Configure identities declaratively
Use a named attribute set for Secure Enclave identities. This is also the
required form when only one identity is configured. When Git signing and SSH
login should use different identities, add another attribute. Attribute names
are arbitrary. If label is omitted, the module derives it as
<attribute-name>-nix-secure-enclave-key.
The former top-level keyFile, label, protection, autoEnsure, and
github options are not accepted; move them under a named identity and set
signingIdentity explicitly.
When github.title is omitted, the module passes the attribute name to the
CLI so the generated GitHub title also includes the Mac name and public-key
fingerprint.
{
programs.nix-secure-enclave-key = {
enable = true; # Install the package and configure SSH/Git integration.
identities = {
git-signing = {
keyFile = "~/.ssh/id_git_signing"; # Non-secret stub used for Git signing.
protection = "none"; # Allow unattended signing without Touch ID.
# label defaults to "git-signing-nix-secure-enclave-key".
github.autoAdd = true; # Register this identity with GitHub during activation.
# github.title defaults to a machine- and public-key-specific value.
};
remote-server-x-ssh-login-key = {
keyFile = "~/.ssh/id_remote_server_x"; # Non-secret stub used for this SSH host.
protection = "bio"; # Request Touch ID protection when the key is used.
# label defaults to "remote-server-x-ssh-login-key-nix-secure-enclave-key".
};
};
signingIdentity = "git-signing"; # Select the identity used by Git SSH signing.
signByDefault = true; # Sign Git commits and tags by default.
};
}All named identities are added to the SSH configuration. Select a particular server identity with a host-specific block, for example:
Host remote-server-x
HostName ssh.example.com
User your-user
IdentityFile ~/.ssh/id_remote_server_x
IdentitiesOnly yes
SecurityKeyProvider /usr/lib/ssh-keychain.dylibThe same identities and signingIdentity options are available in the
standalone Home Manager module.
Use standalone Home Manager instead of nix-darwin
Home Manager is optional. Import its module and use the same
programs.nix-secure-enclave-key options:
{
imports = [ inputs.nix-secure-enclave-key.homeManagerModules.default ];
programs.nix-secure-enclave-key = {
enable = true; # Install the package and configure SSH/Git integration.
identities = {
git-signing = {
keyFile = "~/.ssh/id_enclave_key"; # SSH stub/reference for login and Git signing; its public key is in the .pub file.
label = "nix-secure-enclave-key"; # Reuse this existing CryptoTokenKit identity.
protection = "none"; # "none" skips biometric protection; "bio" requests it and may require Touch ID.
autoEnsure = true; # Create the missing identity and SSH stub/reference during user activation.
github = {
autoAdd = false; # Register the public key during activation; disabled by default because this writes to GitHub.
type = "both"; # Register the key for SSH authentication, Git signing, or both.
# title defaults to a machine- and public-key-specific value; set it to override that title.
};
};
};
signingIdentity = "git-signing"; # Select the identity used by Git SSH signing.
signByDefault = true; # Make Git SSH signing the default for commits and tags.
};
}When autoEnsure is enabled in the Home Manager module, the same operations
run during user activation. GitHub registration remains skipped unless
github.autoAdd = true is explicitly set.
For a real-world configuration, see the Home Manager module in use in ryoppippi's dotfiles, where it is imported through nix-darwin's Home Manager integration.
Show CTK certificate commands
The CTK identity can also be used outside SSH:
nix-secure-enclave-key identity list
nix-secure-enclave-key ctk csr <public-key-hash> <output-file>
nix-secure-enclave-key ctk import-certificate <certificate-file>
These commands preserve the broader CryptoTokenKit identity model and do not assume that every identity is an SSH identity.
The options below apply to the commands shown above:
| Argument | Accepted values | Default | Applies to |
|---|---|---|---|
--key-file |
Path to the SSH stub or public-key reference; its public key is <path>.pub |
~/.ssh/id_enclave_key |
setup, identity ensure, ssh ensure, pub, github add, doctor |
--label |
Any non-empty CryptoTokenKit identity label | nix-secure-enclave-key |
setup, identity ensure, ssh ensure |
--protection |
none: no biometric protection; bio: biometric protection, which may require Touch ID |
none |
setup, identity ensure, ssh ensure |
--copy |
Flag; no value | Off | pub |
--type |
signing, authentication, or both |
both |
github add |
--title |
Any GitHub SSH key title | Generated from the identity prefix, machine name, and public-key fingerprint | github add |
--title-prefix |
Any title prefix; normally an identity attribute name | nix-secure-enclave-key |
github add |
--prompt-only |
Flag; no GitHub API or write is performed | Off | github add |
none is the non-biometric mode. bio requests biometric protection for the
Secure Enclave identity and may require Touch ID when the identity is created or
the key is used. --type selects GitHub's signing-key
endpoint, authentication-key endpoint, or both. Existing GitHub keys are
matched by algorithm and key body, not by title.
When --title is omitted, --title-prefix controls the identity portion of
the generated machine-specific title.
The CTK commands use positional arguments: ctk csr takes an identity hash
from identity list and the output-file path for the CSR; ctk import-certificate takes the certificate-file path to import.
nix-secure-enclave-key does not reuse Secretive keys and does not remove
Secretive identities automatically. Migrate deliberately:
- Keep the existing Secretive key available while creating a new
nix-secure-enclave-keyidentity. - Run
nix-secure-enclave-key doctorandnix-secure-enclave-key pubto verify the new stub and public key. - Add the new public key to GitHub as authentication and/or signing, then
verify
ssh -T git@github.comand a signed test commit. - Update other machines or services that need the new public key.
- Remove the old Secretive key only after all consumers have been migrated, using Secretive itself or an explicit user action.
No Secure Enclave material belongs in a dotfiles repository. The .pub file
and the SSH stub/reference are local artifacts and should be protected with
normal SSH file permissions.