配置 GitHub SSH 远程连接与 GPG 提交签名验证

刚刚重装了操作系统,又要从头配置GitHub的SSH密钥,每次配置时都要现场搜索命令,比较麻烦,索性自己记录一下过程,方便以后使用。

关于本文中使用的操作系统、Git版本和应用场景:

  • Windows 11 IoT 企业版 LTSC 24H2 (崭新出厂)
  • git version 2.47.0.windows.1
  • 两个GitHub账号,都需要配置SSH以及GPG密钥
  • 以下所有使用的命令均在 Git Bash 中执行。
  • 命令中由尖括号< >包裹的内容需要根据个人情况进行调整,例如文件名<Filename>。调整时删掉尖括号,也就是说命令中不应该包含任何的< >
  • 请根据实际情况调整文中的文件路径、设置选项等内容。

安装 Git

崭新出厂的系统,自然是没有安装Git的,先下载一下。

安装后启动 Git Bash。

配置 SSH 密钥

先来配置用于远程连接的SSH密钥。

生成 SSH 密钥

使用Ed25519算法生成SSH密钥,将<your_email@example.com>替换为你的GitHub首选电子邮件地址。

1
ssh-keygen -t ed25519 -C "<your_email@example.com>"

此时系统会提示Enter file in which to save the key来让你选择密钥的存储位置,直接按下回车键接受默认位置/c/Users/<username>/.ssh/id_ed25519

然后会提示Enter passphrase for "/c/Users/<username>/.ssh/id_ed25519" (empty for no passphrase):以及Enter same passphrase again:来让你输入以及确认密钥的密码。可以按两次回车跳过,不设置密码。 如果设置了密码,那么每次执行git push操作时都要输入一遍,比较麻烦。

