一个简单的 GPG 指北

背景: 因为 PGP 是商业软件, 所以 GNU 决定自己写一个...

GnuPG(GNU Privacy Guard), 一个实现了 OpenPGP 标准的自由软件, 也可能是最有名的 OpenPGP 软件, 大部分 Linux 都会自带 GPG.
ArchLinux 的包管理器 pacman 就依赖 GPG 来验证软件包.

GPG 密钥环

首先, 密钥环是什么?
在传统的非对称加密中(如 ssh 用的比较多的 RSA), 一个公钥只对应一个私钥, 这样的结构一般被称为"密钥对", 但是在 GPG 中可以有很多对密钥对, 所以被称为密钥环.

先来看看我的 GPG 钥匙环

gpg --list-keys

输出:

[keyboxd]
---------
pub   ed25519 2024-08-29 [C]
      9122F8F082E447EC5E80945A1AD15F34F92E491B
uid             [ 绝对 ] GSGFs <gsgfs@gsgfs.moe>
sub   ed25519 2024-08-29 [S] [有效至:2027-08-29]
sub   cv25519 2024-08-29 [E] [有效至:2027-08-29]
sub   ed25519 2024-08-29 [A] [有效至:2027-08-29]

...

先来解释一下他的输出内容吧, 其中, 最上面的 pub 是我的主公钥, 使用的算法是 ed25519 椭圆曲线加密算法, 创建日期是 2024-08-29 密钥用途是 C, 也就是用于认证(Certificating), 密钥的指纹是 9122F8F082E447EC5E80945A1AD15F34F92E491B. 下面的 sub 则是我的子公钥, 可以看到我分别创建了三个不同功能的密钥, 具体代表的用途如下.

缩写 全称 用途
C Certificating 认证, 认证子密钥和证书等
S Signing 签名, 对某个东西进行数字签名
E Encrypting 加密, 加密某个东西
A Authenticating 验证, 使用密钥进行身份验证

其中的 UID 是 User ID 的缩写, 用来标识用户身份(不然光看那密钥指纹谁知道谁是谁啊).

创建一个 GPG 密钥

gpg 提供了很多方法创建密钥, 我就以 gpg --full-gen-key --expert 为例.

在终端里输入:

gpg --full-gen-key --expert

输出:

gpg (GnuPG) 2.4.7; 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)卡中现有密钥
您的选择是? 11

我这里选 11, 自定义密钥用途

ECC 密钥的可实现的功能: 签名(Sign) 认证(Certify) 身份验证(Authenticate)
目前启用的功能: 认证(Certify)

   (S) 签名功能开关
   (A) 身份验证功能开关
   (Q) 已完成

您的选择是? q

对于这个密钥, 我只需要认证功能就可以了, 其他的都关掉, 这样会安全点

请选择您想要使用的椭圆曲线:
   (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
您的选择是? 1

直接使用默认的就可以, 这是 128 位的椭圆曲线加密算法, 性能很强(速度快).
如果想要更安全点的话, 可以使用 Curve 448, 这是 224 位的. (不是"整数", 差评!)
最安全的是 NIST P-521Brainpool 256 位.

请设定这个密钥的有效期限。
         0 = 密钥永不过期
      <n>  = 密钥在 n 天后过期
      <n>w = 密钥在 n 周后过期
      <n>m = 密钥在 n 月后过期
      <n>y = 密钥在 n 年后过期
密钥的有效期限是?(0) 0

密钥的过期时间, 可以选不过期.

这些内容正确吗? (y/N) y
GnuPG 需要构建用户标识以辨认您的密钥。

真实姓名: Real Name
电子邮件地址: somestring@gsgfs.moe
注释:

这里输入的信息会显示在 UID 里, 可以方便被人认出你.
注释的话, 没用...

更改姓名(N)、注释(C)、电子邮件地址(E)或确定(O)/退出(Q)? o
我们需要生成大量的随机字节。在质数生成期间做些其他操作(敲打键盘
、移动鼠标、读写硬盘之类的)将会是一个不错的主意;这会让随机数
发生器有更好的机会获得足够的熵。

这时会要求你输入解锁密钥时使用的密码, 如果有 GUI 环境的话会直接弹窗.

然后密钥就创建好了, 类似于下面这样

pub   ed25519 2025-05-27 [C]
      9623534D96CE0EE388F822B346A79F4A9C465F7C
uid                      Real Name <somestring@gsgfs.moe>

这时候有同学就要问了, "诶, 你这只有一个认证密钥有 P 用啊!"
确实, 单一个认证密钥是没什么用, 别着急, 现在就加上其他的密钥.

编辑刚刚生成的密钥:

gpg --edit-key --expert Real

这里我是用名字的方法指明密钥的, 除此之外使用密钥指纹和邮箱都是可以的, gpg 还是相当灵活的.

输出类似于下面这样.

gpg (GnuPG) 2.4.7; 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/46A79F4A9C465F7C
     创建于:2025-05-27  有效至:永不       可用于:C
     信任度:绝对        有效性:绝对
[ 绝对 ] (1). Real Name <somestring@gsgfs.moe>

gpg>

最后可以看到 SHELL 的提示符变成了 gpg>, 这是 gpg 的交互环境, 可以交互式执行操作, 如果想看看有那些指令可以用的话, 还是老样子, 按两下万能的 Tab 键:

gpg>
addadsk     bkuptocard  disable     key         minimize    quit        showphoto
addcardkey  check       enable      keyserver   notation    revkey      showpref
addkey      clean       expire      keytocard   nrsign      revsig      sign
addphoto    delkey      fpr         keytotpm    passwd      revuid      trust
addrevoker  delsig      grip        list        pref        save        tsign
adduid      deluid      help        lsign       primary     setpref     uid

我们使用 addkey 的方法添加子钥

gpg> addkey
请选择您要使用的密钥类型:
   (3) DSA(仅用于签名)
   (4) RSA(仅用于签名)
   (5) ElGamal(仅用于加密)
   (6) RSA(仅用于加密)
   (7) DSA(自定义用途)
   (8) RSA(自定义用途)
  (10) ECC(仅用于签名)
  (11) ECC(自定义用途)
  (12) ECC(仅用于加密)
  (13) 现有密钥
 (14)卡中现有密钥
您的选择是?

接下来要补全剩下的三个不同功能的密钥, 至于这的的算法选择的话其实随便选都可以, 我这里就用性能和安全性都不错的 ECC 椭圆曲线算法.

您的选择是? 10
请选择您想要使用的椭圆曲线:
   (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
您的选择是? 1
请设定这个密钥的有效期限。
         0 = 密钥永不过期
      <n>  = 密钥在 n 天后过期
      <n>w = 密钥在 n 周后过期
      <n>m = 密钥在 n 月后过期
      <n>y = 密钥在 n 年后过期
密钥的有效期限是?(0) 3y
密钥于 2028年05月27日 星期六 21时13分11秒 CST 过期
这些内容正确吗? (y/N) y
真的要创建吗?(y/N) y
我们需要生成大量的随机字节。在质数生成期间做些其他操作(敲打键盘
、移动鼠标、读写硬盘之类的)将会是一个不错的主意;这会让随机数
发生器有更好的机会获得足够的熵。

sec  ed25519/46A79F4A9C465F7C
     创建于:2025-05-27  有效至:永不       可用于:C
     信任度:绝对        有效性:绝对
ssb  ed25519/A5575B6B495D2384
     创建于:2025-05-28  有效至:2028-05-27  可用于:S
[ 绝对 ] (1). Real Name <somestring@gsgfs.moe>

然后按照这个步骤生成剩下两个密钥...这应该还是挺简单的吧, 我就不重复了 (没有身份验证类型的就选自定义的, 那里面可以创建认证密钥)

不出意外的话最后应该有这样的输出:

sec  ed25519/46A79F4A9C465F7C
     创建于:2025-05-27  有效至:永不       可用于:C
     信任度:绝对        有效性:绝对
ssb  ed25519/A5575B6B495D2384
     创建于:2025-05-28  有效至:2028-05-27  可用于:S
ssb  cv25519/6BA8139402051176
     创建于:2025-05-28  有效至:2028-05-27  可用于:E
ssb  ed25519/437F21804219CC8A
     创建于:2025-05-28  有效至:2028-05-27  可用于:A
[ 绝对 ] (1). Real Name <somestring@gsgfs.moe>

最后一步输出 save 保存退出 (别直接Ctrl+c了)

恭喜你已经拥有了一个属于自己的 gpg 密钥了!

现在来说一下 gpg 的功能

使用 GPG

在非对称加密中公钥一般用于加密文件, 私钥用于解密文件, 也就是说如果你需要给被人发送加密文件的话就需要先获得别人的公钥.
如何拿到别人的公钥呢, 很简单, 直接问别人就好了, 可以通过公钥服务器寻找别人的公钥. (但是直接问确实是最优解)

公钥服务器有两种大的类型, 一种是具有 PGP 特色的 "联盟式", 另一种是 "中心式".
联盟式的构想很美好, 没有所谓的权威机构来统一管理密钥, 大家都依靠 "信任网" 来 "相互签名". (密钥里的信任度:绝对就是这个用途). 但是, 因为公钥不可被撤回, 可以随意使用他人的邮箱创建密钥, 于是就有了针对 "联盟式" 的机制的恶意攻击, 感兴趣的话可以自己去了解. 在发布自己的公钥前确保自己先了解了相关的问题, 再发布公钥.
另一种就是常见的 "中心式", 由统一的服务器进行管理, 添加公钥需要先验证邮箱. 我现在使用的就是这种, 你可以在keys.openpgp.org找到我的公钥.
我的公钥指纹是9122F8F082E447EC5E80945A1AD15F34F92E491B, 找到公钥后可以对比一下. 至于为什么不直接在网站上加这个指纹呢, 因为个人感觉没什么意义, 就像是在问一个小偷自己是不是小偷一样, 如果可以替换掉我的公钥的话, 也可以直接顺手把指纹也一起换掉.

同时, 记得备份你的密钥吊销证书.

加密

直接对文件进行加密的话其实很简单. 首先, 需要导入别人的公钥(假设你获得的公钥保存在gpg.asc):

gpg --import gpg.asc

如果是直接上公钥服务器搜的话就是

gpg --keyserver hkps://keys.openpgp.org/ --search-keys gsgfs@gsgfs.moe

加密一个本地文件:

echo "123456789" > 1.txt
gpg -r somestring@gsgfs.moe -e 1.txt

上面的命令先是创建了一个1.txt, 他的内容是123456789.
后面一条就是加密指令了, 使用-r(是--recipient的缩写)参数指定谁要接收这个加密文件(如果不加这个会提示), -e则是--encrypt的缩写, 表示进行加密操作.

ls 看看:

1.txt  1.txt.gpg

可以看到多了一个1.txt.gpg, 这个就是加密后的文件.

至于解密的话就很简单了, 直接gpg -d 1.txt.gpg

输出:

gpg: 由 cv25519 密钥加密,标识为 6BA8139402051176,生成于 2025-05-28
      “Real Name <somestring@gsgfs.moe>”
123456789

其中上面的是加密信息, 最后的 "123456789" 则是解密的结果.

如果要分享你的共钥的话, 可以使用--export参数导出.

gpg --export -a somestring@gsgfs.moe > gpg.asc

上面的指令使用--export指定导出somestring@gsgfs.moe的公钥, -a 参数表示使用 ASCII 字符形式(不然会生成二进制的), 最后就是使用>管道将终端的标准输出重定向到文件.(如果不会用管道的话可以使用--output参数指定文件名)

另外, GPG 也可以用来加密邮件内容, 比如说我在使用的 Thunderbird 邮件客户端就支持 GPG 加密功能.

签名

和 GPG 一样的 git 也有一个 "bug"(实际上并不是), 没有对 git 提交者的身份进行验证, 所以你可以随便使用别人的 "身份" 给 git 添加 commit.
所以, github 有一个 commit 签名功能, 如果签名经过验证就会显示一个绿色的verified, 这个绿色的标签就表示这次的提交是经过本人签名的.

github的verified

添加这个也很简单:
设置使用的签名密钥: git config --global user.signingkey <key ID> 默认启用 GPG 签名: git config --global commit.gpgsign true

对某个文件进行签名的话, 和加密类似, 只是把encrypt改成了sign, gpg -a --detach-sign 1.txt.
验证签名则是gpg --verify 1.txt.asc 1.txt.

身份验证

不常用, 过.