我一直把 YubiKey 当作两步验证密钥,同时用它来给 Git 提交签名。直到几天前,我用的一直是 YubiKey 4。由于我现在的电脑没有任何 USB-A 接口,我的背包里总得带着 USB-A 转 USB-C 的转接头,过去一年里使用这把密钥一直很麻烦。所以我决定是时候换成我那把“新”的 YubiKey 5C 了,这样就不用再折腾那些转接头了。

其实这把密钥我早就有了,当时是因为 Infineon TPM 那个被他们发现的问题,我订了一把 YubiKey 4 作为替换,同时一起买了它,但直到现在我才决定换用新密钥。

现在换的另一个原因是我的旧 GPG 密钥过期了,我不得不重新签发,所以正好可以用这把密钥来给我的 Git 提交签名。

GPG 的疯狂

如果你以前(或者现在)处理过 GPG 密钥,你就知道,对于从没接触过的人来说,想把它正确又安全地跑起来相当困难。主密钥要离线保存,公钥要来回倒腾,密钥过期后要重新签发,可能还要用旧密钥给新密钥签名以便验证,还要吊销密钥……哪怕只是在理论上遵循最佳实践都不容易,真要在实践中做到,事情就变得极其复杂了。

网上大多数教程都是让你用命令行生成密钥,在设计模式很不常见的菜单里跳来跳去,想把它弄好简直是一种折磨。大多数教程还说要用离线机器(比如树莓派)和/或在离线状态下生成主密钥和子密钥,并且不要把主密钥放在你的电脑上。

🤯

过去将近 4 年里,每年一月初我都得面对这件事。每次看到下面这个错误,提示我的签名密钥已经过期,我都心头一紧。

error: gpg failed to sign the data
fatal: failed to write commit object

今年我决定让这个过程变得简单得多,直接使用内置的证书生成功能,避开生成 GPG 密钥之类的所有麻烦。

在 YubiKey 上直接生成密钥

这种方式仍然需要使用命令行界面,但步骤更少,所以更直观一些。

首先我们要设置好卡片/设备。

卡片/设备设置

在终端中运行 gpg --card-status,你应该会看到类似下面的内容:

Reader ………..: Yubico Yubikey 4 OTP U2F CCID
Application ID …: <Serial number>
Version ……….: 2.1
Manufacturer …..: Yubico
Serial number ….: <Device serial number>
Name of cardholder: unspecified
Language prefs …: en
Sex …………..: unspecified
URL of public key : \[not set\]
Login data …….: <e-mail or nothing>
Signature PIN ….: not forced
Key attributes …: rsa4096 rsa4096 rsa4096
Max. PIN lengths .: 127 127 127
PIN retry counter : 3 0 3
Signature counter : 9

如果你看到类似的内容,并且没有出现错误,就可以继续了。

运行 gpg --card-edit

你会进入一个命令行界面,开头显示的是同样的信息,底部是命令提示符 gpg/card>。

输入 admin 并按回车。

现在你可以输入 help,查看如何修改卡片的持有者信息和 PIN。

有一件事你必须做,就是修改卡片的默认 PIN。一共有两个:第一个是用来访问卡上密钥的 PIN,第二个是“管理员”PIN,也叫 PUK(Personal Unblocking Code,个人解锁码)。

要修改它们,输入 passwd,你会看到一个菜单:

gpg/card> passwd  
gpg: OpenPGP card no. <Serial number> detected

1 - change PIN  
2 - unblock PIN  
3 - change Admin PIN  
4 - set the Reset Code  
Q - quit

输入 1,按提示更新你的 PIN。默认 PIN 是 123456。

完成后,选择 3 来修改管理员 PIN。默认值是 12345678。

两个 PIN 都改完之后,回到主菜单,继续完善密钥信息。

你可以使用的命令有:

  • name – 修改持卡人姓名
  • login – 修改持卡人的电子邮件地址或登录名

之后我们就可以进入真正的密钥生成环节了。

生成密钥

现在是时候真正生成密钥了。

输入 generate。

你会被问到 Make off-card backup of encryption key? (Y/n),不过正如 YubiKey 文档中提到的,这只是一个形式上的备份,并不会创建私钥的完整备份,所以回答 Yes 还是 No 都无所谓。我回答的是 No(n)。

