Skip to content

About

Nix-packaged Secure Enclave-backed SSH authentication and Git SSH signing for macOS.

Topics

Resources

Stars

15 stars

Watchers

0 watching

Forks

Latest commit

 

History

80 Commits

Folders and files

Repository files navigation

nix-secure-enclave-key

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.

Features

  • 🍎 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.

Motivation

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.

Requirements

  • macOS for sc_auth, Secure Enclave operations, and Apple’s SSH provider
  • Nix on macOS for the packaged nix-secure-enclave-key binary
  • gh is 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.

Without nix-darwin

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)"

SSH login

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.dylib

Connect 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.dylib

Use 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.

GitHub integration

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.

With nix-darwin

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.dylib

The 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.

CTK certificate operations

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.

CLI reference

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.

Secretive migration

nix-secure-enclave-key does not reuse Secretive keys and does not remove Secretive identities automatically. Migrate deliberately:

  1. Keep the existing Secretive key available while creating a new nix-secure-enclave-key identity.
  2. Run nix-secure-enclave-key doctor and nix-secure-enclave-key pub to verify the new stub and public key.
  3. Add the new public key to GitHub as authentication and/or signing, then verify ssh -T git@github.com and a signed test commit.
  4. Update other machines or services that need the new public key.
  5. 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.

GitHub Sponsors

Sponsors

References

About

Nix-packaged Secure Enclave-backed SSH authentication and Git SSH signing for macOS.

Topics

Resources

Stars

15 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages