XiaO

GPG

XiaO / 2020-06-14


GPG 概念,原理和安装

名词概念

而现在,“PGP” 被普遍用来表示开放的 OpenPGP 标准,而不再指代最初的商业软件,因此 “PGP” 和 “OpenPGP” 是可以互换的,都指代一个标准。

“GnuPG”、“gpg” 和 “GPG” 仅指代 “Gnu Privacy Guard” 软件,它们仅是实现 PGP 功能的工具,它们所实现的 PGP 密钥,PGP 签名,PGP 加密等功能被称作 PGP 功能。

PGP 功能的基本原理

公钥密码学(非对称式密码学)进行明文与密文之间的转换所使用的密钥包含两部分,公钥和私钥。这两部分组成一对,在实际工作中这两部分即各司其职(一个公开,一个保密),又相互配合(加密与解密)。在公钥密码学中所说的密钥实际上指代的都是密钥对,本文中提及的密钥、主密钥和子密钥,其实也都指代的是密钥对。

密钥关联的用户身份

每个 PGP 密钥必须有一个或多个与之关联的用户身份(UID, user identity, 即用户全名和邮件地址),同一密钥上可以有多个身份,当使用多个身份时,其中一个身份将被标记为 “primary identity” 来让检索更简单。

密钥所关联身份的可信度

密钥所关联身份的可信度即确认该公钥属于某人的程度。有完全确定 (full),大致确定 (marginal),不确认 (unknown) 等不同的确认程度。

GPG 安装

GnuPG v.1 和 GnuPG v.2 都实现了同样的 PGP 标准,但它们提供的库和命令行工具可能不兼容,建议新版。

brew search gpg # 查看 gpg 相关信息             
==> Formulae
gpg             gpg1            gpg2            gpgme           libgpg-error
==> Casks
gpg-suite
gpg-suite-no-mail ✔
gpg-suite-pinentry
gpg-sync

brew cask install gpg-suite-no-mail # 安装不含邮箱插件的图形界面版本,使用起来相对更方便,且已经自动最新 (GnuPG/MacGPG2) 2.2.20 以上。

PGP 的主密钥

主密钥

PGP 密钥有四项能力 (capabilities):[E] 加密,[S] 签名,[A] 身份认证以及 [C] 认证其他密钥。

从技术上来说,所有密钥的生成过程都是一样的,他们本质上并无差异。只是在生成的时候,每个密钥被赋予的能力不同而已。一般而言,一个 PGP 密钥只具有一项上述能力,而具有认证能力 [C] 的密钥被称作主密钥。因为只有主密钥可用于:

主身份

身份(user ID, UID)用户身份,即与邮件的发件人一栏相同格式的字符串,包括用户名与邮箱地址。一个密钥可关联多个用户身份(通过创建新的身份或者取消旧的身份)。当有多个身份时,其中一个身份被指定为主身份,主身份可更改。

密码

密码是用于对存储在磁盘上的私钥进行对称加密的,所以这个密码其实非常重要。

密钥的自身身份 (key ID or fingerprint)

% gpg --list-keys     # 列出所有公钥信息
~/.gnupg/pubring.kbx
------------------------------
pub   rsa4096 2020-06-14 [SC]
      D993DA3B74DF27BA45622637D899FD6DE9C11BCA
uid           [ultimate] test <test@exapmle.com>
sub   rsa4096 2020-06-14 [E]

短横线上显示的是存储密钥证书信息的数据库文件。

短横线下第一行显示的是密钥生成时采用的算法 (rsa)、加密长度 (4096)、产生时间 (2020-06-14)、能力 [SC];第二行是该密钥的完整指纹 Fingerprint (40 个字符),是该密钥自身身份 (key ID) 的唯一认证。密钥的自身身份其实就是完整指纹的最后 8 位字符;第三行则是用户身份信息,包括确认程度,名字和邮件;最后一行显示的是一个具有加密能力的子密钥。

% gpg --list-secret-keys  # 列出所有私钥信息              
~.gnupg/pubring.kbx
------------------------------
sec   rsa4096 2020-06-14 [SC]
      D993DA3B74DF27BA45622637D899FD6DE9C11BCA
