Skip to content

Instantly share code, notes, and snippets.

@arianvp
Last active November 25, 2025 07:31
Show Gist options
  • Select an option

  • Save arianvp/5f59f1783e3eaf1a2d4cd8e952bb4acf to your computer and use it in GitHub Desktop.

Select an option

Save arianvp/5f59f1783e3eaf1a2d4cd8e952bb4acf to your computer and use it in GitHub Desktop.
Native Secure Enclaved backed ssh keys on MacOS

Native Secure Enclave backed ssh keys on MacOS

It turns out that MacOS Tahoe can generate and use secure-enclave backed SSH keys! This replaces projects like https://github.com/maxgoedjen/secretive

There is a shared library /usr/lib/ssh-keychain.dylib that traditionally has been used to add smartcard support to ssh by implementing PKCS11Provider interface. However since recently it also implements SecurityKeyProivder which supports loading keys directly from the secure enclave! SecurityKeyProvider is what is normally used to talk to FIDO2 devices (e.g. libfido2 can be used to talk to your Yubikey). However you can now use it to talk to your Secure Enclave instead!

recording.mov

Seems this method was first discovered in https://lists.mindrot.org/pipermail/openssh-unix-dev/2024-July/041451.html

Key setup

See man sc_auth and man ssh-keychain for all the options

To create a Secure Enclave backed key that requires biometrics, run the following command and press TouchID:

% sc_auth create-ctk-identity -l ssh -k p-256-ne -t bio

You can confirm that the key was create with the list-ctk-identities command:

arian@Mac ssh-keychain % sc_auth  list-ctk-identities       
Key Type Public Key Hash                          Prot Label Common Name Email Address Valid To        Valid 
p-256-ne A71277F0BC5825A7B3576D014F31282A866EF3BC bio  ssh   ssh                       23.11.26, 17:09 YES

It also supports listing the ssh key fingerprints instead:

% sc_auth  list-ctk-identities -t ssh
Key Type Public Key Hash                                    Prot Label Common Name Email Address Valid To        Valid 
p-256-ne SHA256:vs4ByYo+T9M3V8iiDYONMSvx2k5Fj2ujVBWt1j6yzis bio  ssh   ssh                       23.11.26, 17:09 YES 

Keys can be deleted with

% sc_auth delete-ctk-identity -h <Public Key Hash>

Usage with ssh

You can "download" the public / private keypair from the secure enclave using the following command:

% ssh-keygen -w /usr/lib/ssh-keychain.dylib -K -N ""
Enter PIN for authenticator: 
You may need to touch your authenticator to authorize key download.
Saved ECDSA-SK key to id_ecdsa_sk_rk
% cat id_ecdsa_sk_rk.pub 
[email protected] AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBKiHAiAZhcsZ95n85dkNGs9GnbDt0aNOia2gnuknYV2wKL3y0u+d3QrE9cFkmWXIymHZMglL+uJA+6mShY8SeykAAAAEc3NoOg== ssh:

You can just use the empty string for PIN. For some reason openssh always asks for it even if the authenticator in question does not use a PIN but a biometric. Note that the "private" key here is just a reference to the FIDO credential. It does not contain any secret key material. Hence I'm specifiyng -N "" to skip an encryption passphrase.

Now if you copy this public key to your authorized keys file, it should work!

% ssh-copy-id -i id_ecdsa_sk_rk localhost
% ssh -o SecurityKeyProvider=/usr/lib/ssh-keychain.dylib localhost

Usage with ssh-agent

Instead of downloading the public/private keypair to a file you can also directly make the keys available to ssh-agent. For this you can use the following command:

% ssh-add -K -S /usr/lib/ssh-keychain.dylib
Enter PIN for authenticator: 
Resident identity added: ECDSA-SK SHA256:vs4ByYo+T9M3V8iiDYONMSvx2k5Fj2ujVBWt1j6yzis
% ssh-add -L
[email protected] AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBKiHAiAZhcsZ95n85dkNGs9GnbDt0aNOia2gnuknYV2wKL3y0u+d3QrE9cFkmWXIymHZMglL+uJA+6mShY8SeykAAAAEc3NoOg== 
% ssh-copy-id localhost
% ssh -o SecurityKeyProvider=/usr/lib/ssh-keychain.dylib localhost

Using the SecurityKeyProvider by default

SecurityKeyProvider can be configured in .ssh/config but I recommend setting export SSH_SK_PROVIDER=/usr/lib/ssh-keychain.dylib in your .zprofile instead as that environment variable gets picked up by ssh, ssh-add and ssh-keygen.

This means you can just do:

ssh-add -K
ssh my-server

or

ssh-keygen -K
ssh -i id_ecdsa_rk_sk my-server

