背景介绍

YubiKey 是一个 著名 很贵的硬件密钥,本次到手的是 YubiKey 5c NFC,不着急的朋友建议等等国人的一个开源项目 CanoKeys,实现了和 YubiKey 5 几乎一致的功能,但便宜很多。不过从用户反应来看,Yubico 似乎性能更强一些,签名时速度较快。

YubiKey 支持的协议很多,这次我们仅关注 GPG 方面。官方的 GUI 程序并不提供 GPG 的可视化配置(也许用到 GPG 的人都更喜欢命令行吧)。

什么是 GPG

首先得搞清楚一个和它很像的东西:PGP (Pretty Good Privacy),这是一个非对称加密协议,可惜它本身是商业软件。于是万能的开源社区搞出了一个遵循此标准的免费实现:GPG (Gnu PG),也就是我们现在可以随意使用的东西啦。在本文中,PGP 与 GPG 是等价的。

通常 "GPG"/"GnuPG" 仅用于指示一个软件。其相关概念与产物最好用 "PGP" 描述。例如:

  • PGP 密钥 ✅; GPG 密钥 ❌
  • PGP 签名 ✅; GPG 签名 ❌

能力(用途)

PGP 有许多能力,见下表:

缩写能力备注
[C]Certificating认证其他密钥,比如签署一个子密钥。
[S]Signing签名。证明某数据没有被篡改。例如文件签名,git commit 签名。
[A]Authenticating认证。比如 SSH 登录。
[E]Encrypting加密。

子密钥

PGP 设计之初就包含子密钥的概念。其实实质上,子密钥和主密钥没有什么区别,只不过 PGP 默认做了包装:

  1. 子密钥由主密钥签名,继承了主密钥的身份。
  2. 子密钥的公钥包含在主密钥里,无需单独分发。
  3. 子密钥与主密钥共享吊销证书,无需单独维护。

有没有感觉和 HTTPS 证书链很像?没错,就是这样!只是 PGP 是去中心化的,没有 CA,并且由于上述包装,客户端可以一次性验证子密钥,而不需要一层层请求。

没有 CA?

是的,没有 CA。也许这就注定了 PGP 小众的结局。

没有 CA 意味着我们必须手动分发公钥,例如 Github 要求我们亲自上传公钥。也有人会把公钥放在自己的博客来 提高B格 供有需要的朋友验证签名或发送加密消息。

现在你知道了,分发公钥很麻烦。因此出现了 Key Server,但它不是 CA! 可以把 Key Server 想象成网盘,大家都把公钥传上去,然后只需相对简短的 id 或者 用户 id 就可以轻松下载导入了。但是 Key Server 不会对用户做认证,也就是说任何人都可以声称自己是任何人。 因此,不要轻易信任服务器上的公钥。

除此之外,Key Server 被设计成无法删除数据——你可以添加信息,可以吊销证书,但是不能删除它。这样做的目的是尽可能避免行政手段的干预。

发布到 CA 很简单,执行 gpg --keyserver keys.openpgp.org --send-keys <id>。Key Server 有几个著名的,他们之间会定时同步。根据需要上传就行。

⚠️ 注意,上传到 Key Server 的公钥是完全公开的,包括内置的名字、电子邮箱等,注意隐私问题。一旦上传则无法删除。

创建 GPG

创建主密钥

知道了什么是 GPG,现在可以着手创建一个密钥了。默认已经装好了 gpg 工具。

创建密钥非常简单,只需找一台安全的电脑,而不是充满了木马与流氓软件(比如 Qx)的设备,然后执行:

gpg --gen-key

跟着引导一步步来就行。截止目前(2022.1)默认已经使用 ECC 而不是 RSA 了。相同强度下 ECC 密钥更小,效率更高。如果有兼容老设备等需求,可以执行

gpg --full-generate-key

来自定义算法。

创建子密钥

从安全角度,主密钥应当仅用于签署子密钥,创建后应该离线妥善保存。而日常工作则使用子密钥。

但是注意,Maven central 要求必须用主密钥签署才能发布

执行 gpg --list-keys(缩写 gpg -k)来列出刚刚创建的密钥。截止目前,gpg 工具会默认生成一个用于加密 ([E]) 的子钥(why)。

> gpg --list-keys
/Users/.../.gnupg/pubring.kbx
--------------------------------
pub   ed25519 2022-01-10 [SC] [有效至:2024-01-10]
      092CF9F4407E1C8D6F2734997D9FDEBFD006FAE0  # <----- 这个就是 id
uid             [ 绝对 ] Chenhe <liangchenhe55@gmail.com>
sub   cv25519 2022-01-10 [E] [有效至:2024-01-10]

要创建新的子钥,得执行 gpg --expert --edit-key <id> 来编辑主密钥,在编辑模式下执行 addkey 即可根据引导添加子密钥。需要注意的是,用于认证 ([A]) 的密钥不是很常用,因此默认没有这个选项,需要选择「自定义用途」来手动指定。