uid           [ultimate] test <test@exapmle.com>
ssb   rsa4096 2020-06-14 [E]

可见密钥的私钥部分和公钥部分拥有完全一样的指纹,即一个密钥其实是一个密钥对,包括公钥和私钥部分。

吊销证书

吊销证书用于将密钥(完整的密钥对)标记为吊销状态,即表明该密钥将不再被使用或信任。任何获得该文件的人,都可以将其对应的密钥标记为吊销状态。

PGP 的子密钥

子密钥可用于签名或加密,一个子密钥只具有一个功能。

子密钥只不过是天然地与主密钥关联在一起而已,其他并没有什么不同。子密钥可以单独被撤消,也可以与主密钥分开存储。一个主密钥下还可添加多个子密钥。和主密钥一样,子密钥其实也是一对。

GPG 软件生成主密钥的时候,实际上是将一个原本只具有签名 [S] 能力的密钥为其增加了认证 [C] 能力,使得主密钥同时具有了 [SC] 的能力,但主密钥依然不具备加密能力。所以,GPG 软件在生成主密钥的同时还会自动生成一个具有 [E] 能力的子密钥,使得 GPG 软件能完成加密工作。

一个主密钥其实就是网络上一个独立的电子身份。如果有人获得了一个主密钥的私钥部分(或者其子密钥的私钥部分),再加上公钥部分,那么他就可以在网络上伪装成丢失私钥的那个人的身份。

子密钥真正的好处是,它让密钥管理变得相对简单,特别是对主密钥私钥部分的管理

在创建主密钥时候,GPG 软件已经自动生成了一个用于加密的子密钥,我们可以再创建一个子密钥用于签名。这样,我们在加密或者签名的时候,实际使用的是子密钥完成的。比如,其他人将使用我们的子密钥中的公钥部分进行加密或者验证签名,而我们也将使用子密钥中的私钥来解密或者对消息签名。在这些常规操作中,我们并不会用到主密钥的私钥部分。这样我们就可以把主密钥的私钥部分备份到其他断网的地方,而不用存放在电脑,减少了其泄漏的机率。

只有当我们需要行使主密钥 [C] 的能力时(比如我们为其他人的密钥签名以进行身份确认时),我们才需要使用到主密钥的私钥部分,因为主密钥在执行 [C] 的能力时候,需要从主密钥的私钥部分新增一个自签名或者一个吊销签名。而 OpenPGP 的认证签名(来自签名者主密钥的私钥)仅与用户身份(UID)相关,而与子密钥无关。这样,如果我们的子密钥被破坏不可用时,我们可以吊销被破坏的子密钥,用一个新的子密钥代替即可。

管理主密钥

密钥存储文件的结构

GPG 生成的密钥文件,存储在 ~/.gnupg/ 目录里,其结构如下:

% tree ~/.gnupg

├── S.gpg-agent
├── S.gpg-agent.browser
├── S.gpg-agent.extra
├── S.gpg-agent.ssh
│
├── openpgp-revocs.d # 存储所有预生成的吊销证书的文件夹 (需备份)
│   └── D993DA3B74DF27BA45622637D899FD6DE9C11BCA.rev # 一个文件名对应了一个相应密钥的指纹,表明其可用于吊销相应的密钥
│
├── private-keys-v1.d # 存储所有密钥的私钥部分的文件夹,每个私钥以单文件的形式分别存放在该文件夹下 (需备份)
│   ├── 4496F1FD131F010998D2B20B05626D8DCFFC6209.key # 一个文件名即一个密钥夹
│   └── 785FF8E130273C6BDFD1F18A51A27EE66D1A3545.key # 密钥夹将密钥的私钥部分与公钥部分对应了起来
│
├── pubring.kbx # 存储密钥的公钥部分及相关信息的数据库文件,所有公钥都存储在这个文件中(需备份)
├── pubring.kbx~
│
└── trustdb.gpg # 可信任数据库 (无需备份)

密钥夹(keygrip)

