The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

如何使用 GPG (GnuPG) 對 Git Commit 與 Tag 進行簽章

最近將 Microsoft Store 上的 Ubuntu 20.04 LTS 安裝起來,這是 WSL (Windows Subsystem for Linux) 的執行環境,我在設定的過程中,原本想把我常用來對 Git 簽章的 GPG Key 匯入,但卻怎樣也無法 commit 新版本,花了我好些時間才理解背後的原理,索性就把我所知道的 GPG 知識與經驗分享出來吧!

簡介 GPG

GPG 全名為 The GNU Privacy Guard,又稱 GnuPG,是一套 OpenPGP 標準規格 RFC4880 的開源實作。你可以用 GnuPG 來對訊息或檔案進行加密(encrypt)或簽章(sign)。GPG 是一組 CLI 命令列工具,可以很容易的跟其他應用程式整合,例如 Git 或 Outlook 就經常使用,也支援 S/MIME 與 Secure Shell (ssh) 等應用。

在 Linux 或 macOS 作業系統底下,幾乎都已經內建 GPG 工具。但是很有可能是 gpg1 的舊版,你可以用 gpg --version 查詢目前的 gpg (GnuPG) 版本是否在 2.x 以上。

如果你要在 Windows 使用的話,安裝 Gpg4win 即可,這套 Windows 實作不僅僅只有 CLI 工具,還有一個 GUI 介面的金鑰管理員(Kleopatra),以及 Outlook 外掛程式,你可以用來收發 PGP/MIME 郵件。

如果你要在 Windows 使用的話,現在已經不用先安裝 Gpg4win 工具就可以順利使用,只是你可能要特別設定 PATH 環境變數,才能方便日後使用 gpg 命令列工具:

Gpg4win 還附有一個 GUI 介面的 金鑰管理員(Kleopatra) 與 Outlook 外掛程式,你可以用來收發 PGP/MIME 郵件,需要的人還是可以自行安裝。

  • Windows PowerShell

    $env:Path += ";C:\Program Files\Git\usr\bin"
    gpg --version
    
  • 使用 Windows PowerShell 寫入到「使用者環境變數」中

    $UserEnvPath = [Environment]::GetEnvironmentVariable("PATH", "User")
    $UserEnvPath += ";C:\Program Files\Git\usr\bin"
    [Environment]::SetEnvironmentVariable("PATH", $UserEnvPath, "User")
    
  • 使用 Command Prompt (命令提示字元) 寫入到「使用者環境變數」中 (建議使用 PowerShell 進行設定)

    SETX PATH "%PATH%;C:\Program Files\Git\usr\bin"
    

建立你的第一個 GPG 金鑰組(key-pair)

請使用 gpg --full-generate-key 建立全新的金鑰組,先擁有一個金鑰組才能對訊息進行數位簽章(Digital Signature)。建立的過程中,若遇到需要選擇的地方,採用預設值即可:

$ gpg --full-generate-key
gpg (GnuPG) 2.2.4; Copyright (C) 2017 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)
Your selection?
RSA keys may be between 1024 and 4096 bits long.
What keysize do you want? (3072)
Requested keysize is 3072 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)
Key does not expire at all
Is this correct? (y/N)
Key is valid for? (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: Will Huang
Email address: will@example.com
Comment:
You selected this USER-ID:
    "Will Huang <will@example.com>"

Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
gpg: key C22DCF85C7B99245 marked as ultimately trusted
gpg: directory '/home/will/.gnupg/openpgp-revocs.d' created
gpg: revocation certificate stored as '/home/will/.gnupg/openpgp-revocs.d/138711BA41E360645F37E23CC22DCF85C7B99245.rev'
public and secret key created and signed.

pub   rsa3072 2020-05-04 [SC]
      138711BA41E360645F37E23CC22DCF85C7B99245
uid                      Will Huang <will@example.com>
sub   rsa3072 2020-05-04 [E]

從產生金鑰的訊息中你會發現有以下這段:

gpg: key C22DCF85C7B99245 marked as ultimately trusted

只要是從你的電腦建立的金鑰組,預設都會是 絕對信任 的金鑰。因為 GPG 對儲存於本機的金鑰有區分不同的信任層級,當你使用 git log --show-signature 列出所有 Git 紀錄時,就會明確的顯示哪些簽章的紀錄不可信任的!相對的,若要信任特定人所簽署的訊息,就要匯入此人的公開金鑰,並調整信任層級才行。

測試 gpg 的訊息簽章功能

建立 GPG 金鑰組之後,就可以開始對訊息進行簽章,未來我們要對 Git 每個 commit 進行簽章,也是一樣的,只是使用的命令不太一樣而已。

請用以下命令測試 gpg 可否正常使用:

echo "test" | gpg --clearsign

如果你在 Windows 已經安裝 Gpg4win 的話,在簽章時會自動跳出一個對話視窗,讓你輸入金鑰密碼,輸入正確就會將訊息(test)進行簽章,並輸出類似以下的內容:

❯ echo "test" | gpg --clearsign
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

test
-----BEGIN PGP SIGNATURE-----

iQEzBAEBCAAdFiEEOwJL70ey3tPRY7dSHxvYbXAYfsEFAmCOLPYACgkQHxvYbXAY
fsHg3wf+LrMkuDryk09yWk+176qh0SPq22DImm/vtOBwClQICUVGRbcgaontY1lo
nnZQeV7p5fpzo5uRNQrfs4A5HiB+SxV3WO/XEtHKteEVo2MdekCDojLq4M4cmP5I
KOM1oN2vNGwVnqMpJnLn45+Fzokc+0jAAy7X0dxX6LVSodyrxFyj52d4u7DZBYau
iTjVVqW9HpOqR8Mehze0Tod4a5Hf8KRrzsjJuAJ8TarvgucCMhYO2ijjtpeJ3gtF
DEKuBPqUSgLDoSv0y3rAfE22ULyj0EmsNws7/0IICDgq+XdW3ygizSiOgXXj7jUx
LEok0lKLoue1O9HuNanShJrvO4GiGg==
=18gO
-----END PGP SIGNATURE-----

不過,如果是在 Linux (包含 WSL2 環境) 或 macOS 的 Shell 環境下,就可能會出現以下錯誤:

will@DESKTOP-K3PI2H2:~$ echo "test" | gpg --clearsign
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512

test
gpg: signing failed: Inappropriate ioctl for device
gpg: [stdin]: clear-sign failed: Inappropriate ioctl for device

或是這個錯誤:

error: gpg failed to sign the data
fatal: failed to write commit object

要能成功對訊息進行簽章,你必須事先設定好 GPG_TTY 環境變數,才能正常使用。由於每次登入都需要這個環境變數,所以建議將這段命令加入到你 Shell 的啟動檔之中。

  • 使用 Bash 的人,請將以下命令加入到你的 ~/.profile 檔案中

    export GPG_TTY=$(tty)
    
  • 使用 Zsh 的人,請將以下命令加入到你的 ~/.zprofile 檔案中

    export GPG_TTY=$(tty)
    
  • 使用 Fish 的人,請將以下命令加入到你的 ~/.config/fish/config.fish 檔案中

    set -x GPG_TTY (tty)
    

🔥 這個步驟非常重要,而且只需要設定一次,你可以透過 man gpg-agent 得知這個注意事項,初學者蠻容易忽略的。我今天在設定 WSL 的時候,就是遺漏了這個步驟,導致鬼打牆好久!

少數 macOS 用戶可能還是會遇到上述設定還是無法成功對訊息簽章的狀況,那麼建議你加裝 pinentry-mac 工具,其設定步驟如下:

brew upgrade gnupg  # 這個步驟可能會花些時間
brew link --overwrite gnupg
brew install pinentry-mac
echo "pinentry-program /usr/local/bin/pinentry-mac" >> ~/.gnupg/gpg-agent.conf
killall gpg-agent

列出目前安裝於本機的金鑰組

你可以從本機列出目前已有的金鑰資訊,金鑰區分「公開金鑰」與「私密金鑰」兩種。公開金鑰是可以在網路上流傳的版本,用來驗證特定訊息是否由同一個人所簽章過。私密金鑰是你必須安全保存的版本,用來對訊息簽章加密之用。

  • 列出公開金鑰資訊

    gpg --list-keys
    
    gpg --list-keys --keyid-format SHORT
    
    gpg --list-keys --keyid-format LONG
    
  • 列出私密金鑰資訊

    gpg --list-secret-keys
    
    gpg --list-secret-keys --keyid-format SHORT
    
    gpg --list-secret-keys --keyid-format LONG
    

如果我們要在 git commit 的時候,對 log 訊息進行 GPG 簽章,那麼你必須先列出私密金鑰資訊,並取得金鑰ID:

$ gpg --list-secret-keys --keyid-format SHORT
gpg: checking the trustdb
gpg: marginals needed: 3  completes needed: 1  trust model: pgp
gpg: depth: 0  valid:   1  signed:   0  trust: 0-, 0q, 0n, 0m, 0f, 1u
/home/will/.gnupg/pubring.kbx
-----------------------------
sec   rsa3072/C7B99245 2020-05-04 [SC]
      138711BA41E360645F37E23CC22DCF85C7B99245
uid         [ultimate] Will Huang <will@example.com>
ssb   rsa3072/FFF9AFD5 2020-05-04 [E]

從上述訊息可發現 sec 就是 secret (私密金鑰) 的意思,而 138711BA41E360645F37E23CC22DCF85C7B99245 是完整的金鑰ID,你可以用簡短的金鑰ID替代,也就是 C7B99245 這段。

設定 Git 使用 GPG 金鑰進行簽章

據我所知,只有 Windows 平台上的 Git 需要特別設定 gpg.program 參數,明確指定 gpg.exe 執行檔的所在路徑,請用以下命令進行設定:

git config --global gpg.program "C:\Program Files\Git\usr\bin\gpg.exe"
  • 執行 git commit 時使用 GPG 簽章 (-S)

    git commit -m "OK" -SC7B99245
    

    注意:這裡的 C7B99245 是你的 金鑰ID (短名)

  • 指定預設 GPG 金鑰

    git config --global user.signingkey C7B99245
    git commit -m "OK" -S
    
  • 強制使用 GPG 簽章

    git config --global commit.gpgsign true
    git commit -m "OK"
    

上述動作的背後,都會使用 gpg 工具進行簽章,預設該程式會自動啟動一個 gpg-agent 程式在背景執行,下次再進行簽章時,就不用再次輸入密碼。你可以透過 gpgconf --kill gpg-agent 命令,強制將該程式停用。

顯示 Git 簽章資訊

你可以用 git log --show-signature 命令顯示每個 commit 物件的簽章資訊,以下是「受信任」的 Log 訊息:

$ git log --show-signature
commit 7fbc730a8ca7d2b7f05873b4f79930d0cfc460b4 (HEAD -> master)
gpg: Signature made Mon May  4 16:10:51 2020 CST
gpg:                using RSA key 138711BA41E360645F37E23CC22DCF85C7B99245
gpg: Good signature from "Will Huang <will@example.com>" [ultimate]
Author: Will <will@example.com>
Date:   Mon May 4 16:10:51 2020 +0800

    OK

以下則是一個「不受信任」的 Log 訊息,但這依然是一個 好的簽章 (Good signature):

$ git log --show-signature
commit a006114a49be7bce37f00b09b9dcee2d4947c9de (HEAD -> master)
gpg: Signature made Mon May  4 17:59:50 2020 CST
gpg:                using RSA key 138711BA41E360645F37E23CC22DCF85C7B99245
gpg: Good signature from "Will Huang <will@example.com>" [unknown]
gpg: WARNING: This key is not certified with a trusted signature!
gpg:          There is no indication that the signature belongs to the owner.
Primary key fingerprint: 1387 11BA 41E3 6064 5F37  E23C C22D CF85 C7B9 9245
Author: Will <doggy.huang@gmail.com>
Date:   Mon May 4 15:09:25 2020 +0800

    OK

上述 WARNING: This key is not certified with a trusted signature! 訊息明確的指出,這個金鑰並沒有被認證為信任的簽章,你只要透過 --edit-key 參數,即可修改金鑰的信任層級,本文稍後就會提到如何修改。

匯出 GPG 私鑰

當你想重灌電腦的時候,就需要將你的私鑰備份起來,以下是備份步驟:

gpg --export-secret-keys C7B99245 > willexample.key

匯出純文字版本的私密金鑰,只要加上 --armor 參數:

gpg --export-secret-keys C7B99245 --armor > willexample.pem

匯出金鑰時會需要你輸入金鑰的密碼。

匯入 GPG 私鑰 (公鑰也會一併匯入)

gpg --import willexample.key

匯入金鑰時也會需要你輸入金鑰的密碼。

匯出 GPG 公鑰

由於 GPG 公鑰本來就是應該要公開的,許多人會將公鑰放在自己的個人網站上,或是直接放在 GitHub 給其他人下載。

匯出公鑰的方式如下:

gpg --export C7B99245 > willexample.gpg

匯出純文字版本的金鑰,只要加上 --armor 參數:

gpg --armor --export C7B99245 > willexample.gpg

其實你在註冊 GitHub 的時候,他們預設就已經幫你建立了一組 GPG 金鑰,其公鑰也會直接公布在網路上。以我的 GitHub 帳號為例 doggy8088,其公鑰的所在網址就是 https://github.com/doggy8088.gpg

匯入其他人的 GPG 公鑰

匯入別人的公鑰,可以直接透過以下命令匯入:

gpg --import willexample.gpg

如果要匯入我在 GitHub 的公鑰,則只要輸入以下命令:

curl -s -o doggy8088.gpg https://github.com/doggy8088.gpg
gpg --import doggy8088.gpg

匯入金鑰之後,執行 gpg --list-keys 命令會看到一個 [ unknown] 字樣,這意味著這個金鑰的信任層級未知

$ gpg --list-keys --keyid-format SHORT
/home/will/.gnupg/pubring.kbx
-----------------------------
pub   rsa3072/C7B99245 2020-05-04 [SC]
      138711BA41E360645F37E23CC22DCF85C7B99245
uid         [ultimate] Will Huang <will@example.com>
sub   rsa3072/FFF9AFD5 2020-05-04 [E]

你可以透過以下命令,將該金鑰設定為「絕對信任」層級:

  • Linux/macOS/WSL

    echo -e "trust\n5\ny\n" |  gpg --command-fd 0 --expert --edit-key C7B99245
    
  • Windows 命令提示字元

    (echo trust &echo 5 &echo y &echo quit) | gpg --command-fd 0 --edit-key C7B99245
    
  • Windows PowerShell

    $(echo trust; echo 5; echo y; echo quit) | gpg --command-fd 0 --edit-key C7B99245
    

透過金鑰伺服器傳遞公鑰

透過 gpg --send-keys C7B99245 命令,可以將你的公鑰自動送到系統預設內建的 金鑰伺服器 (keyserver)。像我的 Ubuntu 18.04.2 LTS 作業系統,預設就設定了 hkps.pool.sks-keyservers.net 這台當成我的預設金鑰伺服器,執行時可以看到以下訊息:

$ gpg --send-keys C7B99245
gpg: sending key C22DCF85C7B99245 to hkps://hkps.pool.sks-keyservers.net

接著你找到另外一台電腦,就可以透過以下命令直接匯入你的公鑰:

gpg --keyserver hkps.pool.sks-keyservers.net --recv-key C7B99245

除此之外,你也可以找一台更為穩定的金鑰伺服器來上傳,例如 keyserver.ubuntu.com 就比預設的穩定很多,速度也較快:

gpg --keyserver keyserver.ubuntu.com --send-keys C7B99245

上傳到金鑰伺服器之後,你就可以到 https://keyserver.ubuntu.com/ 直接透過你的 E-mail 信箱即可搜尋到公鑰!

也可以透過命令快速匯入:

gpg --keyserver keyserver.ubuntu.com --recv-key C7B99245

刪除用不到的金鑰組

如果公鑰私鑰都在同一台電腦,你必須先刪除私鑰,才能刪除相對應的公鑰。也可以直接刪除私鑰就好,公鑰保留。

  • 刪除私鑰

    gpg --delete-secret-key C7B99245
    
  • 刪除公鑰

    gpg --delete-key C7B99245
    

相關連結

留言評論