添加完成后在编辑模式,执行 q 退出,会询问是否保存,选择 Y。注意如果这时候按 ctrl+c 强退了就不会保存刚才的修改。

PS: 一些缩写

缩写全称备注
secSecret Key主私钥
ssbSecret Subkey子钥的私钥
pubPublic Key主公钥
subPublic Subkey子钥的公钥

备份

在把密钥添加到 YubiKey 之前,先导出备份。因为这是「移动」而不是「复制」,同时 YubiKey 不允许导出内部密钥。执行

gpg --armor --export-secret-keys --export-options backup -o /path/to/save/backup.gpg <id>

新的 gpg 工具,只需导出主密钥的私钥,即可自动包含主+子密钥的公钥+私钥。

  • --armor 参数可以导出文本格式。缩写为 -a
  • --export-options backup 可导出用于备份目的的密钥,其包含了恢复所需的全部数据,不仅有 OpenPGP 定义的内容,还包括 GunPG 附加数据。

恢复

gpg --import --import-options restore /path/to/backup.gpg
  • --import-options restore 导入一些常规导入中跳过的数据,包括 GunPG 专有数据。

吊销

以防万一我们私钥泄漏,还有必要创建并备份一个吊销证书。吊销证书一旦生成,可以在没有私钥、无需密码的情况下 声明一个私钥作废。因此吊销证书也应该安全保管,虽然泄漏不会导致数据被破解,但会带来很多麻烦。执行:

gpg --output /path/to/save/revoke.asc --gen-revoke <id>

可以创建并导出一个吊销证书。


等那一天真的来临(无论是私钥泄漏,还是整个密钥不打算继续使用了),只需要简单地导入这个特殊的密钥,就可以作废之:

# 声明作废,不要轻易执行
gpg --import /path/to/save/revoke.asc

现在,你需要把吊销后的密钥(不是吊销密钥本身)重新分发,例如上传到 Key Server,让其他人知道这个公钥已经不可信了。

添加到 YubiKey

现在插入小钥匙🔑,执行 gpg --card-status 应该可以读取到 YubiKey 的 GPG 状态。读取不到的话请检查连接。

> gpg --card-status
Reader ...........: Yubico YubiKey OTP FIDO CCID
Application ID ...: X000000000000000000000000000000
Application type .: OpenPGP
Version ..........: 0.0
Manufacturer .....: Yubico
# ......

添加密钥

再次编辑主密钥 gpg --expert --edit-key <id>,默认选中的是主密钥 (0),可以执行 key <x> 来选择子密钥,x 为顺序号 (0, 1, 2...)。某密钥被选中后,前面会出现 * 的标志,但是主密钥除外。

gpg> key 1 # 选择第一个子密钥

sec  ed25519/7D9FDEBFD006FAE0
     创建于:2022-01-10  有效至:2024-01-10  可用于:SC
     信任度:绝对        有效性:绝对
ssb* cv25519/ED5F951C04BF0D1C
     创建于:2022-01-10  有效至:2024-01-10  可用于:E

选中后执行 keytocard 就可以导入 YubiKey 啦,根据密钥的用途选择对应的存储槽就好。

最后依然是执行 q 保存退出。

⚠️ 再次警告,导入后本地私钥会被删除,务必先导出到文件备份。

到此为止已经导入完成了。为了安全,可以执行 gpg --delete-secret-keys <id> 删除电脑上的私钥,然后把备份文件加密离线脱机保存。现在,除非插上 YubiKey 否则无法进行签名/认证/加解密。

配置 YK

首次使用应该先修改一下 YubiKey 的默认 PIN,以免被其他人盗取后可以签名。

执行 gpg --edit-card 进入智能卡编辑模式,然后执行 admin 进入管理员模式。执行 help 可以列出允许的操作,有许多字段可以编辑。我们这边执行 passwd 根据提示修改 PIN(调用私钥时验证)和 Admin PIN(修改智能卡属性时使用)。前者默认密码是 123456,后者是 12345678

⚠️ 注意,GPG 数据(包括 PIN)和 YubiKey PIV 数据相互独立,没有关系。

通过 YubiKey 的工具 ykman 还可以进行更多设置,例如认证时要求触摸铜片,以免后台被恶意程序偷偷签名。

 ykman openpgp keys set-touch [SIG|ENC|AUT|ATT] [on|off|fixed|cached|cached-fixed]

更多详细说明看看官方文档吧。


拥有公钥+私钥,才可以正常进行签名等操作。可惜 YubiKey 本身不保存公钥,这意味如果使用一台新设备,则部分功能可能受限。如果公钥上没有很重要的隐私信息,一个方便的方案是将其传到 Key Server,或者 Gayhub Github。然后将公钥下载地址配置到 YubiKey 里。例如已经把公钥配置到了 Github,那么地址就是 https://github.com/<username>.gpg,执行下面命令将其配置到 YK:

> gpg --edit-card
gpg/card> admin
gpg/card> url https://github.com/<username>.gpg
gpg/card> q

那么在新设备上,就可以执行下面的命令获取并导入公钥:

> gpg --edit-card
gpg/card> fetch

在新设备使用

导入公钥

首先需要导入公钥,如果恰好手头有文件的话就好办了:

gpg --import /path/to/public.key

不过更可能的情况是手头没有... 那么希望机智的你已经把它传到网上了,如此一来保存一份到本地再导入即可:

wget -O public.key https://chenhe.me/pgp && \
gpg --import public.key && \
rm public.key

URL 记不住?希望你按照上面说的把它写入 Yubikey 了,然后让它自己获取:

echo "fetch" | gpg --edit-card --command-fd 0 --status-fd=1

⬆️ 上面是可用于自动化的命令,人工操作不用那么麻烦,具体之前已经演示过了。

绑定 Yubikey

有了公钥,插上 Yubikey 尝试签名一个文件...啊哦!得到一个错误:

gpg -u FF81007D4327173C -s a.txt

No secret key

事实上,gpg 会储存公钥与 yubikey 的绑定关系。之前执行 keytocard 的时候就自动绑定了。显然,新电脑上缺少这个信息。执行下面的命令:

gpg-connect-agent "scd serialno" "learn --force" /bye

可以自动扫描 Yubikey 并绑定。现在可以愉快地使用啦 😄

绑定是一对一的,每次执行 keytocard 都会把对应的密钥 id 绑定到当前的 Yubikey。

这意味着,如果有 N 个 YK,只有最后一个添加密钥的可以正常使用。其他的哪怕里面的密钥相同,gpg 也不识别。因此同样需要上面的命令来刷新。

给 Git 签名

众所周知,git 的用户名和邮箱是自定义的,没有任何验证。PGP 签名则可以证明『你是你』。有效的签名可以在 Github 等支持的服务商显示认证标记来装逼增强安全性。

确认邮箱

PGP 密钥里的邮箱必须和 git 的邮箱一致,才可以被一些软件或服务认为是有效签名。(若不使用外部软件或服务则没有这个限制)

那不一致并且我不想修改 Git 邮箱怎么办?PGP 密钥是可以绑定多个身份(uid)的。每个身份都是名字和邮箱的组合。只要其中一个身份的邮箱匹配就算认证成功。那么我们可以增加一个 uid:

gpg --edit-key "keyid"

gpg> adduid # 添加一个 uid

# 添加的 uid 会自动被密钥签名以生效,并设置为主 uid
# 可以再主 uid 改回来:
gpg> uid 1 # 1 是要设为主 uid 的编号
gpg> primary
gpg> save

这样一个 PGP 密钥就能关联多个邮箱了。

配置 Git

首先列出所有密钥:

❯ gpg -k --keyid-format long
/Users/chenhe/.gnupg/pubring.kbx
--------------------------------
pub   ed25519/73A6C92C3C69D9AD 2022-04-26 [SC] [有效至:2024-04-25]
      3FBE54B8FE8523D21F40ECC573A6C92C3C69D9AD
uid                   [ 绝对 ] Chenhe <git@chenhe.me>
uid                   [ 绝对 ] Chenhe <pgp@chenhe.me>
sub   cv25519/6361868D8348C132 2022-04-26 [E] [有效至:2024-04-25]
sub   ed25519/FF81007D4327173C 2022-04-27 [S] [有效至:2024-04-26]
sub   ed25519/D5DC28EEEFCFBD49 2022-04-27 [A] [有效至:2024-04-26]

选择一个有 [S] 标记的密钥或子钥(推荐),记下 id,比如我选择 FF81007D4327173C。执行:

git config --global user.signingkey "keyid"
git config --global commit.gpgsign true

这样提交的时候就可以自动签名啦。

配置 Github

接下来要把公钥给到 Github,执行 gpg -a --export "keyid" 可输出公钥。复制到 Github Settings - SSH and GPG keys - New GPG key。如果一切正常,结果如下:

但如果 PGP key 的邮箱地址没有添加到 Github 的话会显示 "Unverified":

此时需要去 Github Settings - Emails - Add email address 添加并验证 PGP key 里的邮箱。

必须 GPG key / Git commit / Github 三者邮箱一致,Github 才会显示认证标记。

给历史 Commit 签名

⚠️ 警告

重新签名相当于重新 Commit,Commit ID 会变更,推送到远程需要 --force 参数。不推荐在多人协作仓库中给历史提交签名。

此外,签名会更改 Commit 日期。尽管原始日期不会丢失,但 Github 只会显示签名时的日期!

如果坚持这么做,执行:

git rebase --exec 'git commit --amend --no-edit -n -S' -i <commit id>

其中 <commit id> 是要签名的 Commit 的前一个的 ID,它和 HEAD 之间的 Commit 将被签名。

参考

Last modification:January 14, 2024