如果卡上已经存有密钥,系统会问你是否要替换它们:

gpg: Note: keys are already stored on the card!

Replace existing keys? (y/N)

请确保你不会替换掉某些没有备份的重要东西,然后输入 Y 继续。如果里面有重要的东西,最好用 Ctrl+C 退出整个流程。

系统会要求你输入之前设置的卡片 PIN。

接着你需要选择密钥的有效期:

Please specify how long the key should be valid.
        0 = key does not expire
      <n> = key expires in n days
     <n>w = key expires in n weeks
     <n>m = key expires in n months
     <n>y = key expires in n years
 Key is valid for? (0)

如果你希望密钥只有 1 年有效期,输入 1y。

系统会要求你确认到期日期。检查一切无误后确认。

然后系统会要求你输入密钥所有者的信息:

Is this correct? (y/N) y
GnuPG needs to construct a user ID to identify your key.
Real name: <Your name>
Email address: <Your e-mail>
Comment: <Comment is optional>
 You selected this USER-ID:
     "<Your name> <comment> <your e-mail>
 Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit?

Type `O` to confirm if everything seems correct.

接下来会要求你输入管理员 PIN 或 PUK。这是必需的,因为它要对卡片执行写操作。

然后如果一切正常,YubiKey 会开始闪烁,并且会持续闪一阵子(我这边大约要五分钟)。

完成后你会看到下面的消息

gpg: key <Short ID> marked as ultimately trusted
gpg: revocation certificate stored as '/Users/bisko/.gnupg/openpgp-revocs.d/<LONG ID>.rev' public and secret key created and signed.

记下 <Short ID>,我们后面会用到。

此时你可以用 quit 或 Ctrl+C 退出 GPG 提示符。

现在该保存公钥了,这样我们就能在另一台电脑上复用这把密钥。

为此,运行下面的命令:

gpg --armor --export <Short ID> > <Short ID>.asc

这会把公钥导出到名为 <Short ID>.asc 的文件中。以后如果你想在另一台电脑上使用这把 YubiKey,或者丢失了 GPG 配置,就会用到它。

要把公钥恢复到你的密钥环中,需要这样做:

gpg --import < <Short ID>.asc
gpg: key <Short ID>: public key "Your Name <your@email>" imported
gpg: Total number processed: 1
gpg: imported: 1

这里有个非常重要的提示。如果这是新安装的 GPG 或者是另一台电脑,GPG 不会自己识别卡上的密钥。你需要运行 gpg --card-status,让它读取卡片并获取卡上的密钥。

设置 Git 签名

要让 Git 给你的提交签名,你需要在 ~/.gitconfig 中添加以下内容。

在 [user] 部分下,添加(或更新)下面这一行:

signingkey = 0x<Short ID>

然后如果你想给所有提交都签名,在文件末尾添加以下内容:

[gpg]
  program = /usr/local/bin/gpg
[commit]
  gpgsign = true

这会让 Git 给你所有的提交签名。

要设置 GitHub,你需要把公钥添加到你的个人资料中。具体做法是进入 GitHub -> Settings -> SSH and GPG keys -> New GPG key。

然后你会看到一个文本框,把我们上面生成的 <Short ID>.asc 文件的全部内容粘贴进去。

如果你想复制文件内容(在 macOS 上),可以这样:

cat <Short ID>.asc | pbcopy

这会把整个文件的内容复制到剪贴板。

把它粘贴到 GitHub 的文本框里,然后点击 Add GPG key 按钮。

之后你可以试着提交并推送到 GitHub,应该就能看到漂亮的 Verified 标签了。

调试

有时候事情不会一帆风顺。为了收集更多信息,你可以试试下面这些方法:

用 GPG 签名

首先试着用这把密钥给某个东西签名:

gpg -s <file>

如果失败了,试着弄清楚失败的原因,这方面我不是专家,帮不上忙。

查看 Git 的输出GIT_TRACE=1 git commit -am “test”

这会告诉你 Git 在尝试提交并签名时所做的每一步具体操作,能给你一些线索,看看签名为什么可能失败。

从中你还可以看到,Git 执行了下面这条命令:

/usr/local/bin/gpg --status-fd=2 -bsau <Key ID>

你可以试着手动运行它,看看能不能得到更有用的错误信息。