使用 GPG 为代码签名,让代码更加可信

前言

  GPG,全称 GNU Privacy Guard,也可以写成 GnuPG。它是一款免费开源的加密软件,也是不开源不免费的 PGP(Pretty Good Privacy)的替代品,有关详情可以查看参考资料。由于 GPG 可以用于签名或者加密,所以在文件加密、邮件加密、代码签名等方面应用较多。

邮件加密

  用过 Gmail 的同学可能看到过像下面这样不同的安全性信息。第一幅图是在没有以 SSL 方式与邮件服务器连接发送的时候,Gmail 会将邮件的安全性等级认定为最低的未加密级别。第二幅图是正常以 SSL 方式与邮件服务器连接发送时,Gmail 验证了 SSL 证书与邮送域一致并认定为标准加密的安全性等级。第三幅图是当我们使用 GPG 给邮件进行加密时,Gmail 会同时收到一个签名公钥的附件。

未加密邮件 Uncertified mail 标准加密邮件 Normal certified mail GPG 加密邮件 GPG certified mail

  这里看起来可能有点奇怪,为什么 GPG 加密的邮件和一般的标准加密邮件除了附件没有别的差别?实际上,我们如果想要给对方发一份 GPG 加密的邮件是需要用对方的 GPG 公钥来加密邮件内容的。当对方收到发送的 GPG 加密邮件时会用自己的私钥进行解密,从而获知加密邮件的内容。也就是说,上面的第三幅图并非是真正的 GPG 加密的邮件。第三幅图是作者用私钥签名的邮件,收件人可以用 keyserver 上查询到的公钥来验证邮件内容是否真的来自于发件人。

代码签名

  相比邮件加密而言,GPG 用于代码签名则正好相反。在代码签名中,我们也像上面第三幅图那样使用私钥为每一次 commit 签名。而代码的使用者可以根据作者公布的公钥对代码内容进行验证,从而确保代码是来自作者本人。苹果开发者证书、Google Play 开发者证书实际上也是起到了这样一个核验代码作者身份的作用。

Git 的“漏洞”

  在 Git 提交 commit 之前,Git 会要求我们设定好 username 和 email(类似下面)。但是如果我们不设置成自己的 github username 和 email 会怎么样呢?其实不会怎么样,只是 Github 不会把这些 commit 算在你头上,而是算在了你伪造的用户头上。如果我们想要伪装成某位业界大咖的 github 账户为项目提交代码,似乎在理论上也没有什么不可以的。在参考资料四中,作者做了一些示例的尝试,发现“只要知道邮箱,就可以用他人的名义提交 commit”。但说到底,这种方式是不道德的,且没有任何实质意义的。

git config --global user.name "zhonger"
git config --global user.email "zhonger@live.cn"

  因此,为了使代码更加可信、确保是由作者本人提交的,Github 等代码托管平台纷纷支持了 GPG 签名。因为 GPG 公钥和私钥是 RSA 非对称加密生成的,所以理论上是不存在被伪造或反编码风险的。和从 GPG 密钥服务器中的公钥查询验证不同,Github 等代码托管平台只信任由作者本人在设置中配置的 GPG 公钥,与只信任配置的 SSH 公钥访问代码类似。

实践

安装 GPG

  在不同的平台上都已经提供了 GPG,大部分只需要一条命令即可完成安装。

# MacOS
brew install gpg

# Debian, Ubuntu
sudo apt install -y gnupg

# CentOS
sudo yum install -y gnupg

# Windows
# 推荐使用 WinGPG,下载地址为 https://scand.com/products/wingpg/

# Archlinux
sudo pacman -S gnupg

验证安装

╰─$ gpg --version                                                        
gpg (GnuPG) 2.3.6
libgcrypt 1.10.1
Copyright (C) 2021 Free Software Foundation, Inc.
License GNU GPL-3.0-or-later <https://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Home: /Users/zhonger/.gnupg
支持的算法:
公钥: RSA, ELG, DSA, ECDH, ECDSA, EDDSA
密文: IDEA, 3DES, CAST5, BLOWFISH, AES, AES192, AES256, TWOFISH,
    CAMELLIA128, CAMELLIA192, CAMELLIA256
