曾经有一段时间,我随大流嫌弃 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>

如上,一共有两个密钥,一个主密钥,一个子密钥。

  1. 比如将 E 功能的子密钥导入到智能卡中,需要先选中这个密钥。

    选中指令为 key 后面跟子密钥的编号,如上为 1 ,则输入 key 1

  2. 在插入 Yubikey 且能够正常识别的情况下,输入 keytocard 并按提示执行导入操作。

  3. 之后,会有一行 note 提示如果此时输入 save 保存,硬盘中的子密钥私钥将被抹去。

  4. 如果要接着导入其他子密钥,需要再输入一次选择指令反选密钥,再选择其他密钥。

  5. 导入全部完成后,输入 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 签名

可阅读 GitHub 的相关介绍5 6

认证

用 A 密钥进行 SSH 身份验证需要先配置好 gpg-agent 才能工作。

不同的系统配置方法也不一样,可参考 Archwiki 的配置方法4,以及我之前写过的一篇博客

我用的是 NixOS 。NixOS 的配置方法比较简单,可以参考我的系统配置7 8

设置 Yubikey

如果想在 Yubikey 等其他硬件密钥上使用 GnuPG ,需要先设置好 GnuPG 的智能卡接口 scdaemon 。

具体参考 Archwiki9 ,和我之前写过的一篇博客

网络公钥目录(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 等。

参考资料

  1. https://developers.yubico.com/PGP/YubiKey\_5.2.3\_Enhancements\_to\_OpenPGP\_3.4.html

  2. https://crypto.stackexchange.com/questions/67457/elliptic-curve-ed25519-vs-ed448-differences

  3. https://wiki.archlinux.org/title/GnuPG#Symmetric

  4. https://www.ruanyifeng.com/blog/2013/07/gpg.html 2

  5. https://docs.github.com/zh/authentication/managing-commit-signature-verification/telling-git-about-your-signing-key#telling-git-about-your-gpg-key

  6. https://docs.github.com/zh/authentication/managing-commit-signature-verification/signing-commits

  7. https://github.com/A1ca7raz/flamework/blob/main/modules/hardware/fido.nix#L5

  8. https://github.com/A1ca7raz/flamework/blob/main/modules/programs/desktop/security/gnupg.nix#L7-L19

  9. https://wiki.archlinux.org/title/GnuPG#Smartcards

  10. https://wiki.archlinux.org/title/OpenPGP#Web\_Key\_Directory

  11. https://wiki.gnupg.org/WKDHosting#Using\_gpg-wks-client\_from\_newer\_GnuPG\_.28GnuPG\_v.3E.3D2.2.12.29

  12. https://gist.github.com/kafene/0a6e259996862d35845784e6e5dbfc79

  13. https://wiki.gnupg.org/WKDHosting#Publishing