在 Github 上使用 GPG 的全过程

2019-08-04
  1. Github
  2. GPG
  3. Git

起因

其实在很早之前 Github 就已经充分支持 GPG 密钥了,而在我之前使用 Github 的两年时间内,竟对此一无所知,实在有些“没见过世面”。直至近日,在一次偶然查看仓库的 commit 历史中,发现某些 commit 有一个不同寻常的绿色标记(Verified),不仅美观(?)而且看上去舒心,如图所示:

漂亮的 Verified 标记

点击这个标记,得知这一次 commit 是经过签名验证的(signed with a verified signature),因此,我便开始研究如何利用 GPG 对自己的每次 commit 进行签名验证。

什么是 GPG

GnuPG is a complete and free implementation of the OpenPGP standard as defined by RFC4880(also known as PGP). GnuPG allows you to encrypt and sign your data and communications; it features a versatile key management system, along with access modules for all kinds of public key directories. GnuPG, also known as GPG, is a command line tool with features for easy integration with other applications.

以上是从 GPG 网站 上摘取的部分简介,总的来说,GPG 的功能十分丰富,然而我这次主要是用它来对 Git 中的 commit 进行签名验证,所以需要做的事情也不算太复杂:

  1. 生成自己的 GPG 密钥
  2. 关联 GPG 公钥与 Github 账户
  3. 设置利用 GPG 私钥对 commit 进行签名
  4. 可选步骤:信任 Github 的 GPG 密钥

过程

安装 GPG

由于我的目的是在 Git 中使用 GPG,而 Windows 版本的 Git 发行包中,已经包含了可用的 GPG 命令行。判断方法也很简单,打开 Git Bash,输入gpg --version,可以看到类似的 GPG 版本信息:

$ gpg --version
gpg (GnuPG) 2.2.16-unknown
libgcrypt 1.8.4
Copyright (C) 2019 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 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: /c/Users/---/.gnupg
Supported algorithms:
Pubkey: RSA, ELG, DSA, ECDH, ECDSA, EDDSA
Cipher: IDEA, 3DES, CAST5, BLOWFISH, AES, AES192, AES256, TWOFISH,
        CAMELLIA128, CAMELLIA192, CAMELLIA256
Hash: SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224
Compression: Uncompressed, ZIP, ZLIB, BZIP2

不过需要说明的是,如果所安装的 Git 版本比较久远(比如我一开始所用的 Git 发行包是 2017 年的),那么很可能其包含的 GPG 版本过低,影响后续的操作,所以建议直接更新 Git 发行包至最新版本。

生成自己的 GPG 密钥

打开 Git Bash,运行gpg --full-generate-key,根据提示,输入相应的个人信息(需要注意的是邮箱必须要使用在 Github 中验证过的邮箱)、自定义密钥参数、设置私钥密码等等,即可生成自己的 GPG 密钥。(补充说明,使用gpg --gen-key亦可生成密钥,但是会略去自定义密钥参数的步骤,对于一般场合的使用倒也问题不大。)

输出结果的末尾大致如下:

gpg: key DC3DB5873563E6B2 marked as ultimately trusted
gpg: revocation certificate stored as '/c/Users/---/.gnupg/openpgp-revocs.d/1BA074F113915706D141348CDC3DB5873563E6B2.rev'
public and secret key created and signed.

pub   rsa2048 2019-08-04 [SC] [expires: 2021-08-03]
      1BA074F113915706D141348CDC3DB5873563E6B2
uid                      fortest <[email protected]>
sub   rsa2048 2019-08-04 [E] [expires: 2021-08-03]

需要记下的,是上述输出信息中的密钥 ID:1BA074F113915706D141348CDC3DB5873563E6B2 或者DC3DB5873563E6B2,后者是前者的简短形式。

当然,如果没有及时将其记下也不要紧,可以运行gpg --list-keys,列出本地存储的所有 GPG 密钥信息,大致如下:

$ gpg --list-keys
# some output is omitted here
pub   rsa2048 2019-08-04 [SC] [expires: 2021-08-03]
      1BA074F113915706D141348CDC3DB5873563E6B2
uid           [ultimate] fortest <[email protected]>
sub   rsa2048 2019-08-04 [E] [expires: 2021-08-03]

稍微解读一下这些结果:

  • pub其后的是该密钥的公钥特征,包括了密钥的参数(加密算法是 rsa,长度为 2048,生成于 2019-08-04,用途是 Signing 和 Certificating,一年之后过期)以及密钥的 ID。
  • uid其后的是生成密钥时所输入的个人信息。
  • sub其后的则是该密钥的子密钥特征,格式和公钥部分大致相同(E 表示用途是 Encrypting)。