最后系统会输出密钥的位置、指纹等信息,完整的输出类似下面这种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Generating public/private ed25519 key pair.  
Enter file in which to save the key (/c/Users/<username>/.ssh/id_ed25519):
Enter passphrase for "/c/Users/<username>/.ssh/id_ed25519" (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /c/Users/<username>/.ssh/id_ed25519
Your public key has been saved in /c/Users/<username>/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:jfalkjfstsdfgy <your_email@example.com>
The key's randomart image is:
+--[ED25519 256]--+
| +*+*- |
| -*//- |
| /-/++/ |
| **/** |
| /+*/+ |
| //*+. |
| +-*/* |
| /*--/ |
| /*-** |
+----[SHA256]-----+

将 SSH 密钥添加到 GitHub 帐户

创建完SSH密钥对后,我们需要将公钥添加到我们的 GitHub 帐户中。

首先使用记事本打开公钥文件,然后复制里面的所有内容。

1
notepad ~/.ssh/id_ed25519.pub

此时在~/.ssh目录,也就是C:\Users\<username>\.ssh下,会有id_ed25519id_ed25519.pub两个文件,前者是私钥,后者是公钥。只需要将公钥添加到 GitHub 帐户中,请不要将私钥上传到网络。

然后在浏览器中登录你的GitHub账户,依次点击:页面右上角的个人资料照片 → Settings → 侧边栏中的SSH and GPG keys → 右上角的New SSH key按钮。

然后在Title中填写新密钥的称谓,以方便区分来自不同的设备的密钥。Key type保持默认的Authentication Key选项。向Key中粘贴复制的公钥。最后点击Add SSH Key

Authentication Key 与 Signing Key 的区别

引用自:ChatGPT
在 GitHub 中添加 SSH 密钥时,你会看到两种密钥类型:Authentication Key 和 Signing Key。它们的区别如下:

  • Authentication Key(认证密钥)
    • 用途:Authentication Key 是用于身份验证的密钥。它用于通过 SSH 协议安全地连接到 GitHub,从而允许你执行 git push、git pull 等操作。
    • 工作原理:当你使用 SSH URL(如 git@github.com:username/repo.git)连接到 GitHub 时,GitHub 会使用你的 SSH 公钥进行身份验证。你需要在 GitHub 上添加相应的公钥,以便在使用 SSH 时能够正确验证你的身份。
    • 使用场景:所有需要访问 GitHub 的操作,如克隆、推送和拉取代码,都需要使用 Authentication Key。
  • Signing Key(签名密钥)
    • 用途:Signing Key 是用于对提交进行数字签名的密钥。这可以确保提交是由特定的用户创建的,增加了代码的可信度。
    • 工作原理:当你进行提交时,可以使用 GPG 或 SSH 密钥来对提交进行签名。通过签名,其他开发者可以验证这次提交是否确实来自你,并且没有被篡改。
    • 使用场景:在需要验证提交来源的项目中,尤其是开源项目,使用 Signing Key 可以提供额外的安全性。

最后验证一下是否配置成功,如果是第一次进行连接,系统会提示Are you sure you want to continue connecting (yes/no/[fingerprint])?,输入yes并回车即可。

1
ssh -T git@github.com

配置成功的话系统会输出Hi <Username>! You've successfully authenticated, but GitHub does not provide shell access.

配置全局 Git 用户名与电子邮件地址

引用自:GitHub文档
可使用 git config 命令更改与 Git 提交关联的名称。 您设置的新名称将在从命令行推送到 GitHub 的任何未来提交中显示。 如果您想要将真实姓名保密,则可以使用任意文本作为您的 Git 用户名。使用 git config 更改与 Git 提交关联的名称仅影响未来的提交,而不会更改用于过去提交的名称。
可使用 git config 命令更改与 Git 提交关联的电子邮件地址。您设置的新电子邮件地址将在您从命令行推送到 GitHub 的任何将来都可见。您在更改提交电子邮件地址之前做出的任何提交仍然与您之前的电子邮件地址相关联。

设置全局用户名,将<Username>替换为你的 GitHub 用户名。

1
git config --global user.name "<Username>"

设置全局电子邮件地址,将<your_email@example.com>替换为你的GitHub首选电子邮件地址。

1
git config --global user.email "<your_email@example.com>"

相关设置项可以在C:\Users\<Username>\.gitconfig中修改。

提交签名验证

SSH 的配置已经完成了,此时你可以正常的使用git push等操作。但是!由于我们还没有对提交进行签名验证,现在进行的提交缺少灵魂(下图中的Verified小绿标)。接下来我会向我的 GitHub 帐户添加 GPG 密钥,并对我的 Git 提交进行签名验证。

验证提交签名的作用

引用自:ChatGPT
Q:在进行push时我们已经通过ssh密钥验证了身份,为何还要再次验证提交签名?
A:虽然SSH密钥验证了你的身份,确保你有权访问远程仓库,但提交签名和身份验证有不同的目的和作用:

  1. 身份验证与提交的来源
    • SSH密钥验证:当你通过SSH密钥进行push时,GitHub或GitLab验证的是你是否有权限推送代码。这是一个身份验证过程,确保你是一个合法的用户。
    • 提交签名:签名是用来验证每个具体提交的来源。它确保该提交确实是由持有相应私钥的人生成的。这意味着,即使一个恶意用户能够访问你的远程仓库(例如,获取了你的SSH密钥),他们仍然无法伪造你过去的提交。
  2. 防止篡改
    • SSH密钥验证:只是验证身份,确保你可以访问远程仓库,但不保证仓库中的每个提交都是安全的。
    • 提交签名:提供了对提交内容的完整性保证。如果提交被篡改,签名会失效,警告用户这个提交不再是可信的。
  3. 增强安全性
    • 签名是额外的安全层,可以防止内外部的恶意行为。即使SSH密钥被泄露,签名验证可以确保代码的完整性和来源。

关于使用 SSH 密钥进行签名

其实有个偷懒的办法,就是在前面将 SSH 密钥添加到 GitHub 帐户后,再重复一边操作,将密钥类型从 Authentication Key 切换为 Signing Key。这样就可以使用 SSH 密钥来进行提交签名验证,不用在单独设置 GPG 密钥,属于是一只牛马打两份工。不过 GPG 密钥用作签名验证时的功能多一丢丢,所以我还是设置了 GPG 密钥。

使用 SSH 密钥进行签名还需要单独设置一下:

  • 使用 git config --global gpg.format ssh 配置 Git 使用 SSH 对提交和标记签名。
  • 使用 git config --global user.signingkey ~/.ssh/<your_ssh_key.pub> 在 Git 中指定要使用的签名密钥。

生成 GPG 密钥

安装 Git 时会默认安装 GPG,先检查一下。

1
gpg --version

正常情况会返回版本信息,如果报错的话可能需要手动安装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
gpg (GnuPG) 2.4.5-unknown  
libgcrypt 1.9.4-unknown
Copyright (C) 2024 g10 Code GmbH
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: /c/Users/<Username>/.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

接下来生成 GPG 密钥对

1
gpg --full-generate-key

系统会引导我们配置密钥的类型:

  1. 选择加密方式,直接按回车键,选择默认的ECC,也就是椭圆曲线加密。
  2. 选择加密用的曲线,直接按回车选择默认的Curve25519曲线。
  3. 选择密钥有效期,同样直接按下回车保持默认的永不过期选项。
  4. 最后按下y并回车确认以上选择。

最后来填写密钥信息:

  1. 填写名称,这个任意填写,可以直接用GitHub的用户名。
  2. 填写电子邮件地址,必须是GitHub账户绑定的首选电子邮件地址。
  3. 填写注释,这个不重要,可以空着。
  4. O键并回车确认以上信息。
  5. 在弹出的Pinentry窗口中填写密钥的保护密码,然后再输入一遍密码来确认。以后使用该密钥对提交进行签名时需要输入密码。懒的话可以留空。

将 GPG 密钥添加到 GitHub 帐户

同SSH密钥一样,我们也需要将GPG密钥的公钥添加到 GitHub 帐户中。

首先列出系统现有的 GPG 密钥。

1
gpg --list-secret-keys --keyid-format=long

得到的结果类似于:

1
2
3
4
5
6
/c/Users/<Username>/.gnupg/pubring.kbx  
----------------------------------
sec ed25519/21231BBF246642D21 2024-10-01 [SC]
16534564331BBDF314564321231
uid [ultimate] Name (Comments) <your_email@example.com>
ssb cv25519/652153215AB215621 2024-10-01 [E]

看上去很懵逼对不对,实际上它的格式为:

1
2
3
4
sec <加密算法>/<密钥ID> <生成日期> [<功能>]
<公钥>
uid [ultimate] <密钥名称> (<注释>) <电子邮件地址>
ssb <加密算法>/<密钥ID> <生成日期> [<功能>]

密钥的功能分为:

  • S:签名 Signing
  • E:加密 Encrypt
  • C:认证 Certify

我们需要使用具备签名功能的密钥的ID,也就是后缀为[SC]的那一行中的21231BBF246642D21,或者是电子邮件地址,来使用下面的命令导出ASCII编码的公钥,并自动复制到剪切板。

1
gpg --armor --export <密钥ID或电子邮件地址> | clip

接下来在浏览器中登录你的GitHub账户,依次点击 GitHub 任意页面右上角的个人资料照片 → Settings → 侧边栏中的SSH and GPG keys → 右侧的New GPG key按钮。

Title中填写新签名密钥的称谓,方便区分来自不同的设备的密钥。在Key中粘贴签名复制的公钥。最后点击Add GPG key完成添加。

配置提交签名验证

最后我们需要配置全局GPG密钥。

1
git config --global user.signingkey <密钥ID>

另外在进行提交时,需要在命令中加入-S选项来为提交签名。如果你前面设置了密码,则需要在弹出的验证窗口中输入密码。

1
git commit -S -m "<提交信息>"

(可选)如果你觉得每次输入-S选项麻烦,可以将 Git 配置为默认对所有提交进行签名,这样提交时就不用输入-S了。

1
git config --global commit.gpgsign true

(可选)设置 GPG-Agent 缓存

如果觉得每次都输入密码很麻烦,可以设置 GPG-Agent 缓存时间,在缓存过期前进行提交签名都不需要再输入密码。

首先在~/.gnupg目录创建配置文件,并用记事本打开。

1
touch ~/.gnupg/gpg-agent.conf && notepad ~/.gnupg/gpg-agent.conf

粘贴配置,然后保存并关闭文件。

1
2
default-cache-ttl 604800
max-cache-ttl 604800
  • 后面数字的单位是秒,数值不能设置的太大,我起初设置的是一个月的密码缓存时间(2592000秒),但是没有生效,改成一个周(604800秒)后才生效。
  • default-cache-ttl n:将缓存条目有效的时间设置为 n 秒,默认时间为 600 秒。每次访问缓存条目时,都会重置条目的定时器。
  • max-cache-ttl n:将缓存条目有效的最大时间设置为 n 秒。到期之后,即使最近已访问过缓存条目,缓存条目也将过期。默认时间为 2 小时(7200 秒)。

(可选)导入并信任 GitHub 公钥

如果我们直接在GitHub网页上进行提交,提交签名验证使用的是GitHub的密钥,而不是我们自己的。而我们本地没有GitHub签名验证的公钥,就会在相关提交中显示gpg: Can't check signature: No public key

可以切换到一个Git仓库目录,使用下面的命令查看。

1
git log --show-signature

输出结果如下所示,可以看到GitHub密钥的ID是B5690EEEBB952194,被标红了。

这个红色对强迫症来说很难受,所以我选择在本地导入并信任 GitHub 公钥来消除这个红框框。

首先在本地导入GitHub的公钥。

1
curl https://github.com/web-flow.gpg | gpg --import

然后查看现有的密钥。

1
gpg --list-keys --keyid-format=long

输出内容如下,第一个是自己的GPG密钥,下面两个是GitHub的(甚至还有一个过期的哈哈哈哈)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ gpg --list-keys --keyid-format=long  
/c/Users/Sirius/.gnupg/pubring.kbx
----------------------------------
pub ed25519/21231BBF246642D21 2024-10-01 [SC]
16534564331BBDF314564321231
uid [ultimate] Name (Comments) <your_email@example.com>
sub cv25519/652153215AB215621 2024-10-01 [E]

pub rsa2048/4AEE18F83AFDEB23 2017-08-16 [SC] [expired: 2024-01-16]
5DE3E0509C47EA3CF04A42D34AEE18F83AFDEB23
uid [ expired] GitHub (web-flow commit signing) <noreply@github.com>

pub rsa4096/B5690EEEBB952194 2024-01-16 [SC]
968479A1AFF927E37D1A566BB5690EEEBB952194
uid [ unknown] GitHub <noreply@github.com>

然后编辑GitHub的公钥。

1
gpg --edit-key B5690EEEBB952194

信任该密钥。

1
trust

然后会提示你选择信任等级,输入4并回车。

最后保存更改。

1
save

完整输出如下:

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
Sirius@Pikachu MINGW64 ~/.gnupg  
$ gpg --edit-key B5690EEEBB952194
gpg (GnuPG) 2.4.5-unknown; Copyright (C) 2024 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.


pub rsa4096/B5690EEEBB952194
created: 2024-01-16 expires: never usage: SC
trust: unknown validity: unknown
[ unknown] (1). GitHub <noreply@github.com>

gpg> trust
pub rsa4096/B5690EEEBB952194
created: 2024-01-16 expires: never usage: SC
trust: unknown validity: unknown
[ unknown] (1). GitHub <noreply@github.com>

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? 4

pub rsa4096/B5690EEEBB952194
created: 2024-01-16 expires: never usage: SC
trust: full validity: unknown
[ unknown] (1). GitHub <noreply@github.com>
Please note that the shown key validity is not necessarily correct
unless you restart the program.

gpg> save
Key not changed so no update needed.

如果日后这个密钥也到期了,就删掉它,再重新导入最新的。

1
gpg --delete-key B5690EEEBB952194

(可选)VS Code 配置

如果直接在VS Code中使用Git并进行签名验证,需要在VS Code中使用ctrl+,组合键打开设置,搜索git.enableCommitSigning,勾选允许使用 GPG、X.509 或 SSH 提交签名

多账户配置

如果你只需要设置一个账号,那么到这里所有的配置就都已经结束了。接下来我会继续配置第二个账号的SSH与GPG密钥,与第二账号相关的文件/路径/后缀我都命名为second,请根据个人喜好进行修改。

配置 SSH 密钥

生成第二个 SSH 密钥

先来生成第二个 SSH 密钥:

  • <your_email@example.com>替换为第二个账号的首选电子邮件地址。
  • <username>替换为你的Windows账户名称,也就是用户文件夹的名称。
  • <id_ed25519_second>替换为第二个密钥的文件名。
    1
    ssh-keygen -t ed25519 -C "<your_email@example.com>" -f "/c/Users/<username>/.ssh/<id_ed25519_second>"

因为我们在命令中指定了密钥的文件路径,所以系统只会提示输入和确认密码,同样按两次回车跳过。

创建 SSH 配置文件

我们可以创建一个config配置文件,使用不同的 Host 区分不同账户的 Git 操作。

1
touch ~/.ssh/config && notepad ~/.ssh/config

配置文件的格式为:

1
2
3
4
Host <别名>
HostName <实际 Host>
User <用户名>
IdentityFile <私钥路径>

粘贴下面的内容,并结合实际情况调整的Host别名和密钥文件路径,然后保存并关闭文件。

1
2
3
4
5
6
7
8
9
10
11
# Default config for main account
Host github.com
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519

# Config for second account
Host github.com-second
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519_second

这两个配置块的含义是:

  • 对使用 @github.com-second 的 Git URL 使用 id_ed25519_second 密钥。
  • 对其他的所有 Git URL 使用 id_ed25519 密钥。

将 SSH 密钥添加到 GitHub 帐户

将 SSH 密钥添加到第二个 GitHub 帐户的步骤和第一个账户的配置基本相同,只要将命令中第一个密钥的名称替换为第二个的即可。

验证是否配置成功的命令调整为:

1
ssh -T git@github.com-second

提交签名验证

生成第二个GPG密钥并将它添加到第二个GitHub账号的步骤和第一个完全一样,按前面的内容操作即可,注意不要再进行全局设置

Git 命令调整

对于第二个账号,在使用git clone等命令时需要将默认的@github.com替换为前面配置的Host 别名,即@github.com-second,例如:

1
git clone git@github.com-second:Username/Repo.git

多账户区分

我在前面已经将第一个账号的配置作为全局设置使用,为了避免两个账户混淆,我们需要将它们的Git配置区分开来,以下是我找到的两种设置方法。

我仍使用second作为实例中的用户名/文件名,请注意将它修改为你自己的第二账户名。

批处理快速配置

这种方法是对第二个账号的每个Git仓库进行本地Git设置。借助本地设置更高的优先级将仓库的所有账户区分开。

但是,每次配置时都要输入多条命令,显得太麻烦,不如把这些命令添加到批处理文件中,然后将批处理文件添加到系统环境变量中,这样每次只要执行一遍批处理文件就可以完成配置。

首先在用户目录下创建 ~/.gitswitch/second.bat文件并用记事本打开。

1
mkdir -p ~/.gitswitch/ && touch ~/.gitswitch/second.bat && notepad ~/.gitswitch/second.bat

然后粘贴并编辑下面的内容,保存并关闭文件。

1
2
3
4
git config --local user.name <第二账户用户名>
git config --local user.email <第二账户用户名电子邮件地址>
git config --local user.signingkey <第二账户 GPG 密钥 secret ID>
git config --local commit.gpgsign true

接下来在具有管理员权限的终端中运行下面的指令,打开系统环境变量设置。

1
rundll32.exe sysdm.cpl,EditEnvironmentVariables

将 C:\Users\<Username>\.gitswitch添加到系统环境变量,注意将<Username>替换为你的Windows用户名

  1. 双击打开系统变量下的Path
  2. 点击右上角的新建
  3. 粘贴路径
  4. 确定保存
  5. 重启终端使更改生效

之后如果需要在某个仓库进行第二账户的本地配置,直接输入并执行脚本即可。

1
second.bat

条件设置

此类方法使用了2.13版本新增的条件设置,可以结合条件语句和通配符实现更加灵活的区分。

在配置条件设置前,先在用户目录下创建第二个账号的Git配置文件,并使用记事本打开:

1
mkdir -p ~/.gitswitch/second/ && touch ~/.gitswitch/second/.gitconfig && notepad ~/.gitswitch/second/.gitconfig

然后粘贴并修改下面的配置,保存并关闭记事本。

1
2
3
4
5
6
[user]
name = <第二账户用户名>
email = <第二账户用户名电子邮件地址>
signingkey = <第二账户 GPG 密钥 secret ID>
[commit]
gpgsign = true

最后在全局Git配置文件~/.gitconfig的末尾添加条件,我总结了两种常用的条件:路径匹配和配置项URL匹配,可以叠加使用。

请注意:条件设置需要放在全局设置的下方!

路径匹配

我们可以为第二个账号的Git仓库指定一个父目录,然后让父目录下所有的Git仓库都使用同一个指定的配置,而不在这个目录下的其他Git仓库会默认使用全局配置,这样就不需要再为第二个账号的每个Git仓库都进行单独配置。

在全局Git配置文件~/.gitconfig的末尾添加下面的配置,Windows系统下必须加上/i来关闭大小写敏感,否则会因为路径问题导致配置不生效。

1
2
[includeIf "gitdir/i:D:/Project/Second/"]
path = ~/.gitswitch/second/.gitconfig

这样在D:/Project/Second/路径下存放的所有项目都会自动使用~/.gitswitch/second/.gitconfig中的配置。而其它路径下的项目会保持原有的全局配置。

配置项URL匹配

这个方法需要Git版本高于2.36,是通过判断使用的远程URL来设置不同的Git配置。这个方法同样需要为第二个账号创建独立的配置文件,方法同上,然后在全局配置文件中加入以下内容:

1
2
[includeIf "hasconfig:remote.*.url:git@github.com-second:*/**"]
path = ~/.gitswitch/second/.gitconfig

这行配置的意思是,如果某个项目的本地配置中,remote.*.url项对应的值是git@github.com-second:Username/Repo.git,则使用指定的配置文件~/.gitswitch/second/.gitconfig。这个方法可以和之前设置的 SSH Host 无缝衔接,非常方便。

完整的全局Git配置文件~/.gitconfig如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
[user]
name = <主账户用户名>
email = <主账户电子邮件地址>
signingkey = <主账户 GPG 密钥 secret ID>
[commit]
gpgsign = true

[includeIf "gitdir/i:D:/Project/Second/"]
path = ~/.gitswitch/second/.gitconfig

[includeIf "hasconfig:remote.*.url:git@github.com-second:*/**"]
path = ~/.gitswitch/second/.gitconfig

参考与引用: