在 macOS 上使用 Touch ID 配置 SSH 密钥
摘要
一篇教程博文,讲解如何在 macOS 上利用 Secure Enclave 和 Touch ID 创建不可提取的 SSH 身份密钥,并配置 SSH 与 GitHub 认证,同时深入解释其底层工作原理。
<p><a href="https://lobste.rs/s/d7m5j1/setting_up_ssh_keys_on_macos_with_touch_id">评论</a></p>
查看缓存全文
缓存时间: 2026/10/21 06:20
# Nicolas Mattia – 在 macOS 上使用 Touch ID 配置 SSH 密钥
来源:https://nmattia.com/posts/2026-10-19-setting-up-ssh-keys-touch-id/
2026年10月19日
---
博客文章社交卡片
在这篇文章的第一部分,我将展示在 Mac 上创建 SSH 密钥的一种简单且安全的方法(只需 3 条命令,包括对 GitHub 的支持)。第二部分则会深入讲解背后到底发生了什么,供好奇的读者参考。
Mac 拥有内置的所谓 [Secure Enclave](https://support.apple.com/en-gb/guide/security/sec59b0b31ff/web),我们可以在其中创建"身份",这些身份根本无法被盗取(除非你和某个国家机器结了仇)。这些身份受 Touch ID 保护,可用于 SSH 身份认证。
相比传统的 SSH 密钥文件,使用这种方法有很多好处:
- 私钥材料无法被提取、从未显示、无法被盗取,
- 每次使用该身份时都会提示你确认,
- 不会有 `~/.ssh/id_ed25519.pub` 之类的密钥文件散落在文件系统中;也不会有误删的风险。
这种额外的安全性会在你每次使用 SSH 时生效,包括像 GitHub 这样的 git-over-ssh。像这样先进的安全技术默认就出现在世界上最广泛使用的电脑中,真是太好了!几年前,这样的东西只能在 CERN 和/或丹·布朗的小说里找到。
让我们看看如何在你日常使用的 Mac 上配置它。
> 人机界面的设计必须以易用为目标,这样用户才会自然而然且自动地正确使用保护机制。
> —— Saltzer 和 Schroeder,《计算机系统中的信息保护》
## 快速配置
我们将在 Secure Enclave 上创建一个安全身份,然后告诉 SSH 使用它进行身份认证,最后将密钥添加到 GitHub。
### 第 1 步:创建你将在这台 Mac 上使用的身份
```
sc_auth create-ctk-identity -l ssh-key -k p-256-ne -t bio
# 运行一次以创建身份
```
系统会提示你触摸"Touch ID"。你的身份现已创建并安全保存(各参数的含义在下方[深度解析](https://nmattia.com/posts/2026-10-19-setting-up-ssh-keys-touch-id/#under-the-hood)部分有解释)。
### 第 2 步:告诉 SSH 使用该身份(提示输入 PIN 时,直接按回车即可):
```
ssh-add -K -S /usr/lib/ssh-keychain.dylib
# 重启后运行一次
```
从这以后,SSH 将知道在通过 SSH 连接时使用 Secure Enclave 中的身份;不过它的记性不太好,所以重启后需要你再提醒它一次。
### 第 3 步:当你需要将公钥注册到新服务器(如 GitHub)时,运行以下命令:
```
ssh-add -L
# 需要公钥时运行
```
如果列出了多个身份,请使用以 `sk-ecdsa-sha2-nistp256...` 开头的那一行。它是该身份的一个可共享句柄,这就是 GitHub 会询问的 SSH 密钥。
图片
*`ssh-add -L` 的输出可以直接添加为 GitHub 的 SSH 密钥*
这就是超级安全配置所需的全部操作!新的身份现在可以用于 SSH,认证时会弹出 Touch ID 提示:
```
git push
# 触发 Touch ID 提示
```
没有人能盗取你的身份(事实上,你自己甚至无法主动交出去,因为它无法从 Secure Enclave 中提取)。但能力越大……如果你的 Mac 坏了,身份也随之消亡,你需要创建一个新的(并在需要的地方重新注册,比如 GitHub)。
图片
*当 git 尝试通过 SSH 认证时,会弹出 Touch ID 提示*
如果你好奇这是如何工作的,请继续往下读!
## 深度解析
现在我们来深入理解底层发生了什么。我们将从 Secure Enclave 中实际创建身份开始,然后看看 SSH 如何通过 `ssh-keychain.dylib` 与 Secure Enclave 交互,最后看看完整的 SSH 登录流程中发生了什么。
这是阅读文章(如 [`arianvp` 的精彩调查](https://gist.github.com/arianvp/5f59f1783e3eaf1a2d4cd8e952bb4acf))、手册页和实验的结果。其中很多内容没有文档记录,以下是我构建出心智模型并说服自己这一切确实有效所需的最小信息量。
> 下面的部分命令输出为了可读性做了截断,如果你想要完整的体验,建议在本地运行它们!
### 在 Secure Enclave 上创建身份
身份通过 `sc_auth` 命令管理(其中 `sc_` 代表 Smart Card)。在 Apple 的术语中,它们被称为"CryptoTokenKit"身份,简称 CTK 身份。为什么不叫 Smart Card 身份?Smart Card 是什么?不知道!Apple 的术语体系里有很多奇怪的用词,但实践中它们只是存储在 Secure Enclave 上的私钥:
```
$ sc_auth list-ctk-identities
Key Type Public Key Hash Prot Label
p-256-ne 458F712F281B53C683F23FC02E69D44DAFDAA3DE bio ssh-key
```
如果你按照[快速配置](https://nmattia.com/posts/2026-10-19-setting-up-ssh-keys-touch-id/#quick-setup)操作,这里应该恰好有一个身份,那就是你运行 `sc_auth create-ctk-identity` 命令时创建的。
让我们更详细地看看 `create-ctk-identity` 命令:
```
sc_auth create-ctk-identity -l <label> -k <key type> -t <protection>
```
使用 `p-256-ne` 密钥类型时,该命令会在 Secure Enclave 上创建身份——我猜测过程是先收集一些熵源,然后生成一些私钥材料,并分配所需的簿记空间,确保它只能通过你的指纹访问。
标签(label)是自由格式的,只是附加在密钥上的一个备注。密钥类型是一个加密设置(使用的椭圆曲线)。在撰写本文时,接受的值有:`p-256 | p-384 | p-521 | p-256-ne | p-384-ne`。
`-ne` 后缀表示"Non-Extractable"(不可提取),意味着密钥由 Enclave 自身生成,并且*无法*从 Secure Enclave 中提取。对于 SSH,我们只需要*签名*功能,而签名由 Secure Enclave 自身完成(且在 Secure Enclave 上),我们不需要也不需要导出私钥。
`p-384` 曲线不被(Open)SSH 支持,所以范围缩小到了 `p-256-ne`。
最后,`protection` 可以是 `bio` 或 `none`。设置为 `bio` 时,使用密钥需要生物识别(如 Touch ID)。设置为 `none` 时,密钥*仍然*无法被提取(即被盗),*但*程序可以静默使用 SSH。我建议使用 `bio`,但你可能有不同的想法;如果你觉得每次 `git pull` 都弹出 Touch ID 提示很烦,也许可以使用 `none`。
现在我们在 Secure Enclave 上拥有了一个神秘的身份,接下来让我们看看如何告诉 SSH 使用它进行身份认证。
### 在 SSH 和 Secure Enclave 之间做翻译
两者之间存在一定的阻抗失配,默认情况下 `ssh` 不知道如何使用该身份:
```
$ ssh -T [email protected]
[email protected]: Permission denied (publickey).
```
SSH agent 无法直接使用 Secure Enclave 身份,但 Apple 提供了一个小型翻译层——以 `ssh-keychain.dylib` 的形式,这是一个在所有近期 macOS 版本中原生提供的可通过 `dlopen` 加载的库:
```
$ ssh-add -K -S /usr/lib/ssh-keychain.dylib
...
Resident identity added: ECDSA-SK SHA256:QZ+Cx6/AkS9sek9INtsXt6t7wuHOtA555CLJu8a8V7w
```
这里我们利用了 `ssh-add` 通过 `-K` 参数内置的对 FIDO 风格认证器的支持。这告诉 SSH agent 不应该读取标准私钥文件(比如 `~/.ssh/id_ed25519`),而是应该预期密钥存储在"外部"(姑且这么说吧)。
仅使用 `-K` 时,`ssh-add` 会在 USB 设备上寻找公钥(想想 YubiKey);但我们可以用 `-S <provider>` 注册一个"安全密钥提供者"来自定义此行为。该提供者应该是一个无状态的动态可加载库,而 `/usr/lib/ssh-keychain.dylib` 正是如此。
该接口没有文档记录(至少没有我能有效理解的文档),但我们可以看看 `ssh-keychain.dylib` 暴露了什么:
```
$ dyld_info -exports /usr/lib/ssh-keychain.dylib
/usr/lib/ssh-keychain.dylib:
-exports:
offset symbol
0x00001A2C _C_GetFunctionList
0x00000F30 _sk_api_version
0x00000F38 _sk_enroll
0x0000136C _sk_load_resident_keys
0x00000F40 _sk_sign
```
最有可能的情况是,`_sk_load_resident_keys` 用于返回一组身份(的公钥),而 `_sk_sign` 使用指定的身份对负载进行签名(这里,resident 意为"驻留在如 Secure Enclave 上")。
我们已经了解了如何在 Secure Enclave 上创建身份,以及它们如何(理论上)被 SSH agent 使用。让我们实际测试一下,看看创建 SSH 连接时会发生什么。
### 使用 ssh-keychain 提供者的 SSH 登录流程
让我们看看认证到远程 SSH 服务器需要什么。SSH agent 始终需要:(1) 一组可用于认证的(公钥)列表,以及 (2) 向服务器证明自己拥有其中任一公钥对应私钥的方法。
当 `ssh-add` 首次使用 `-S <provider>` 调用时,`_sk_load_resident_keys` 例程会被调用,它返回 FIDO 风格认证器上所有驻留密钥的公钥。这些公钥(连同其对应的提供者,即 `ssh-keychain.dylib`)存储在 SSH agent 中,之后会作为候选提供给我们尝试连接的任何远程 SSH 服务器。(另外,由于 SSH agent 现在知道了我们之前创建的 Secure Enclave 身份,我们可以用 `ssh-add -L` 查看它的 SSH 风格公钥。)
假设发起了一次 SSH 连接:远程服务器接受了 agent 提供的其中一个公钥,并"质询"客户端证明其身份。agent 调用 `_sk_sign`,使用私钥(与被接受的公钥匹配)和质询(大体上说)创建签名。这将转发给 Secure Enclave,弹出 Touch ID 提示,如果批准,则返回一个针对该质询的签名。签名被转发回服务器,服务器可以确认我们——客户端——拥有相应的私钥。
让我们看看实际中是什么样子:
```
$ ssh -T [email protected]
[email protected]: Permission denied (publickey).
$ ssh-add -K -S /usr/lib/ssh-keychain.dylib
Enter PIN for authenticator:
Resident identity added: ...
$ ssh -vvv -T [email protected]
...
debug1: get_agent_identities: agent returned 1 keys
debug1: Offering public key: ssh: ECDSA-SK SHA256:QZ+Cx6/AkS9sek9INtsXt6t7wuHOtA555CLJu8a8V7w authenticator agent
debug1: Server accepts key: ssh: ECDSA-SK SHA256:QZ+Cx6/AkS9sek9INtsXt6t7wuHOtA555CLJu8a8V7w authenticator agent
debug3: sign_and_send_pubkey: signing using [email protected] SHA256:QZ+Cx6/AkS9sek9INtsXt6t7wuHOtA555CLJu8a8V7w
Authenticated to github.com ([20.250.119.67]:22) using "publickey".
Hi nmattia! You've successfully authenticated, but GitHub does not provide shell access.
```
`ssh` 的调试日志显示 agent 提供了一个密钥,被服务器接受,然后用于创建签名。服务器验证签名后,认证通过(尽管 `GitHub 不提供 shell 访问权限`)。
有一点值得注意:每当我们将身份注册到 SSH agent 时,`ssh-add` 都会提示输入 PIN。这是为什么?为什么输入空 PIN 就能工作,这不是很不安全吗‽ 嗯,不,并不不安全!当我们告诉 `ssh-add` 使用 FIDO 风格认证器时,我猜测其默认行为是要求输入 PIN,因为它假设我们将使用 USB YubiKey 风格的认证器;只不过我们用 `ssh-keychain.dylib` 走了捷径(安全性不是来自 PIN,而是来自你指尖下的柔软皮肤;酷多了)。在可用性方面不算好,但*我猜*是有道理的。(我不知道如果有多个 FIDO 认证器会怎样;如果想支持我,可以在 [GitHub](https://github.com/sponsors/nmattia) 上赞助我,我会买一堆 YubiKey 来实验)
## 总结
回顾一下:我们使用了一个有点神秘、文档不足的"Smart Card"工具,在"Secure Enclave"上创建了一个"CryptoTokenKit"身份。然后,通过使用文档同样不足的 `ssh-keychain.dylib`,我们可以使用该身份生成 Apple 的 `ssh` 能够使用的签名。
我们无法保证那些缺失的文档不会写着"不要使用;实践中非常不安全",但话说回来:Apple 那么多安全宣传,他们不会骗我们的,对吧?* spooky *
玩笑归玩笑,将科幻风格的安全模块内置在大多数现代设备中确实很酷;越多的人开始使用它们并推动开放性,厂商的支持就会越好。我们也能拥有好东西!
再次说明,很多内容源自[这篇 gist](https://gist.github.com/arianvp/5f59f1783e3eaf1a2d4cd8e952bb4acf);强烈推荐阅读,也欢迎在讨论区提问。
如果你喜欢这篇文章,可以[告诉我](https://bsky.app/profile/nmattia.bsky.social)或[加入讨论](https://lobste.rs/s/d7m5j1/setting_up_ssh_keys_on_macos_with_touch_id)!你也可以[订阅](https://buttondown.com/nmattia)以获取更新。
---
以下是关于 macOS 的更多文章:
- 在 macOS 上访问 LUKS 和 ext4 驱动器 (https://nmattia.com/posts/2026-01-13-using-luks-on-macos/)
- 用 caffeinate 让你的 Mac 保持唤醒 (https://nmattia.com/posts/2022-07-12-keep-you-mac-awake-with-caffeinate/)
相似文章
在HN上展示:macOS数据保护密钥链用于Electron应用
一个为macOS上的Electron和Node.js应用提供的安全存储工具,利用现代数据保护密钥链实现生物识别认证和iCloud同步等功能。
@dreamsofcode_io: 现在正是考虑将你的 SSH 密钥放在硬件安全密钥(如 Yubikey)上的好时机。
一条推文建议将 SSH 密钥使用硬件安全密钥(如 Yubikey)进行保护,并提及 npm、PyPI 和 Crates.io 上正在活跃的跨生态系统供应链攻击(TrapDoor),该攻击涉及恶意包和窃取加密货币的恶意软件。
xtty
xtty 是一款面向日常 SSH 与编码代理工作的原生 macOS 终端,支持批量连接已保存主机、密码自动填充、sz/rz 文件传输、标签页与分屏、广播输入及编码代理状态显示,提供免费版与一次性 Pro 购买。
@AmberTreelet: https://x.com/AmberTreelet/status/2057453069261279629
这篇教程详细介绍了GitHub的基础概念、使用方法和在AI时代的重要性,帮助新手从零开始掌握GitHub。
无需在编程代理环境中放置密钥,为其提供SSH访问真实服务器的权限 - 信任模型解析
作者提出了一种安全方法,通过一个持有密钥并签名命令的中介客户端,为编程代理提供SSH访问真实服务器的权限,并支持按主机策略、实时监控和审计日志,同时讨论局限性并寻求反馈。