% gpg --list-keys --with-keygrip # 列出所有公钥信息,以及其密钥夹

~/.gnupg/pubring.kbx
------------------------------
pub   rsa4096 2020-06-14 [SC]
      D993DA3B74DF27BA45622637D899FD6DE9C11BCA
      Keygrip = 785FF8E130273C6BDFD1F18A51A27EE66D1A3545 # 主密钥对应的密钥夹
uid           [ultimate] test <test@exapmle.com>
sub   rsa4096 2020-06-14 [E]
      Keygrip = 4496F1FD131F010998D2B20B05626D8DCFFC6209 # 子密钥对应的密钥夹

如上,我们可以看到两个密钥夹,分属主密钥和子密钥,因为我们在使用 GPG 软件创建密钥的时候,自动生成了主密钥和一个子密钥。这两个密钥夹正好对应了密钥的私钥部分所在文件夹 (private-keys-v1.d) 中文件的名字。

私钥文件夹与公钥密钥盒

pubring.gpg 与 secring.gpg 属于历史遗留产物,他们分别用于存储所有公钥和私钥,即所有的公钥放在一个单文件中,所有的私钥放在一个单文件中。

GPG 新的版本改变了文件存储结构,其中的公钥部分 (pubring.gpg) 由公钥密钥盒 (pubring.kbx) 取代,可用工具 kbxutil 显示其内部结构;而私钥部分 (secring.gpg) 则专门放入私钥文件夹 (private-keys-v1.d) 中,且每个私钥分开以单文件的形式存放。如此,新的文件存储结构更利于快速检索和处理。

如下,当我们用命令 gpg --list-secret-keys 列出私钥的时候,似乎私钥也存储在 pubring.kbx 公钥密钥盒中,实则不然。gpg --list-secret-keys 命令原本是在旧版本的 GPG 软件里提取存放在 secring.gpg 文件下的私钥的,现在新版本里其实没有 secring.gpg 文件,此刻命令 gpg --list-secret-keys 所列出的私钥其实是从私钥文件夹 (private-keys-v1.d) 经过 gpg-agent 转换而来的。

% gpg --list-secret-keys --with-keygrip

~/.gnupg/pubring.kbx
------------------------------
sec   rsa4096 2020-06-14 [SC]
      D993DA3B74DF27BA45622637D899FD6DE9C11BCA
      Keygrip = 785FF8E130273C6BDFD1F18A51A27EE66D1A3545
uid           [ultimate] test <test@exapmle.com>
ssb   rsa4096 2020-06-14 [E]
      Keygrip = 4496F1FD131F010998D2B20B05626D8DCFFC6209

备份与删除密钥

如上我们知道,在日常加密签名操作时,都只使用子密钥来加密和签名,不会用到主密钥。所以为了安全起见,我们在备份了主密钥的私钥部分以及吊销证书后,需要将其从我们的联网电脑里删除。

-----BEGIN PGP PRIVATE KEY BLOCK-----

lQdGBF7l2qABEADZelFYeVt3vNxdoNn0FC2i3blvuupdrfwFP6HZq6J9v+Iv5MlJ
mHFvgx5zX+UwzeUHuCBClN4qnfI5CCkYD7c6LeBhEonDIM1BpLY0yJfPIUtoU+wo
... # deleted lines
bW+ej7o+CEV7xf+hyW4jOiyy+mG1tbIbZiyOQVDnZ3RG0QkZIHmhlRqtO2yzQ8O0
0MiAAlXeJYSVVXrWrg2omtZti4Yy
=O8Sa
-----END PGP PRIVATE KEY BLOCK-----
-----BEGIN PGP PUBLIC KEY BLOCK-----

mQINBF7l2qABEADZelFYeVt3vNxdoNn0FC2i3blvuupdrfwFP6HZq6J9v+Iv5MlJ
mHFvgx5zX+UwzeUHuCBClN4qnfI5CCkYD7c6LeBhEonDIM1BpLY0yJfPIUtoU+wo
... # deleted lines
uJ8uld+Bz5E3IIecO4ltb56Puj4IRXvF/6HJbiM6LLL6YbW1shtmLI5BUOdndEbR
CRkgeaGVGq07bLNDw7TQyIACVd4lhJVVetauDaia1m2LhjI=
=mGS5
-----END PGP PUBLIC KEY BLOCK-----  
% gpg --list-keys --with-keygrip E9C11BCA 
pub   rsa4096 2020-06-14 [SC]
      D993DA3B74DF27BA45622637D899FD6DE9C11BCA
      Keygrip = 785FF8E130273C6BDFD1F18A51A27EE66D1A3545 # 主密钥对应的密钥夹
uid           [ultimate] test <test@exapmle.com>
sub   rsa4096 2020-06-14 [E]
      Keygrip = 4496F1FD131F010998D2B20B05626D8DCFFC6209

在 GnuPG 2.1 及后续版本里,直接在 ~/.gnupg/private-keys-v1.d/文件夹中,删除以与主密钥对应的密钥夹命名的私钥文件即可。例如文中删除 ~/.gnupg/private-keys-v1.d/785FF8E130273C6BDFD1F18A51A27EE66D1A3545.key文件。

我们通过列出密钥的私钥部分,可见主密钥的私钥标志 sec 旁边多了一个 # 符号,表示此刻我们的密钥文件里已不存在该私钥文件了。

% gpg --list-secret-keys --with-keygrip
~/.gnupg/pubring.kbx
------------------------------
sec#  rsa4096 2020-06-14 [SC]
      D993DA3B74DF27BA45622637D899FD6DE9C11BCA
      Keygrip = 785FF8E130273C6BDFD1F18A51A27EE66D1A3545
uid           [ultimate] test <test@exapmle.com>
ssb   rsa4096 2020-06-14 [E]
      Keygrip = 4496F1FD131F010998D2B20B05626D8DCFFC6209

关于命令行与图形界面软件的插曲

末了,讲一个关于使用图形界面软件和命令行工具导入私钥时的插曲。

在记录本文的时候,我生成了 UID 为 test <test@example.com> 的密钥。当需要重新导入自己所用的私钥的时候,我随手清空了 ~/.gnupg/ 文件夹下的全部内容,然后尝试导入密钥:

 % gpg --import Secret.asc
 
gpg: key 6116A1BA/6116A1BA: error sending to agent: No such file or directory
gpg: error building skey array: No such file or directory
gpg: error reading 'Secret.asc': No such file or directory
gpg: import from 'Secret.asc' failed: No such file or directory

原本目录下有存放密钥信息的文件和文件夹,导入密钥的时候,密钥文件被 gpg-agent 处理后写入密钥存储文件,比如 private-keys-v1.d。当密钥存储文件被清空后,gpg-agent 处理过的文件就找不到存放密钥的位置了,故而无法导入。gpg-agent 心里估计也纳闷儿:“咦,我原本是建了这个文件的啊,怎么找不到啦?那算了,不搞了,直接给他说找不到路径吧。“此刻的处理办法:

所以,感觉上应该是重启后的 gpg-agent,当导入密钥时,如果不存在相关密钥存储文件,会自动新建一个吧。另外,此番风波,可见图形界面软件和命令行工具的差异性,命令行工具能给出更多更准确的提示信息。

~/.gnupg 文件夹手动清空前:

% tree ~/.gnupg

├── S.gpg-agent
├── S.gpg-agent.browser
├── S.gpg-agent.extra
├── S.gpg-agent.ssh
├── openpgp-revocs.d
│   └── D993DA3B74DF27BA45622637D899FD6DE9C11BCA.rev
├── private-keys-v1.d 
│   ├── 4496F1FD131F010998D2B20B05626D8DCFFC6209.key 
│   └── 785FF8E130273C6BDFD1F18A51A27EE66D1A3545.key
├── pubring.kbx
├── pubring.kbx~
└── trustdb.gpg)

~/.gnupg 文件夹手动清空后:

% tree ~/.gnupg

├── S.gpg-agent
├── S.gpg-agent.browser
├── S.gpg-agent.extra
└── S.gpg-agent.ssh