to ssh into your server

Exportable keys

There's also an exportable variant where the private key is encrypted using the secure enclave as opposed to generated on the secure enclave. This is might be considered less secure but is convenient for key backup.

% sc_auth create-ctk-identity -l ssh-exportable -k p-256 -t bio
% sc_auth list-ctk-identities
p-256    A581E5404ED157C4C73FFDBDFC1339E0D873FCAE bio  ssh-exportable ssh-exportable               23.11.26, 19:50 YES  
% sc_auth export-ctk-identity -h A581E5404ED157C4C73FFDBDFC1339E0D873FCAE -f ssh-exportable.pem
Enter a password which will be used to protect the exported items:
Verify password:

You can then re-import it on another device

% sc_auth import-ctk-identities -f ssh-exportable.pem.p12 -t bio
Enter PKCS12 file password:
@sandstrom
Copy link

I reverse-engineered the ssh-keychain.dylib using binary ninja and some help of Claude.

Wow! 🙂

Asking because setting .biometryCurrentSet is a nice feature in Secretive, since it (a) makes it impossible to extract the key and (b) if we know who setup the biometrics, it's impossible to later add new biometrics [without discarding keys] to circumvent.

Nice to hear that it might be possible with this approach too!

@arianvp
Copy link
Author

arianvp commented Nov 24, 2025

So the reverse-engineered ssh-keychain.dylib happily does sk_sign and sk_load_resident_keys; even when not code-signed; as apple disables library validation on the ssh-sk-helper: https://github.com/apple-oss-distributions/OpenSSH/blob/main/Entitlements/ssh-sk-helper.entitlements

However, my implementation of sk_enroll fails with:

ssh-sk-helper[51504]/1#1 LF=0 add Error Domain=NSOSStatusErrorDomain Code=-34018 "Client has neither com.apple.application-identifier nor com.apple.security.application-groups nor keychain-access-groups entitlements" UserInfo={numberOfErrorsDeep=0, NSDescription=Client has neither com.apple.application-identifier nor com.apple.security.application-groups nor keychain-access-groups entitlements}

the ssh-sk-helper doesn't have an entitlement that allows it to store an item in a keychain. Even if it did,
The keys are stored in kSecAttrAccessGroupToken access group (which is com.apple.token) ; so ssh-sk-helper would need an entitlement for directly creating keys in that access group. Which it doesn't have.

So we're out of luck. Keys need to be created by the sc_auth tool; which only supports enabling or disabling biometrics. But not any more advanced flags than that.

@ukd1
Copy link

ukd1 commented Nov 24, 2025

Would be nice to not have to press enter for the empty password on ssh-add -K - anyone solved that?

@arianvp
Copy link
Author

arianvp commented Nov 24, 2025

From the openssh code I expected:

echo "" | ssh-add -K

to work; but it doesn't.

My hacky workaround has been:

SSH_ASKPASS_REQUIRE=force SSH_ASKPASS=echo ssh-add -K

@andreykeen
Copy link

In case there are multiple keys in the enclave:

$ sc_auth list-ctk-identities
Key Type Public Key Hash                          Prot Label  Common Name Email Address Valid To          Valid
p-256    BDD6A944F33DDF745705DD17FFC218A32713FB80 bio  test-1 test-1                    25.11.2026, 00:18 YES
p-256    996803631F7F4E9FB631861EB92501363589CC27 bio  test-2 test-2                    25.11.2026, 00:18 YES
p-256    1B526665B17E3B987E3F95927BA79C8E6579E454 bio  test-3 test-3                    25.11.2026, 00:19 YES

the following command goes through all keys: it exports the private/public keypair of the test-1 key and creates id_ecdsa_sk_rk/id_ecdsa_sk_rk.pub files, then it automatically tries to export the second key – test-2, and asks for overwriting of the existing id_ecdsa_sk_rk/id_ecdsa_sk_rk.pub files, and then it goes to the third key – test-3.

$ ssh-keygen -w /usr/lib/ssh-keychain.dylib -K -N ""
Enter PIN for authenticator:
You may need to touch your authenticator to authorize key download.
Saved ECDSA-SK key to id_ecdsa_sk_rk
id_ecdsa_sk_rk already exists.
Overwrite (y/n)? y
Saved ECDSA-SK key to id_ecdsa_sk_rk
id_ecdsa_sk_rk already exists.
Overwrite (y/n)? y
Saved ECDSA-SK key to id_ecdsa_sk_rk

So, if I have more than three keys in the enclave and I want to get the public key of the key that appears in the middle on the list, the process becomes ridiculous.
I haven't found a way to specify a particular key for this kind of export.

Is there any way to do this more easily?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment