使用 GPG 加密、解密和验证信息

本文将主要利用 GNU Privacy Guard (简称 “GnuPG” 或 “GPG”) 完成信息的加密, 解密和验证任务. 本文不会对 GPG 的工作原理以及 OpenPGP 的规范 (参见 RFC 4880) 做过多的解读. 仅针对完成上述的任务而言, 只需要对其了解大体的情况即可.

简要原理

How PGP encryption works visually

以上截取自 Pretty Good Privacy 维基词条的 SVG 图像 (可能需要切换为亮色模式以看清图中的文字). 其中的大部分工作将会由 GPG 完成. 读者需要注意的是从图中可以看到 “receiver’s public key” 和 “receiver’s private key” - 这暗示 OpenPGP 标准中存在 “公钥” 和 “私钥” 两个概念, 且这两种密钥都归属于同一个人. 加密时, 信息发送者需要信息接收者的公钥; 解密时, 信息接收者需要自己的私钥.

测试环境

笔者将主要使用本机的 Arch Linux 进行测试和演示. 同时会利用一台远端的 Linux 服务器用以演示双方通信操作.

涉及软件包

  • core/gnupg - 提供 gpg
  • core/coreutils - 提供 tr, head
  • core/openssh - 提供 ssh, scp

信息

本文中涉及到本地机器的所有工作将在 ~/Temp/gpg 文件夹下进行. 该文件夹下有需要传输的信息 message.txt:

1
2
$ cat message.txt        
CjYIdc["Ns`3[BA\=5#+TM<[ORUhQ|i-E>!S0e!hc_A'R_h!9SU2v$oJ2SIKGp"l

信息收发双方

角色 用户名 邮箱
发送者 test1 test1@foo.bar
接收者 test2 test2@foo.bar

准备

安装 GPG

Linux 环境无需多言. 对于 Windows 可以选择 Gpg4win - 这是一个 GPG 在 Windows 上的实现.

生成强密码

后续的操作中可能会涉及到生成强密码的需求. 自然, 一个可以在脑中记住的强密码是最好不过的, 不过这里将演示一个利用 /dev/urandom 生成高强度密码的方法:

1
2
$ tr -dc "[:graph:]" < /dev/urandom | head -c 64; echo ''
Q-SU0QZA:{6h%LLoR@Bd2M#jPOb.nu8&xcimt}atyo|G]O{Ay>+R;T'gGrUH)bWk

其主要原理是, tr 可以 “translate or delete characters”^1. tr/dev/urandom 设备文件中获取到随机字符流后, 可以仅保留符合 [:graph:] 条件 (“all printable characters, not including space”^1) 的字符, 再让 head 截取其中的前 64 个字符即可. 由此得到的则是长度为 64 的随机可见字符串.

同理, 若要生成 32 位仅包含小写字母和数字的字符串:

1
2
$ tr -dc "a-z0-9" < /dev/urandom | head -c 32; echo ''
b2ylyqcs9x5gureu43b3ycw9txgww5sb

生成低熵随机数

一些无头服务器可能无法满足 “低熵” 的条件, 从而导致生成的随机数不满足一些安全性要求较高的程序 (例如本文提到的 GPG) 的需求. Linux 上可以借助 haveged, rng-tools 等来避免这种情况. 本文不再赘述.

密钥操作

生成密钥

使用 gpg --full-generate-key 来生成一个密钥对. 以下的代码块记录了终端中回显的整个过程.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
$ gpg --full-generate-key      
gpg (GnuPG) 2.2.40; Copyright (C) 2022 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.

Please select what kind of key you want:
(1) RSA and RSA (default)
(2) DSA and Elgamal
(3) DSA (sign only)
(4) RSA (sign only)
(14) Existing key from card
Your selection? 1
RSA keys may be between 1024 and 4096 bits long.
What keysize do you want? (3072) 4096
Requested keysize is 4096 bits
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) 0
Key does not expire at all
Is this correct? (y/N) y

GnuPG needs to construct a user ID to identify your key.

Real name: test1
Email address: test1@foo.bar
Comment: test1's PGP key
You selected this USER-ID:
"test1 (test1's PGP key) <test1@foo.bar>"

Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? o
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
gpg: revocation certificate stored as '/home/littleye233/.gnupg/openpgp-revocs.d/EB28C6D42ACFC1798DC7D1CB46D85BAE755DB198.rev'
public and secret key created and signed.

pub rsa4096 2022-12-09 [SC]
EB28C6D42ACFC1798DC7D1CB46D85BAE755DB198
uid test1 (test1's PGP key) <test1@foo.bar>
sub rsa4096 2022-12-09 [E]

其中需要用户回答如下几个问题:

  • 密钥的种类 - RSA and RSA
  • RSA 密钥的长度 - 4096
  • 密钥的有效期 - key does not expire
  • 确认信息正确性 - y
  • 真实姓名 - test1
  • 电子邮件地址 - test1@foo.bar
  • 注释 - test1's PGP key
  • 确认信息正确性 - o
  • (未回显) (可选) 密码片语 (passphrase)

若设置密码片语, 在解密时则需要输入该字符串.

查看密钥

查看本机中导入或生成的密钥有多种形式.

列出私钥和指纹

1
2
3
4
5
6
7
8
9
10
11
12
$ gpg --list-secret-keys --keyid-format=long --with-fingerprint
gpg: checking the trustdb
gpg: marginals needed: 3 completes needed: 1 trust model: pgp
gpg: depth: 0 valid: 11 signed: 0 trust: 0-, 0q, 0n, 0m, 0f, 11u
/home/littleye233/.gnupg/pubring.kbx
------------------------------------
... 省略多个私钥 ...

sec rsa4096/46D85BAE755DB198 2022-12-09 [SC]
Key fingerprint = EB28 C6D4 2ACF C179 8DC7 D1CB 46D8 5BAE 755D B198
uid [ultimate] test1 (test1's PGP key) <test1@foo.bar>
ssb rsa4096/EEAD650E9DE15AB8 2022-12-09 [E]

列出公钥和指纹

1
2
3
4
5
6
7
8
9
$ gpg --list-keys --keyid-format=long --with-fingerprint 
/home/littleye233/.gnupg/pubring.kbx
------------------------------------
... 省略多个公钥 ...

pub rsa4096/46D85BAE755DB198 2022-12-09 [SC]
Key fingerprint = EB28 C6D4 2ACF C179 8DC7 D1CB 46D8 5BAE 755D B198
uid [ultimate] test1 (test1's PGP key) <test1@foo.bar>
sub rsa4096/EEAD650E9DE15AB8 2022-12-09 [E]

如果仅仅需要获取公钥指纹 (Key fingerprint) 等信息, 这个指令和上一条指令呈现的结果几乎类似. 但前者同时可以列出导入的公钥 - 这对于加密和验证信息的工作很有用.

导出公钥

1
2
3
4
$ gpg --export --armor 46D85BAE755DB198
-----BEGIN PGP PUBLIC KEY BLOCK-----
... 省略具体内容 ...
-----END PGP PUBLIC KEY BLOCK-----

--armor 选项在 GPG 的帮助文档中有如下说明:

1
2
--armor
-a Create ASCII armored output. The default is to create the binary OpenPGP format.

这种输出形式便于在不同机器之间传输密钥^2. 同时这也是一种很好的分享公钥的方式:

1
gpg --output test1_at_foo_dot_bar.asc --export --armor 46D85BAE755DB198

值得注意的是, 这里确定密钥使用的是密钥 ID - 46D85BAE755DB198. 事实上, 换用真实姓名和电子邮箱也是可以用来确定密钥的. 这里可以看出, 如果对同一个真实姓名或电子邮箱生成了多个密钥, GPG 在搜索密钥时, 默认会从列表中的第一个符合条件的密钥开始. 这在一些场合下 (例如需要输入密码片语的情景, 此时为了跳过某个密钥可能需要反复关闭输入密码片语的窗口) 可能会比较烦人. 因此, 若确实有需要多个 PGP 密钥的场合, 请最好避免使用相同的真实姓名和电子邮箱.

导出私钥

1
2
3
4
$ gpg --export-secret-key 46D85BAE755DB198
-----BEGIN PGP PRIVATE KEY BLOCK-----
... 省略具体内容 ...
-----END PGP PRIVATE KEY BLOCK-----

导入密钥

假设 test2 用户发送来了 Ta 的 PGP 公钥 (test2_at_foo_dot_bar.asc), 则此时可以使用 gpg --import 导入:

1
gpg --import test2_at_foo_dot_bar.asc

对于远端机器, 则可以使用管道 (pipe)^2:

1
2
3
4
sec   rsa4096/11B3A67EC13C2571 2022-12-09 [SC]
Key fingerprint = 5359 F3FA 8933 9C43 DB47 61A8 11B3 A67E C13C 2571
uid [ultimate] test2 (test2's PGP key) <test2@foo.bar>
ssb rsa4096/9C0A6E908BC7D25D 2022-12-09 [E]
1
2
3
4
$ ssh root@54.255.5.62 gpg --export --armor 11B3A67EC13C2571 | gpg --import
gpg: key 11B3A67EC13C2571: public key "test2 (test2's PGP key) <test2@foo.bar>" imported
gpg: Total number processed: 1
gpg: imported: 1

移除密钥

移除公钥需要 --delete-keys 选项:

1
gpg --delete-keys test2@foo.bar

移除私钥则需要 --delete-secret-keys 选项:

1
gpg --delete-secret-keys test2@foo.bar

设置信任等级

默认导入的密钥的信任等级是 “unknown”. 在后续解密和验证信息的时候, 则可能会出现如下的输出:

1
2
3
gpg: WARNING: This key is not certified with a trusted signature!
gpg: There is no indication that the signature belongs to the owner.
Primary key fingerprint: EB28 C6D4 2ACF C179 8DC7 D1CB 46D8 5BAE 755D B198

此时则需要将信任等级更改为 “utimate” - 与本地生成的密钥的信任等级相同. 具体操作则是执行 gpg --edit-key. 例如, 在 test2 用户的远端服务器上更改 test1 用户的 PGP 密钥的信任等级时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
$ gpg --edit-key test1@foo.bar
gpg (GnuPG) 2.2.27; Copyright (C) 2021 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.


pub rsa4096/46D85BAE755DB198
created: 2022-12-09 expires: never usage: SC
trust: full validity: unknown
sub rsa4096/EEAD650E9DE15AB8
created: 2022-12-09 expires: never usage: E
[ unknown] (1). test1 (test1's PGP key) <test1@foo.bar>

gpg> trust
pub rsa4096/46D85BAE755DB198
created: 2022-12-09 expires: never usage: SC
trust: full validity: unknown
sub rsa4096/EEAD650E9DE15AB8
created: 2022-12-09 expires: never usage: E
[ unknown] (1). test1 (test1's PGP key) <test1@foo.bar>

Please decide how far you trust this user to correctly verify other users' keys
(by looking at passports, checking fingerprints from different sources, etc.)

1 = I don't know or won't say
2 = I do NOT trust
3 = I trust marginally
4 = I trust fully
5 = I trust ultimately
m = back to the main menu

Your decision? 5
Do you really want to set this key to ultimate trust? (y/N) y

pub rsa4096/46D85BAE755DB198
created: 2022-12-09 expires: never usage: SC
trust: ultimate validity: unknown
sub rsa4096/EEAD650E9DE15AB8
created: 2022-12-09 expires: never usage: E
[ unknown] (1). test1 (test1's PGP key) <test1@foo.bar>
Please note that the shown key validity is not necessarily correct
unless you restart the program.

gpg> q

信息加密

正如前文所述, 信息的加密需要信息接收者的公钥. 而刚刚笔者已经导入了 test2 用户的公钥. 现在则需要 --encrypt 选项来进行加密:

1
gpg --encrypt --armor --recipient test2@foo.bar message.txt

--armor 的作用略去不提. --recipient 用以指定信息接收方 (这样其就可以使用自己的私钥解密), 可以指定多个 (诸如 --recipient email1 --recipient email2 的格式). 命令执行成功后, 当前目录会生成一个扩展名为 .asc 的文件 (message.txt.asc). 使用 file 查看其文件信息, 得到:

1
2
$ file message.txt.asc 
message.txt.asc: PGP message Public-Key Encrypted Session Key (old)

签名

--sign 可以表明信息的发送者是谁. 默认签名使用 “默认密钥” (若未明确设置, 则是密钥列表的第一个密钥). 若要显式指定一个密钥, 则需要使用 --local-user 选项. 另外换用 --default-key 选项可以同时设置 “默认密钥”.

1
gpg --encrypt --armor --sign --local-user test1@foo.bar --recipient test2@foo.bar message.txt

当然, 签名操作需要验证签名方的密码片语, 如果有的话.

信息解密

解密时, 则需要使用 --decrypt 选项:

1
gpg --decrypt message.txt.asc

与加密操作不同的是, 解密时, GPG 会尝试所有的密钥, 无需自行指定密钥.

对于未签名的加密信息, 可能的输出如下:

1
2
3
4
5
$ gpg --decrypt message.txt.asc                                   
CjYIdc["Ns`3[BA\=5#+TM<[ORUhQ|i-E>!S0e!hc_A'R_h!9SU2v$oJ2SIKGp"l
gpg: encrypted with RSA key, ID EEAD650E9DE15AB8
gpg: encrypted with 4096-bit RSA key, ID 9C0A6E908BC7D25D, created 2022-12-09
"test2 (test2's PGP key) <test2@foo.bar>"

对于已签名的加密信息, 在信息发送者的公钥未导入时, 此时无法检查签名, 可能的输出如下:

1
2
3
4
5
6
7
8
$ gpg --decrypt message.txt.asc
CjYIdc["Ns`3[BA\=5#+TM<[ORUhQ|i-E>!S0e!hc_A'R_h!9SU2v$oJ2SIKGp"l
gpg: encrypted with 4096-bit RSA key, ID 9C0A6E908BC7D25D, created 2022-12-09
"test2 (test2's PGP key) <test2@foo.bar>"
gpg: Signature made Fri 09 Dec 2022 04:25:43 PM UTC
gpg: using RSA key EB28C6D42ACFC1798DC7D1CB46D85BAE755DB198
gpg: issuer "test1@foo.bar"
gpg: Can't check signature: No public key

导入设置信任等级后, 可能的输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
$ gpg --decrypt message.txt.asc                    
CjYIdc["Ns`3[BA\=5#+TM<[ORUhQ|i-E>!S0e!hc_A'R_h!9SU2v$oJ2SIKGp"l
gpg: encrypted with 4096-bit RSA key, ID 9C0A6E908BC7D25D, created 2022-12-09
"test2 (test2's PGP key) <test2@foo.bar>"
CjYIdc["Ns`3[BA\=5#+TM<[ORUhQ|i-E>!S0e!hc_A'R_h!9SU2v$oJ2SIKGp"l
gpg: Signature made Fri 09 Dec 2022 04:25:43 PM UTC
gpg: using RSA key EB28C6D42ACFC1798DC7D1CB46D85BAE755DB198
gpg: issuer "test1@foo.bar"
gpg: checking the trustdb
gpg: marginals needed: 3 completes needed: 1 trust model: pgp
gpg: depth: 0 valid: 4 signed: 0 trust: 0-, 0q, 0n, 0m, 0f, 4u
gpg: Good signature from "test1 (test1's PGP key) <test1@foo.bar>" [ultimate]

信息验证

分离密钥

之前生成的加密文件 message.txt.asc 包含了原始文件的信息. 如下的命令使用了 --detach-sign 选项, 则是生成一个分离式 (detached) 的密钥. 这可以用来在无需加密的场合验证身份:

1
gpg --local-user 46D85BAE755DB198 --output message.txt.asc --detach-sign --armor message.txt

验证

1
2
3
4
$ gpg --verify message.txt.asc message.txt
gpg: Signature made Sat 10 Dec 2022 12:46:26 AM CST
gpg: using RSA key EB28C6D42ACFC1798DC7D1CB46D85BAE755DB198
gpg: Good signature from "test1 (test1's PGP key) <test1@foo.bar>" [ultimate]

脚注

Posted on

2022-12-09

Updated on

2022-12-09

Licensed under

Comments