曾经有一段时间,我随大流嫌弃 GnuPG 功能臃肿不够现代。再后来,由于自己的密钥管理过于糟糕,我急需一款合适的密钥管理方案,不停寻找能与 GnuPG 相媲美的替代品,未果。这时我才意识到,我对 GnuPG 并不了解。于是,我决定系统地扒一下 GnuPG 的使用方法,并总结到这篇文章中。
创建密钥
一些教程会讲,创建密钥的最佳实践是在没有网络连接的系统上创建密钥,以避免泄漏。
这个见仁见智,对安全有追求的朋友可以研究一下。
当然也可以直接在硬件密钥上创建密钥,缺点是无法导出私钥备份。
我习惯用 gpg --expert --full-gen-key
创建密钥。
选择密钥类型
gpg (GnuPG) 2.4.4; Copyright (C) 2024 g10 Code GmbH
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
请选择您要使用的密钥类型:
(1) RSA 和 RSA
(2) DSA 和 Elgamal
(3) DSA(仅用于签名)
(4) RSA(仅用于签名)
(7) DSA(自定义用途)
(8) RSA(自定义用途)
(9) ECC(签名和加密) *默认*
(10) ECC(仅用于签名)
(11) ECC(自定义用途)
(13) 现有密钥
(14)卡中现有密钥
您的选择是?
一般都是用 RSA 和 ECC 类型的密钥。不建议用 RSA 密钥,如果要用,建议设置密钥长度为 4096 。
选择主密钥功能
ECC 密钥的可实现的功能: 签名(Sign) 认证(Certify) 身份验证(Authenticate)
目前启用的功能: 认证(Certify)
(S) 签名功能开关
(A) 身份验证功能开关
(Q) 已完成
您的选择是?
如果要导入物理密钥(如 Yubikey)使用,建议选择 11 自定义密钥用途并关闭所有功能,且仅保留认证功能,将所有功能分散到子密钥中并导入对应密钥槽,主密钥则可以安全地导出备份离线保存。
选择椭圆曲线
请选择您想要使用的椭圆曲线:
(1) Curve 25519 *默认*
(2) Curve 448
(3) NIST P-256
(4) NIST P-384
(5) NIST P-521
(6) Brainpool P-256
(7) Brainpool P-384
(8) Brainpool P-512
(9) secp256k1
您的选择是?
ECC 密钥需要选择曲线类型,对于需要使用 A 功能的密钥,只能选择 Curve 25519(导出 ssh-ed25519)和 NIST P-XXX(导出 ECDSA 类型的 SSH 公钥)的曲线。
Curve 448 虽然导出 ssh-ed448
开头的 SSH 公钥,但 ssh-keygen 暂不支持生成该类型的密钥,且 GitHub 不支持上传这个类型的公钥,等于不可用(OpenSSH 服务端暂时没有测试)。
其他曲线均不能导出 SSH 公钥。
另外,导入 Yubikey 的密钥,也要注意曲线类型。Yubikey 支持以下类型的椭圆曲线1:
YubiKeys support the following Elliptic Curve algorithms in addition to RSA (Firmware 5.2.3 and above only)
secp256r1
secp256k1
secp384r1
secp521r1
brainpoolP256r1
brainpoolP384r1
brainpoolP512r1
curve25519
x25519 (decipher only)
ed25519 (sign / auth only)
简言之,除了 Curve 448 不能用,其他都能用。
仅导出离线保存的主密钥,可以随意选择曲线,只有子密钥有曲线要求。主密钥的算法类型并不会影响子密钥—— RSA 的主密钥也可以创建 ECC 的子密钥,反之亦然。
至于 Curve 448 和 25519 哪个算法强度更高,在协议上,两者并没有什么实质性的差异,在参数上,Curve 448 更强一些2。
设置密钥有效期
密钥有效期对于 PGP 倒没有 X.509 那么重要,设置为永不过期并不会有太大的问题。
对于大多数人而言,影响密钥安全性最大的因素不是密钥被窃取,而是自己会不会遗忘密码或误删除密钥导致自己无法访问密钥——如果真的发生这种情况,设置一个有效期会比较好,真丢了也会到期失效。
创建用户标识
用户标识的信息非常重要。
真实姓名
可以填写自己常用的 ID ,一般填写自己的 GitHub 用户名。
电子邮件地址
填写自己主要的公开邮箱,一般填写自己在 GitHub 上预留的公开邮箱,如果没有则可以填写 GitHub 提供的保密邮箱(ID编号+用户名 @users.noreply.github.com 格式的邮箱)。如果同时使用多个公开邮箱,也可以后期添加,包括 GitHub 的保密邮箱。
注释
如果没有特殊需要则留空。
用户标识需要谨慎设置,尤其是需要上传至公共密钥服务器的密钥。
如果上传后修改用户标识,之前的用户标识并不会直接抹除,而是会保留痕迹。
如果不希望别人知道自己过去的 ID ,新建密钥会更容易一些,但废弃的旧密钥会永久保留在公共服务器上,即使吊销密钥也不会从服务器上删除,而是保留公开的吊销信息。
所以作为一个有公德心的社会人,请不要给公共服务器制造不必要的垃圾。
目前主流 Git 托管平台都支持 SSH 密钥的签名功能。在没有定好自己的 ID 和邮箱的情况下,可以使用不包含用户信息的 SSH 密钥代替 GPG 的部分功能。
设置密码
最后是给主密钥设置一个密码。请谨慎设置,这里可没有忘记密码的选项。当然也可以留空,但不建议这么做。如果使用硬件密钥,可以使用硬件自身的 PIN 来保护密钥安全,不过要注意保护未加密的主密钥备份。
创建子密钥
输入 gpg --edit-key
加密钥 ID 就可以进入密钥管理控制台。如果使用 fish 等功能丰富的 shell 或扩展,按 tab 键可以自动补全密钥 ID 。进入后输入 help
可查看所有指令。
输入 addkey
创建子密钥。如果需要如创建主密钥时详细的设置选项,需要在 --edit-key
时添加 --expert
参数,即 gpg --expert --edit-key
加密钥 ID 。
方法同创建主密钥。
导出
具体看 Archwiki 。
导入到 Yubikey
导入前需要先备份好所有的私钥。
你的管理控制台大概是这样的:
gpg (GnuPG) 2.4.4; Copyright (C) 2024 g10 Code GmbH
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
私钥可用。
sec ed25519/A6154504E8103C97
创建于:2024-04-13 有效至:2027-04-13 可用于:SC
信任度:绝对 有效性:绝对
ssb cv25519/F5A2CFFF7D4C455C
创建于:2024-04-13 有效至:2027-04-13 可用于:E
[ 绝对 ] (1). a <a@a>
gpg>
如上,一共有两个密钥,一个主密钥,一个子密钥。
比如将 E 功能的子密钥导入到智能卡中,需要先选中这个密钥。
选中指令为
key
后面跟子密钥的编号,如上为 1 ,则输入key 1
在插入 Yubikey 且能够正常识别的情况下,输入
keytocard
并按提示执行导入操作。之后,会有一行 note 提示如果此时输入
save
保存,硬盘中的子密钥私钥将被抹去。如果要接着导入其他子密钥,需要再输入一次选择指令反选密钥,再选择其他密钥。
导入全部完成后,输入
save
保存。
导入到 Yubikey 的行为是不可逆的,所有导入前务必确保做好备份。
导出公钥
gpg --export key-id
添加 -a
/ --armor
选项导出为 ASCII 格式,否则导出二进制格式。
导入公钥 & 私钥备份
gpg --import key-file
上传公钥
如果需要使用 gpg 上传公钥到公共服务器,使用以下指令:
gpg --send-keys key-id
默认使用 hkps://keys.openpgp.org
对于在 OpenPGP 的公共服务器上传公钥有一些限制,OpenPGP 的服务器并不直接接受通过 --send-keys
上传的公钥,需要通过邮件验证才允许在服务器上通过用户标识检索密钥。有两种方法,一种是导出后网页上传,另一种是使用指令形式的快捷方式
gpg --export key-id | curl -T - https://keys.openpgp.org
根据提示验证一个邮箱,之后就算上传完成了。
要在密钥服务器上查找密钥的详细信息而不导入它:
gpg --search-keys user-id
要从密钥服务器导入密钥:
gpg --receive-keys key-id
要使用密钥服务器的最新版本刷新/更新钥匙串:
gpg --refresh-keys
切换服务器
dirmngr 是 GnuPG 用来访问 PGP 密钥服务器的内部服务,通过修改它的配置文件来更改默认密钥服务器,一般在 ~/.gnupg/dirmngr.conf
keyserver hkp://keyserver.ubuntu.com
当默认服务器无法正常工作时,临时使用另一台服务器也很方便。可以添加 --keyserver
参数来指定服务器。
gpg --keyserver hkps://keys.openpgp.org/ --search-keys user-id
公共密钥服务器
除了默认的 OpenPGP ,还有其他规模较大的公共密钥服务器。
Ubuntu Keyserver http://keyserver.ubuntu.com
Keybase https://keybase.io
这本是一家端到端加密的即时聊天软件,不过也有一些朋友用它发布自己的公钥。同理,其他 Git 托管平台也可以用来发布自己的公钥。
但其本身只提供公钥的下载,并不是正式的 Keyserver 。如果需要与他人公开交换密钥签名,还是需要使用上面提到的 Keyserver 。
具体过程可以参考 Debian 的资料。
使用密钥
加密 & 解密
这里仅讲基于 GPG 密钥的非对称加解密,基于文本密码的对称加解密看 Archwiki3 。
主要参数为 -e
/ --encrypt
加密和 -d
/ --decrypt
解密。
指令格式为 gpg [选项] --操作参数 [filename]
,即所有选项必须放在 -e
/ -d
的前面,-e
/ -d
后面只能跟文件名。
一条指令一次只能处理一个文件,可以将多个文件打包成一个压缩包再处理。
添加 -o
/ --output
选项指定明文输出路径,否则会打印在标准输出。
加密
加密文件需要提前导入接收方的公钥。
需要指定接收方的公钥。
使用 -r
/ --recipient
显式指定接收方;
使用 -R
/ --hidden-recipient
隐式指定接收方,将不会把接收方的密钥 ID 放入密文中。
添加 --no-emit-version
以避免打印版本号,或将相应的设置添加到配置文件中。
添加 -a
/ --armor
选项导出为 ASCII 格式。
解密
解密文件需要提前导入接收方的私钥。不需要手动指定私钥。
签名 & 验证
一共有三种签名,区别主要是输出内容的形式。
-s
/--sign
输出内容为原始文件的压缩内容和签名的混合文本。添加-a
/--armor
选项导出为 ASCII 格式,内容用PGP MESSAGE
标签包裹。--clearsign
输出一个文件,内容为原文和签名文本的拼接。原文用PGP SIGNED MESSAGE
标签包裹,签名用PGP SIGNATURE
标签包裹。签名默认为 ASCII 格式。--detach-sig
输出与源文件分离的独立签名文件。添加-a
/--armor
选项导出为 ASCII 格式,内容用PGP SIGNATURE
标签包裹。
所有指令的格式均为 gpg [选项] --签名参数 [filename]
。
验证签名使用 --verify
指令,格式为
gpg --verify sig-file
如果签名文件不包含原文内容,指令末尾还需要追加源文件的路径。
签名 + 加密
上面的指令,都是一次只进行一个操作。如果想同时签名和加密,可以使用下面的命令4。
gpg --local-user sender-id --recipient recipient-id --armor --sign --encrypt src
下面是部分参数的简写和含义:
-u
/--local-user
指定用发信者的私钥签名-r
/--recipient
-a
/--armor
-s
/--sign
-e
/--encrypt
简化后为
gpg -u sender-id -r recipient-id -ase src
对于解密和验证签名操作,GnuPG 可以省略相关操作参数,软件会根据文件内容自动猜测用户的意图。
gpg file.asc
Git 签名
认证
用 A 密钥进行 SSH 身份验证需要先配置好 gpg-agent 才能工作。
不同的系统配置方法也不一样,可参考 Archwiki 的配置方法4,以及我之前写过的一篇博客。
我用的是 NixOS 。NixOS 的配置方法比较简单,可以参考我的系统配置7 8。
设置 Yubikey
如果想在 Yubikey 等其他硬件密钥上使用 GnuPG ,需要先设置好 GnuPG 的智能卡接口 scdaemon 。
网络公钥目录(WKD)
网络公钥服务(Web Key Service,WKS)协议是公钥分发的新标准。电子邮件域将提供其自己的公钥服务器,称为网络公钥目录(Web Key Directory,WKD)。在高于 2.1.16 版本的 GPG 中,在依电子邮件地址(如[email protected]
)加密时,如果该公钥不在本地密钥环中,GPG 将以 HTTPS 向电子邮件的域(example.com
)查询 OpenPGP 公钥10。
这个并不是啥刚需,不是所有人都需要用 GPG 加密邮件,简中互联网甚至鲜有关于 WKD 应用和搭建的资料。
这个功能需要邮件提供商支持(主流邮件服务商中只有 Protonmail 等支持 PGP 加密的小众服务商提供此功能),或者自己拥有邮箱域名的所有权。
如果在 OpenPGP Keyserver 上传了自己的公钥,可以直接使用他们提供的 WKD as a Service 服务。
将邮箱域名的 openpgpkey
二级域 CNAME 指向 wkd.keys.openpgp.org
即可。
搭建自己的 WKD
搭建一个自己的静态 WKD 也不难,本质上只是一堆文件。
首先创建好一个 WKD 需要的目录结构。WKD 通过 WELL-KNOWN 提供服务。在站点根目录创建 WELL-KNOWN 目录:
mkdir -p .well-known/openpgpkey
只需要一行指令,就可以生成提供静态 WKD 所需的文件11:
gpg --list-options show-only-fpr-mbox -k @example.org | gpg-wks-client -v --install-key -C .well-known/openpgpkey
将其中的 example.org
替换为自己的邮箱域。
这条指令会在 .well-known/openpgpkey
下创建一个以你的邮箱域为名称的目录,目录里有一个 policy 文件和一个存放公钥文件的 hu 目录,文件名为某种方式哈希过的邮箱用户名12。
如果自己的密钥里还包含其他域名的邮箱地址,也会创建一个对应的目录。这些目录是多余的目录,可以删除。
部署自己的 WKD
WKD 需要发布到邮箱域的 openpgpkey
子域或邮箱域的顶级域下,两种对目录结构的要求并不同。
如果是 openpgpkey
域,直接发布上一步构建的站点根目录即可。
如果想挂在自己的顶级域下,则需要去除包含自己域名的那一层目录,即 openpgpkey.example.org/.well-known/openpgpkey/example.org/hu
变为 example.org/.well-known/openpgpkey/hu
13 。
由于只是一堆静态文件,所以它可以在所有的静态网站服务上部署,比如 Cloudflare Pages 、Github Pages 等。
参考资料
https://developers.yubico.com/PGP/YubiKey\_5.2.3\_Enhancements\_to\_OpenPGP\_3.4.html ↩
https://crypto.stackexchange.com/questions/67457/elliptic-curve-ed25519-vs-ed448-differences ↩
https://docs.github.com/zh/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key#telling-git-about-your-gpg-key ↩
https://docs.github.com/zh/authentication/managing-commit-signature-verification/signing-commits ↩
https://github.com/A1ca7raz/flamework/blob/main/modules/hardware/fido.nix#L5 ↩
https://github.com/A1ca7raz/flamework/blob/main/modules/programs/desktop/security/gnupg.nix#L7-L19 ↩
https://wiki.archlinux.org/title/OpenPGP#Web\_Key\_Directory ↩
https://wiki.gnupg.org/WKDHosting#Using\_gpg-wks-client\_from\_newer\_GnuPG\_.28GnuPG\_v.3E.3D2.2.12.29 ↩
https://gist.github.com/kafene/0a6e259996862d35845784e6e5dbfc79 ↩