关联 GPG 公钥与 Github 账户

还记得在上一步中记下的密钥 ID 吗?现在,我们需要根据这个 ID 来导出对应 GPG 密钥的公钥字符串。继续在 Git Bash 中,运行命令gpg --armor --export {key_id}:

$ gpg --armor --export 1BA074F113915706D141348CDC3DB5873563E6B2
-----BEGIN PGP PUBLIC KEY BLOCK-----

mQENBF1GT3wBCADC9Hb3HtDc69XzYlohVKvdL1KnK0FslJISRuF6S0sdoOiWo2wJ
OiYVplWguTSkrMytjnMsoysZVolkYluY1wk67NT8YuYfnu6LSuF/doihrRldnKmz
9NZWw+15MLnENKsWCtwNwcCGDeZNJACyyUMYk7nJeIiM72k3/rnsyEpHqB25W/Zf
1VBkwf/ShePZ2W+rUktJ8j1TZuxe2bQpJdHQ9EKWG50D8O3xk+N+xEg4pcXLMfwT
vnVpf2wINGLA6+3ypVMDipC0fgAnINBrrjiKsq2Sskv0O73D3sZlkOi0jgAhx+21
5dI2xHbcs3DrcZbWAF1xEA8wGsoyYQWoSCBrABEBAAG0F2ZvcnRlc3QgPHRlc3RA
dGVzdC5jb20+iQFUBBMBCAA+FiEEG6B08RORVwbRQTSM3D21hzVj5rIFAl1GT3wC
GwMFCQPCZwAFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQ3D21hzVj5rJ3BQgA
nUusNKaf8SIWq1w4ZR6CKhZP+kz+5kOEBs3+qIXJV++9nbjs4jnqOnXJUUdpLS9E
HGYnd6XSeyqWmBAuFCcmld4VGIajYxgDbF11/ql5Gnbu26/jV7hnrBBK6Xn/6oV9
bBmLoT9xget5xFC6g2VE0EvneRqacUgMBCkvrMzcVnHmpkSOpjfXRAItnyK/bhia
8k/+5URO8v7Ao2+QO0zk8XzgGc5B8H/yItzDiKe7gpzdUyCviG8m/tkDUURzloY4
09wCmQWWzerbBHJT4RdpPqdTEtC6f4jTuT32zp5NtLpJ740WmSJly/8nAJ/0x3Vf
pVkzhsg9gVHe/JSFa6/hXbkBDQRdRk98AQgAyjXZ98VOgftRThuGuYxKhqahonLf
Ihu+NuNMFG6sGGzkm2T+1i4uKyM8T/kGdcTzTXE/SMHmrCMz94FNcQ77/OFLz5HY
8hjaz5Sun7iNmz5HGct8OrsP6gQeJ5ucqm3vDZmnwU/+J+wcTosv5mgWoBVob7jb
PBnoNVBQSVhD2ek4CDljn3PdReqYfe+ee8yn+6K1t9c1HHHMco3WpdgofUABd+7l
Q1LF8IpBRDvWgdMciAPaSthIqFT6R6xLQhXV8SUm0mr2/GXbYqIptjvy1JmUwNk3
jE3LOLYulZChRdvVg3Y+xgkVlMYLy3SBQ1EaTnUUsGYbhGQnOwDwVAlxgwARAQAB
iQE8BBgBCAAmFiEEG6B08RORVwbRQTSM3D21hzVj5rIFAl1GT3wCGwwFCQPCZwAA
CgkQ3D21hzVj5rLzvAf/QzfDOrhRz9AVLiAqus3Z/WfZY81sUiewNM+YdV9aODht
q4VE92SYHeR/b72+Fl62SRbDqxw7qG5FJGByuqo6nJjHEpnFzqB/pepTVDzlwvdn
JO46tmepFAChPBpeTTjTs2CF/BG0As0KxXQCpdFw4m8UdkZ7Olt1/LKnXrFmr1BA
jp2MvmAo38j2RyPTyXKWmJW+vC8DwmOGMoHCL6fM0TeaWey3rNxST7bbxPdRVc4Z
/26k450FEW5D+VInb9NuFYSoE2UXs6DgI1OWuuGvWePrtXHeQvuNbGdEdUwU14mf
msQ78G2MjX4AAYR5iNnQ/IWDBKbOWt3ajIoJuebArw==
=oHpZ
-----END PGP PUBLIC KEY BLOCK-----

然后,在 Github 的 SSH and GPG keys 中,新增一个 GPG key,内容即是上述命令的输出结果。

