專文 10 / 數位

[OpenPGP - 離線主鑰] 把秘密留在機器之外

離線保存 OpenPGP 主私鑰,使用子鑰執行日常操作,並在移除任何鑰匙前測試復原。

本譯文版本尚待母語編輯審閱。

斷開連接的低壓電路板、萬用電表探針和空電池盒整齊排列,準備進行故障診斷。
故障範圍愈小,問題愈容易回答。原創插畫場景。

將 OpenPGP 主私鑰保存在日常電腦之外。使用不同的子鑰執行簽署、解密與驗證身分。這可限制電腦遭入侵後攻擊者能做的事:認證新的子鑰與身分資訊需要主鑰。

此程序使用 Linux 上的 GnuPG、加密卸除式儲存裝置,以及位於 /dev/shm 的暫存鑰匙圈。你必須能辨識正確的鑰匙與儲存裝置。修改現有身分前,先用測試鑰匙練習。

範例沿用原始操作指南的 RSA-8192 主鑰與 RSA-4096 子鑰。RSA-8192 是範例設定,並非離線保存主鑰的必要條件。請換成你自己的完整指紋。透過 pinentry 輸入通行片語,不要寫進指令碼或 shell 歷史。

基礎 · 主鑰與子鑰的用途

發布公鑰,讓他人驗證簽章或加密寄給你的訊息。保護用來授權操作的私鑰,並分開各種能力:

能力 用途 此處使用的鑰匙
認證 [C] 認證使用者 ID、子鑰及其他公鑰 主鑰
簽署 [S] 簽署訊息、檔案或 Git 提交 簽署子鑰
加密 [E] 接收加密資料,並用私密子鑰解密 加密子鑰
驗證身分 [A] 向服務驗證身分,例如透過 gpg-agent 使用 SSH 身分驗證子鑰

將認證與日常操作分開

  • 主鑰只啟用認證能力。
  • 分別建立簽署、加密與身分驗證子鑰。
  • 將所有私鑰的已測試備份保存在加密卸除式儲存裝置。
  • 測試還原後,從日常鑰匙圈移除主私鑰。
  • 可選擇將日常子鑰移至 OpenPGP 智慧卡。

使用完整指紋辨識鑰匙。keygrip 是另一種識別碼,供 gpg-agent 辨識私鑰元件。刪除前先檢查兩者:

gpg --list-secret-keys --fingerprint --with-keygrip
進階 · 建立主鑰與三把子鑰

將此範本存為 primary_gpg_key_constructor.tpl,並替換姓名與電子郵件地址。此範例的 Expire-Date: 0 表示主鑰不過期;若你的鑰匙政策要求期限,請設定到期日。GnuPG 會透過 pinentry 要求通行片語。

%echo Generating a RSA key 8192
Key-Type: RSA
Key-Length: 8192
Key-Usage: cert
Name-Real: My Name
Name-Email: something@something
Expire-Date: 0
%commit
%echo done
gpg --batch --generate-key --enable-large-rsa primary_gpg_key_constructor.tpl
gpg --list-secret-keys

此範例用 --enable-large-rsa 建立 8192 位元鑰匙。確認主鑰僅顯示 [C]。以下為示意輸出:

sec   rsa8192 2023-01-14 [C]
      E6B0933DD47E3B2F823334F0A567B3B17C27BF6D
uid           [ultimate] My Name <something@something>
gpg --expert --edit-key E6B0933DD47E3B2F823334F0A567B3B17C27BF6D

在 gpg> 提示字元加入子鑰:

  • 簽署:addkey,選 RSA(僅簽署)、4096 位元,並設定到期日。
  • 加密:addkey,選 RSA(僅加密)、4096 位元,並設定到期日。
  • 身分驗證:addkey,選 RSA(自訂能力)。停用 S 與 E,啟用 A,再選大小與到期日。

選單編號可能隨版本不同。請依能力選擇並確認結果。用 save 儲存變更,再列出所有指紋:

gpg --fingerprint --fingerprint E6B0933DD47E3B2F823334F0A567B3B17C27BF6D
pub   rsa8192 2023-01-14 [C]
      E6B0 933D D47E 3B2F 8233  34F0 A567 B3B1 7C27 BF6D
uid           [ultimate] My Name <something@something>
sub   rsa4096 2023-01-14 [S] [expires: 2030-01-12]
      61DF 146E F7F6 0971 5E60  46B8 8C75 87C8 6EAC 2F2F
sub   rsa4096 2023-01-14 [E] [expires: 2030-01-12]
      DBBC AE3F 8166 9FF4 75B7  5F35 51D8 8833 7727 23AD
sub   rsa4096 2023-01-14 [A] [expires: 2030-01-12]
      F4CF F234 E969 2304 CC58  D55B D13A E6B6 5E8B E723

替換現有主鑰時,可在停用舊鑰前用它認證新鑰。請明確指定舊簽署鑰匙,不要依賴 GnuPG 的預設選擇:

gpg --local-user "$OLD_FINGERPRINT" --sign-key "$NEW_FINGERPRINT"
中階 · 選擇能持續維護的設定

若不需要 OpenPGP 認證,可考慮用 age 加密檔案,以及專用 SSH 鑰匙登入。若主鑰留在日常電腦上,請用通行片語保護、保存撤銷憑證,並測試加密備份。此設定仍讓主鑰暴露於日常電腦,應記錄這項限制。

移除主鑰前先備份與測試

在移除主鑰或將子鑰移至卡片前匯出私鑰。之後的匯出可能只有不可用鑰匙的參照,而非可還原的私密資料。匯出指令成功並不代表能成功還原。

以下 RAM 鑰匙圈可減少持久儲存寫入。/dev/shm 可能使用 swap,刪除其中檔案也不保證安全抹除。請使用可信任的環境,停用或加密 swap,且不使用休眠。連網或遭入侵電腦上的 RAM 鑰匙圈並非離線環境。

進階 · 匯出至加密裝置並測試還原

請在 Bash 中執行以下範例。先確認目前的 GnuPG 目錄:

gpgconf --list-dirs homedir

設定目的地與指紋,並確認預期的加密裝置已掛載。mountpoint 只檢查是否存在掛載點,不會驗證加密狀態或裝置身分。

set -euo pipefail
umask 077
ENCRYPTED_MOUNT="/run/media/user/Encrypted_Device"
EXPORT_DIR="$ENCRYPTED_MOUNT/secrets/gpg"
EXPORT_FILENAME="MySelf_Master_MyComment_private_rsa8192"
EXPORT_FINGERPRINT="E6B0933DD47E3B2F823334F0A567B3B17C27BF6D"
if ! mountpoint -q -- "$ENCRYPTED_MOUNT"; then
    echo "Encrypted device is not mounted at $ENCRYPTED_MOUNT" >&2
    exit 1
fi
install -d -m 0700 -- "$EXPORT_DIR"

每版備份使用新的檔名。保留上一份已測試的備份,直到新備份通過還原檢查。匯出完整私鑰、日常子鑰、公鑰及 ownertrust:

gpg --armor --output "$EXPORT_DIR/$EXPORT_FILENAME.private-key.sec" \
    --export-secret-keys "$EXPORT_FINGERPRINT"
gpg --armor --output "$EXPORT_DIR/$EXPORT_FILENAME.subkeys.sec" \
    --export-secret-subkeys "$EXPORT_FINGERPRINT"
gpg --armor --output "$EXPORT_DIR/$EXPORT_FILENAME.pubkey.asc" \
    --export "$EXPORT_FINGERPRINT"
gpg --list-secret-keys --fingerprint --with-keygrip "$EXPORT_FINGERPRINT" \
    > "$EXPORT_DIR/$EXPORT_FILENAME.keygrip.txt"
gpg --export-ownertrust > "$EXPORT_DIR/gnupg-ownertrust.txt"

趁主私鑰仍可用時產生撤銷憑證。妥善保護它們:取得憑證的人都能發布撤銷。以下範例涵蓋四種撤銷原因:

printf '%s\n' y 0 "" y | gpg --no-tty --command-fd 0 --armor \
    --output "$EXPORT_DIR/revoke.no-reason.asc" --gen-revoke "$EXPORT_FINGERPRINT"
printf '%s\n' y 1 "" y | gpg --no-tty --command-fd 0 --armor \
    --output "$EXPORT_DIR/revoke.compromised.asc" --gen-revoke "$EXPORT_FINGERPRINT"
printf '%s\n' y 2 "" y | gpg --no-tty --command-fd 0 --armor \
    --output "$EXPORT_DIR/revoke.superseded.asc" --gen-revoke "$EXPORT_FINGERPRINT"
printf '%s\n' y 3 "" y | gpg --no-tty --command-fd 0 --armor \
    --output "$EXPORT_DIR/revoke.no-longer-used.asc" --gen-revoke "$EXPORT_FINGERPRINT"
chmod 0600 -- "$EXPORT_DIR"/*
  • private-key.sec:主鑰與子鑰的私密資料,用於完整復原。
  • subkeys.sec:日常私密子鑰,不含主私鑰。
  • pubkey.asc:供發布的公開憑證。
  • keygrip.txt:用於檢查鑰匙元件的識別碼。
  • gnupg-ownertrust.txt:本機信任設定,匯入其他鑰匙圈前先檢查。
  • revoke.*.asc:撤銷主鑰的憑證。

將完整私鑰備份匯入另一個暫存鑰匙圈:

umask 077
TEST_GNUPGHOME="$(mktemp -d /dev/shm/gnupg-test.XXXXXX)"
chmod 0700 "$TEST_GNUPGHOME"
gpg --homedir "$TEST_GNUPGHOME" \
    --import "$EXPORT_DIR/$EXPORT_FILENAME.private-key.sec"
gpg --homedir "$TEST_GNUPGHOME" \
    --list-secret-keys --fingerprint --with-keygrip "$EXPORT_FINGERPRINT"

確認完整指紋、可用的 sec 及三個 ssb 項目。測試簽署、解密先前加密的測試檔案,以及已設定的身分驗證。也要測試還原的主鑰能認證一把測試鑰匙。僅列出鑰匙並不能證明私密資料可用。

停止暫存代理程式,並且只移除上方建立的測試目錄:

case "$TEST_GNUPGHOME" in
    /dev/shm/gnupg-test.*)
        gpgconf --homedir "$TEST_GNUPGHOME" --kill all
        rm -rf -- "$TEST_GNUPGHOME"
        ;;
    *) echo "Unexpected test keyring path" >&2; exit 1 ;;
esac

確認復原成功後,只從日常鑰匙圈移除主私鑰。引號中的 ! 指定該元件;省略它也會刪除私密子鑰。繼續前先確認目前目錄與指紋。GnuPG 指令參考。

gpg --delete-secret-keys "${EXPORT_FINGERPRINT}!"
gpg --list-secret-keys "$EXPORT_FINGERPRINT"
sec#  rsa8192 2023-01-14 [C]
      E6B0933DD47E3B2F823334F0A567B3B17C27BF6D
uid           [ultimate] My Name <something@something>
ssb   rsa4096 2023-01-14 [S] [expires: 2030-01-12]
ssb   rsa4096 2023-01-14 [E] [expires: 2030-01-12]
ssb   rsa4096 2023-01-14 [A] [expires: 2030-01-12]

sec# 表示此鑰匙圈無法使用主私鑰,不能證明其他鑰匙圈、備份、檔案系統快照或可復原的磁碟資料中沒有副本。建立新身分時,請在離線環境產生主鑰,只將日常子鑰匯入日常電腦。

在獨立環境使用主鑰

在可信任的離線環境執行認證、身分變更與子鑰管理。只有執行必要操作時才連接加密備份。將它與日常電腦分開存放,並在不同地點保留另一份已測試副本。

sec    actual primary secret available
sec#   primary secret unavailable in this keyring
ssb    secret subkey locally available
ssb#   secret subkey unavailable
ssb>   secret subkey on a smartcard
進階 · 還原、修改並測試新備份

從一般 GnuPG 目錄開始,不要預先覆寫 GNUPGHOME。在可信任的離線環境確認加密掛載點,並將備份匯入暫存目錄:

umask 077
ENCRYPTED_MOUNT="/run/media/user/Encrypted_Device"
EXPORT_DIR="$ENCRYPTED_MOUNT/secrets/gpg"
EXPORT_FILENAME="MySelf_Master_MyComment_private_rsa8192"
EXPORT_FINGERPRINT="E6B0933DD47E3B2F823334F0A567B3B17C27BF6D"
mountpoint -q -- "$ENCRYPTED_MOUNT" || exit 1
NORMAL_GNUPGHOME="$(gpgconf --list-dirs homedir)"
GNUPGHOME="$(mktemp -d /dev/shm/gnupg.XXXXXX)"
export GNUPGHOME
chmod 0700 "$GNUPGHOME"
gpg --import "$EXPORT_DIR/$EXPORT_FILENAME.private-key.sec"
gpg --import-ownertrust "$EXPORT_DIR/gnupg-ownertrust.txt"
gpg --list-secret-keys --fingerprint --with-keygrip "$EXPORT_FINGERPRINT"

編輯前確認主鑰指紋及可用的私密元件:

gpg --edit-key "$EXPORT_FINGERPRINT"

修改此鑰匙圈不會更新備份檔案。變更通行片語、到期日、使用者 ID、認證或子鑰後,請匯出新的候選備份:

NEW_BACKUP="$EXPORT_DIR/$EXPORT_FILENAME.private-key.NEW.sec"
gpg --armor --output "$NEW_BACKUP" --export-secret-keys "$EXPORT_FINGERPRINT"
chmod 0600 "$NEW_BACKUP"

將候選備份還原至第二個暫存鑰匙圈並重做檢查。同時更新並測試子鑰與公鑰匯出。通過後才封存前一版並替換整組備份。在他人取得公鑰的位置發布更新後的公開憑證。

case "$GNUPGHOME" in
    /dev/shm/gnupg.*)
        gpgconf --homedir "$GNUPGHOME" --kill all
        rm -rf -- "$GNUPGHOME"
        ;;
    *) echo "Unexpected working keyring path" >&2; exit 1 ;;
esac
unset GNUPGHOME
gpgconf --list-dirs homedir

確認最後的路徑與 NORMAL_GNUPGHOME 相同。在日常電腦上只匯入更新後的公開憑證與日常子鑰:

gpg --import "$EXPORT_DIR/$EXPORT_FILENAME.pubkey.asc"
gpg --import "$EXPORT_DIR/$EXPORT_FILENAME.subkeys.sec"
gpg --import-ownertrust "$EXPORT_DIR/gnupg-ownertrust.txt"

在日常電腦確認 sec#,並測試日常子鑰。在該電腦匯入 private-key.sec 會讓主私鑰再次可用。

撤銷受影響的鑰匙

進階 · 撤銷前先選取子鑰

若子鑰遭洩漏,在離線環境載入主鑰並編輯憑證:

gpg --expert --edit-key E6B0933DD47E3B2F823334F0A567B3B17C27BF6D

用 key N 選取受影響的子鑰。確認選取標記與指紋,執行 revkey,再執行 save。匯出並發布更新後的公開憑證。用 delkey 刪除本機鑰匙不會通知他人該鑰匙已撤銷。

主鑰遭洩漏或停用時,使用適當的主鑰撤銷憑證,並發布已撤銷的公開憑證。若仍需解密歷史資料,依你的資料保留與洩漏處理政策保存舊加密私鑰。

驗證結果

完成設定的條件是:完整備份已測試、撤銷憑證可取得、日常子鑰可正常使用,以及日常鑰匙圈無法使用主私鑰。記錄備份位置與還原程序。修改鑰匙後重新執行復原檢查。

下一篇專文 →回到開頭 ↑