为了避免每次远程ssh连接的时候输入用户名和密码,我们会用ssh-keygen生成的RSA公私密钥对来自动化ssh登陆认证的过程。GnuPG可以生成和管理密钥,于是我就想把用于ssh登陆的RSA密钥也托管给它。这样以来,所有日常工作用到的密钥都由GnuPG集中统一管理,同时也方便了备份。这篇文章完整介绍如何用GnuPG配置一个可以用于ssh登陆认证的RSA密钥,以及配置过程中需要注意的那些坑。
当我们讨论PGP密钥的时候,很多时候指的是一组密钥,而不是一个密钥。PGP密钥分为主密钥和子密钥,通常的配置方式是一个主密钥外加多个子密钥,每个子密钥被分配不同功能,且都用主密钥签过名。这样做的好处是可以最大程度保护主密钥,因为几乎所有日常操作都通过子密钥完成,主密钥的私钥可以离线保存。另外如果某个子密钥被破坏了,我们可以很方便地用主密钥去撤销该子密钥,同时主密钥的信任度不受影响。关于PGP子密钥,非常推荐花时间读下Using OpenPGP subkeys in Debian development这篇文章,里面介绍了子密钥概念以及配置PGP密钥的最佳实践。
我的开发环境是MacOS 10.15.5 + gpg (GnuPG) 2.2.21,配置开始之前假设还没有任何PGP密钥。
我们可以简单运行gpg --gen-key来生成一组新密钥,gpg会默认生成一个开启了Sign和Certify功能的主密钥以及一个Encrypt功能的子密钥(这里说的“一个”指一个公私密钥对)。为了了解所有细节,我们进入专家模式手动配置所有参数。
1)先建一个只有Certify功能的主密钥。
$ gpg --expert --full-gen-key
gpg (GnuPG) 2.2.21; Copyright (C) 2020 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.
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)
(7) DSA (set your own capabilities)
(8) RSA (set your own capabilities)
(9) ECC and ECC
(10) ECC (sign only)
(11) ECC (set your own capabilities)
(13) Existing key
(14) Existing key from card
Your selection? 8
Possible actions for a RSA key: Sign Certify Encrypt Authenticate
Current allowed actions: Sign Certify Encrypt
(S) Toggle the sign capability
(E) Toggle the encrypt capability
(A) Toggle the authenticate capability
(Q) Finished
Your selection? s
Possible actions for a RSA key: Sign Certify Encrypt Authenticate
Current allowed actions: Certify Encrypt
(S) Toggle the sign capability
(E) Toggle the encrypt capability
(A) Toggle the authenticate capability
(Q) Finished
Your selection? e
Possible actions for a RSA key: Sign Certify Encrypt Authenticate
Current allowed actions: Certify
(S) Toggle the sign capability
(E) Toggle the encrypt capability
(A) Toggle the authenticate capability
(Q) Finished
Your selection? q
RSA keys may be between 1024 and 4096 bits long.
What keysize do you want? (2048) 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: [your name]
Email address: [your email]
Comment:
Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O
Certify和Sign的区别在于,Certify是给其它密钥签名,其它密钥可以是自己的其它子密钥或者他人的主密钥;Sign指的是给数据签名。生成主密钥后,gpg会自动在~/.gnupg/openpgp-revocs.d目录下为我们创建用于撤销主密钥的证书。
2)用刚创建的主密钥生成一个只有Authenticate功能的子密钥
$ gpg --list-keys --keyid-format long
/Users/xxxxxxxxxx/.gnupg/pubring.kbx
------------------------------------
pub rsa4096/0291021E5F56BD92 2020-07-12 [C]
B782E151823E6A19A756C55A0291021E5F56BD92
uid [ultimate] yyyyyyyyyy
$ gpg --expert --edit-key 0291021E5F56BD92
gpg (GnuPG) 2.2.21; Copyright (C) 2020 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.
Secret key is available.
sec rsa4096/0291021E5F56BD92
created: 2020-07-12 expires: never usage: C
trust: ultimate validity: ultimate
[ultimate] (1). yyyyyyyyyy
gpg> addkey
Please select what kind of key you want:
(3) DSA (sign only)
(4) RSA (sign only)
(5) Elgamal (encrypt only)
(6) RSA (encrypt only)
(7) DSA (set your own capabilities)
(8) RSA (set your own capabilities)
(10) ECC (sign only)
(11) ECC (set your own capabilities)
(12) ECC (encrypt only)
(13) Existing key
(14) Existing key from card
Your selection? 8
Possible actions for a RSA key: Sign Encrypt Authenticate
Current allowed actions: Sign Encrypt
(S) Toggle the sign capability
(E) Toggle the encrypt capability
(A) Toggle the authenticate capability
(Q) Finished
Your selection? s
Possible actions for a RSA key: Sign Encrypt Authenticate
Current allowed actions: Encrypt
(S) Toggle the sign capability
(E) Toggle the encrypt capability
(A) Toggle the authenticate capability
(Q) Finished
Your selection? e
Possible actions for a RSA key: Sign Encrypt Authenticate
Current allowed actions:
(S) Toggle the sign capability
(E) Toggle the encrypt capability
(A) Toggle the authenticate capability
(Q) Finished
Your selection? a
Possible actions for a RSA key: Sign Encrypt Authenticate
Current allowed actions: Authenticate
(S) Toggle the sign capability
(E) Toggle the encrypt capability
(A) Toggle the authenticate capability
(Q) Finished
Your selection? q
RSA keys may be between 1024 and 4096 bits long.
What keysize do you want? (2048) 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
Really create? (y/N) y
gpg> save
编辑密钥的时候需要提供主密钥的ID,密钥ID可以通过运行gpg --list-keys --keyid-format long找到。这里有几个关于密钥ID的概念:Short Key ID,Long Key ID,Fingerprint以及Keygrip。Stackflow上的最高票回答详细解释了前三者的区别,这里补充一下Keygrip,待会需要用到。Keygrip是密钥的内部id,比如gpg-agent就是通过Keygrip来唯一标识密钥的。但是在运行gpg命令的时候,我们是通过Short Key ID,Long Key ID或者Fingerprint这些外部ID来指定目标密钥的。默认情况下gpg只显示主密钥的Fingerprint,--keyid-format long开启所有主子密钥Long Key ID的显示。
3)为gpg-agent开启ssh支持
$ echo 'enable-ssh-support' >> ~/.gnupg/gpg-agent.conf
4)让gpg-agent介入ssh登陆认证
$ echo 'export SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket)' >> ~/.bashrc
这样以来gpg-agent可以通过socket和ssh-agent交换信息。
5)指定ssh登陆所使用的RSA密钥
$ gpg --list-secret-keys --with-keygrip
/Users/xxxxxxxxx/.gnupg/pubring.kbx
------------------------------------
sec rsa4096 2020-07-12 [C]
B782E151823E6A19A756C55A0291021E5F56BD92
Keygrip = AC85B97781F550C9EB3C124F81A50FAD8BEDE215
uid [ultimate] yyyyyyyyyy
ssb rsa4096 2020-07-12 [A]
Keygrip = A3D71EEA4B5297F4D066AFAAD0AF4A61D6850C9A
$ echo 'A3D71EEA4B5297F4D066AFAAD0AF4A61D6850C9A' > ~/.gnupg/sshcontrol
将步骤2)中生成的Authenticate子密钥的Keygrip写入sshcontrol文件,这样不用每次都手动去指定使用哪个密钥来登陆。注意这里写入文件的是子密钥的Keygrip而不是Fingerprint。
6)打开新shell,重启gpg-agent
$ gpgconf --kill gpg-agent
$ gpg-agent --daemon
至此GnuPG部分的配置就完成了。
7)远端配置RSA公钥
上面所有步骤完成后运行ssh-add -L可以看到登陆所使用的RSA公钥,但是这个公钥不能直接复制粘贴到远端的authorized_keys中,这里我们需要手动从gpg中导出符合OpenSSH格式的公钥(对比ssh-add -L和export的结果发现公钥数据只是备注有区别,这里不大明白为什么前者不行)。
$ gpg --list-keys --keyid-format long
/Users/xxxxxxxxx/.gnupg/pubring.kbx
------------------------------------
pub rsa4096/0291021E5F56BD92 2020-07-12 [C]
B782E151823E6A19A756C55A0291021E5F56BD92
uid [ultimate] yyyyyyyyyy
sub rsa4096/54FD729941AC0B0A 2020-07-12 [A]
$ gpg --export-ssh-key 54FD729941AC0B0A | ssh -i ~/.ssh/id_rsa username@host '>> ~/.ssh/authorized_keys'
其中54FD729941AC0B0A是步骤2)生成的Authenticate子密钥的Long ID。
大功告成,以后ssh登陆远端服务器的时候会透明地使用gpg管理的私钥,测试没问题的话可以放心把原来ssh-keygen生成的密钥对删除。
如果只关心如何用GnuPG配置用于ssh连接的密钥,可以忽略本文之后部分。下面是关于如何用GnuPG管理和使用密钥的最佳实践。
我们目前只创建了用于Authenticate的子密钥,实际使用中一般还需要另外两个子密钥,分别用于Encrypt和Sign。具体创建过程类似上文步骤2),在此不再赘述。最终完整的Keyring包括一个用于Certify的主密钥和三个分管Authenticate、Encrypt和Sign的子密钥。配置完成后,我们分别导出私钥和公钥进行备份。
$ gpg --export-secret-keys --armor 0291021E5F56BD92 > key.ssb
$ gpg --export --armor 0291021E5F56BD92 > key.sub
以后需要恢复的时候运行gpg --import keyfile即可重新导入密钥。为了提高主密钥私钥的安全性,我们可以将它离线存储然后彻底从本地删除,毕竟平时几乎不会用到它。
$ shred --remove ~/.gnupg/private-keys-v1.d/AC85B97781F550C9EB3C124F81A50FAD8BEDE215.key
$ gpg --list-secret-keys --with-keygrip
/Users/xxxxxxxxxx/.gnupg/pubring.kbx
------------------------------------
sec# rsa4096 2020-07-12 [C]
B782E151823E6A19A756C55A0291021E5F56BD92
Keygrip = AC85B97781F550C9EB3C124F81A50FAD8BEDE215
uid [ultimate] yyyyyyyyyy
ssb rsa4096 2020-07-12 [E]
Keygrip = EE8ABF2C0543C0C60E55A43727342B32BD15C7F4
ssb rsa4096 2020-07-12 [S]
Keygrip = 315EF4524B8EA2DD1C8A1E7DFA179FC6A77829E8
ssb rsa4096 2020-07-12 [A]
Keygrip = A3D71EEA4B5297F4D066AFAAD0AF4A61D6850C9A
文件名中的hex字符串是主密钥的Keygrip。主密钥的私钥从本地删除后,可以发现sec后面多出了一个‘#’符号,表示已经删除了。
接下来我们需要把主密钥的公钥分享出去,这样方便对方验证我们的签名或者向我们发送加密数据。除了线下交换公钥之外,我们还可以把公钥发布到服务器上。
$ gpg —send-keys B782E151823E6A19A756C55A0291021E5F56BD92
gpg: sending key 0291021E5F56BD92 to hkps://hkps.pool.sks-keyservers.net
$ gpg —search-keys B782E151823E6A19A756C55A0291021E5F56BD92
gpg: data source: [https://209.244.105.201:443](https://209.244.105.201/)
(1) xxxxxxxxxx(mailto:yyyyyyyyyy)
4096 bit RSA key 0291021E5F56BD92, created: 2020-07-12
Keys 1-1 of 1 for “B782E151823E6A19A756C55A0291021E5F56BD92”. Enter number(s), N)ext, or Q)uit
公钥发布之后任何人都可以运行gpg --recv-keys keyid来导入我们的公钥。要注意的是公钥服务器遍布全球各地,相互间同步数据延迟特别大,所以如果对方在我们上传公钥后立马导入,gpg极有可能报错说找不到数据。这时候要么我们耐心等待同步完成,要么重新发布密钥通过--keyserver选项指定服务器IP,对方再从同一服务器下载我们的公钥。
导入公钥后,对方可以用它来检查签名:
# Alice: sign the data
$ gpg --sign file
# Bob: verify the signature of Alice
$ gpg --verify file.gpg
这里我们签名的时候并没有显式指定用哪个私钥。当有多个Sign功能的密钥的时候,gpg会默认优先选择最近创建的那个。
对方也可以给我们发送加密数据:
# Alice: encrypt the data
$ echo 'hello, world!' > greetings
$ gpg -r bob@email.com --encrypt greetings
# Bob: decrypt the data
$ gpg --decrypt greetings.gpg
gpg: encrypted with 4096-bit RSA key, ID 8A0F1E53243E8B63, created 2020-07-12
...
hello, world!
另外,我们可以很方便地配置Git使得所有commit都附带自己的签名。GitHub也支持账户绑定PGP密钥,签过名的commit页面上会多出一个verified的绿色标签。
这样一来,无论是给commit签名,加密解密数据还是远程ssh登陆,所有的密钥都由GnuPG集中管理,方便很多。