再次提醒,GPG 密钥中个人信息的邮箱部分,必须使用在 Github 中验证过的邮箱,否则添加 GPG key 会提示未经验证。

利用 GPG 私钥对 Git commit 进行签名

首先,需要让 Git 知道签名所用的 GPG 密钥 ID:

git config --global user.signingkey {key_id}

然后,在每次 commit 的时候,加上-S参数,表示这次提交需要用 GPG 密钥进行签名:

git commit -S -m "..."

如果觉得每次都需要手动加上-S有些麻烦,可以设置 Git 为每次 commit 自动要求签名:

git config --global commit.gpgsign true

但不论是否需要手动加上-S,在 commit 时皆会弹出对话框,需要输入该密钥的密码,以确保是密钥拥有者本人操作,如图所示:

GPG Signing on commit

输入正确密码后,本次 commit 便被签名验证,push 到 Github 远程仓库后,即可显示出 Verified 绿色标记(由于fortest <[email protected]>密钥的邮箱未经验证,所以此处实际用的是我本人的密钥进行签名):

结果

可选步骤:信任 Github 的 GPG 密钥

事实上,在完成上述步骤后,已经可以基本完全正常地同时使用 Github 和 GPG 了,那为什么还需要这一步骤呢?很简单,不妨用git log --show-signature试试查看本地的某个 Git 仓库的 commit 记录和签名信息:

$ git log --show-signature
# some output is omitted
commit ec37d4af120a69dafa077052cfdf4f5e33fa1ef3 (HEAD -> master)
gpg: Signature made 2019 年 08 月 412:52:29
gpg:                using RSA key 1BA074F113915706D141348CDC3DB5873563E6B2
gpg: Good signature from "fortest <[email protected]>" [ultimate]
Author: keithnull <[email protected]>
Date:   Sun Aug 4 12:52:29 2019 +0800

    test GPG

commit 6937d638d950362f73bfbf28bc4a39d1700bf26b
gpg: Signature made 2019 年 07 月 2415:58:46
gpg:                using RSA key 4AEE18F83AFDEB23
gpg: Can't check signature: No public key
Author: Keith Null <20233656[email protected]>
Date:   Wed Jul 24 15:58:46 2019 +0800

    Initial commit

可以发现,虽然所有的 commit 在 Github 中查看都是 Verified,但是有一些比较特殊:在 Github 网页端进行的操作,比如创建仓库。这些 commit 并没有用我们之前生成的密钥进行签名,而是由 Github 代为签名了。这样的结果就是,我们本地无法确认这些签名的真实性。

为了解决这个问题,我们需要导入并信任 Github 所用的 GPG 密钥

先是导入:

$ curl https://github.com/web-flow.gpg | gpg --import
# curl's output is omitted
gpg: key 4AEE18F83AFDEB23: public key "GitHub (web-flow commit signing) <[email protected]>" imported
gpg: Total number processed: 1
gpg:               imported: 1

然后是信任(用自己的密钥为其签名验证,需要输入密码):

$ gpg --sign-key 4AEE18F83AFDEB23
pub  rsa2048/4AEE18F83AFDEB23
     created: 2017-08-16  expires: never       usage: SC
     trust: unknown       validity: full
[  full  ] (1). GitHub (web-flow commit signing) <[email protected]>

pub  rsa2048/4AEE18F83AFDEB23
     created: 2017-08-16  expires: never       usage: SC
     trust: unknown       validity: full
 Primary key fingerprint: 5DE3 E050 9C47 EA3C F04A  42D3 4AEE 18F8 3AFD EB23

     GitHub (web-flow commit signing) <[email protected]>

Are you sure that you want to sign this key with your
key "Keith Null <[email protected]>" (7C4BC917F7B12E8A)

Really sign? (y/N) y

至此,再尝试查看本地仓库的 commit 签名信息,则会发现所有的 commit 签名都已得到验证:

$ git log --show-signature
# some output is omitted
commit 6937d638d950362f73bfbf28bc4a39d1700bf26b
gpg: Signature made 2019 年 07 月 2415:58:46
gpg:                using RSA key 4AEE18F83AFDEB23
gpg: Good signature from "GitHub (web-flow commit signing) <[email protected]>" [full]
Author: Keith Null <20233656[email protected]>
Date:   Wed Jul 24 15:58:46 2019 +0800

    Initial commit

结束

经过这一番操作,Github 和 GPG 圆满结合在了一起,而我也得到了我想要的 Verified 标记。不过,GPG 的功能远非止于此,它还可以用来对文件、邮件等进行加密,还可以进行身份验证等等,都有待我去学习研究。