AEAD: EAX, OCB
散列: SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224
压缩:  不压缩, ZIP, ZLIB, BZIP2

生成密钥

  验证安装成功后,即可使用以下命令生成字母的密钥。如下所示,我们可以很简单地得到一对 GPG 密钥。

╰─$ gpg --full-generate-key                                                       
gpg (GnuPG) 2.3.6; 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.

请选择您要使用的密钥类型:
   (1) RSA 和 RSA
   (2) DSA 和 Elgamal
   (3) DSA(仅用于签名)
   (4) RSA(仅用于签名)
   (9) ECC(签名和加密) *默认*
  (10) ECC(仅用于签名)
 (14)卡中现有密钥
您的选择是?
请选择您想要使用的椭圆曲线:
   (1) Curve 25519 *默认*
   (4) NIST P-384
   (6) Brainpool P-256
您的选择是?
请设定这个密钥的有效期限。
         0 = 密钥永不过期
      <n>  = 密钥在 n 天后过期
      <n>w = 密钥在 n 周后过期
      <n>m = 密钥在 n 月后过期
      <n>y = 密钥在 n 年后过期
密钥的有效期限是?(0)
密钥永远不会过期
这些内容正确吗? (y/N) y

GnuPG 需要构建用户标识以辨认您的密钥。

真实姓名: zhonger
电子邮件地址: zhonger@lisz.me
注释: zhonger
您选定了此用户标识:
    “zhonger (zhonger) <zhonger@lisz.me>”

更改姓名(N)、注释(C)、电子邮件地址(E)或确定(O)/退出(Q)? O
我们需要生成大量的随机字节。在质数生成期间做些其他操作(敲打键盘
、移动鼠标、读写硬盘之类的)将会是一个不错的主意;这会让随机数
发生器有更好的机会获得足够的熵。
gpg: 警告:服务器 ‘gpg-agent’ 比我们的版本更老 (2.2.34 < 2.3.6)
gpg: 注意: 过时的服务器可能缺少重要的安全修复。
gpg: 注意: 使用 “gpgconf --kill all” 来重启他们。
我们需要生成大量的随机字节。在质数生成期间做些其他操作(敲打键盘
、移动鼠标、读写硬盘之类的)将会是一个不错的主意;这会让随机数
发生器有更好的机会获得足够的熵。
gpg: 吊销证书已被存储为‘/Users/zhonger/.gnupg/openpgp-revocs.d/612E7E8200528FEC0B8AC3C715F73C3703B9796C.rev’
公钥和私钥已经生成并被签名。

pub   ed25519 2022-06-01 [SC]
      612E7E8200528FEC0B8AC3C715F73C3703B9796C
uid                      zhonger (zhonger) <zhonger@lisz.me>
sub   cv25519 2022-06-01 [E]

  这里生成密钥过程中要求选择的密钥类型、椭圆曲线、密钥有效期都采用了默认的选择(按 Enter 键即可),可根据个人需要自行选择。接下来的用户标识根据个人的 真实姓名(英文)、Github 邮件和用户名(注释),最后输入大写字母 O 结束设置。这一步骤结束后系统会提醒输入对私钥的密码,通常需要两次验证输入。至此成功生成了一对 GPG 密钥。

Github 配置密钥

  生成密钥之后我们就要将公钥添加到 Github上,并尝试使用私钥给代码签名并提交,验证是否被 Github 成功验证。

打印公钥

  如下命令所示,可以查询刚才生成密钥的公钥内容,并复制。

# 列举本地所有密钥
gpg --list-keys

# 查询指定 id 密钥的公钥内容
gpg --armor --export <GPG_KEY_ID》

列举密钥 List gpg keys 查询公钥内容 Check public key content

添加 GPG 公钥

  访问 https://github.com/settings/gpg/new 添加刚复制的 GPG 公钥内容,此处的 Title 可以任意命名,只要与已有的 Title 不重复即可。

添加公钥 Add public key

查询已添加公钥

  访问 https://github.com/settings/keys 即可看到刚才新添加的 GPG 公钥。这里可以看到,邮箱地址后面多了个 Unverified。这是因为这个邮箱是一个没有绑定 Github 账户的假邮箱,如果邮箱是验证过的,这里就不会有这个标识了。

查询公钥 List all keys

为代码签名并提交

  为了不用每次提交 commit 的时候都要手动声明使用某个 GPG 私钥进行签名,这里在 git 的全局配置中添加两个配置项:user.signingkey(签名密钥 ID)和 commit.gpgsign(全部提交使用 GPG 签名)。

git config --global user.signingkey 612E7E8200528FEC0B8AC3C715F73C3703B9796C
git config --global commit.gpgsign true

  当我们设置好全局 Git 配置后,再次像平常那样执行 commit 提交时,就会弹出输入私钥密码的窗口,正常输入即可。

验证提交签名

  虽然以上步骤已经完成了 GPG 为 commit 加一把锁,但是可能还不敢确认这把锁是否存在。这里有两种方法可以验证:一种是通过查询本地 git 来查看,另一种是通过 Github 在线查看。下面是使用个人的私钥(非本文示例密钥)分别采用两种方法在日常 git 项目中验证的效果图。

# 本地验证提交签名
git log --show-signature

本地验证提交签名 Verify the signature on local git Github 验证提交签名 Verify the signature on github

其他相关问题

问题一

如果想要导出私钥和公钥备份或迁移怎么办?

解答

  一般来说,GPG 密钥的保管非常重要。如果 GPG 密钥的私钥丢失或者被他人窃取,那么将会很危险,因为别人可以使用该私钥在任何文件或邮件上签上你名字。所以重装电脑之前一定要注意好备份,即使平时也可能需要将 GPG 密钥存在一个非本地且安全可靠的位置。以下命令可以实现公钥和私钥的导出。

gpg --armor --output gpg.pub --export <GPG_KEY_ID>
gpg --armor --output gpg.key --export-secret-keys <GPG_SECRET_KEY>
问题二

如何吊销已生成或丢失的 GPG 密钥?

解答

  当 GPG 私钥发生丢失(公钥丢失不影响安全,找回即可)时,我们需要在 Github 删除对应的公钥,这样可以保证 Github 不会再承认丢失私钥签名的代码。当然,为了除 Github 外其他正常的邮件、文件不再信任该丢失的私钥签名,我们需要向 GPG 声明该 GPG 公钥吊销,从而达到吊销私钥的目的(并非是删除密钥本身,而是标记作废)。具体操作如以下命令所示。

# 撤销密钥
gpg --output revoke.asc --gen-revoke <GPG_KEY_ID>
# 将撤销密钥导入本地钥匙环
gpg --import revoke.asc
# 搜索 GPG 密钥服务器中的密钥
gpg --keyserver hkps://keys.openpgp.org --search-keys <GPG_KEY_ID> 
# 撤销 GPG 密钥服务器上的密钥
gpg --keyserver hkps://keys.openpgp.org --send-keys <GPG_KEY_ID> 

(2022年9月30日补充)

问题三

如果不弹出输入密码框怎么办?

解答

  这主要是因为终端窗口加载的问题,如下所示添加对 GPG 使用的终端的声明,认定输入密码框的终端与现有终端一致,那么就会正常弹出输入密码框了。

export GPG_TTY=$(tty)

参考资料

版权声明: 如无特别声明,本文版权归 仲儿的自留地 所有,转载请注明本文链接。

(采用 CC BY-NC-SA 4.0 许可协议进行授权)

本文标题:《 GPG:为你的 Git 提交记录加一把锁 》

本文链接:https://lisz.me/tech/webmaster/gpg.html

本文最后一次更新为 天前,文章中的某些内容可能已过时!