以下の内容はhttps://blog.lufia.org/entry/2025/03/16/195658より取得しました。


GitHubでコミットの署名を必須にする

先日、比較的広く使われているGitHub Actionsであるtj-actions/changed-filesに不正なコードが混入された問題があった。インシデントの発生した原因は後で詳しい人が書いてくれると思うけれど、少なくとも今(2025-03-16)の理解では、bot用のPersonal Access Token(PAT)が適切に管理されていなかったことによるものらしい。

なので対策としてはPATの管理方法に向くのが筋だとは思うのだが、オープンなPRとその作者のPATがあれば悪意のある変更を入れられるんじゃないか、というのが気になってしまった。例えば過去に何度もコントリビュートしてくれている人のPRに自動生成ファイルが含まれていたとき、その人が作成した repo の権限を持ったPAT*1が運悪く漏洩していたなら、第三者が後からコミットを書き換えられるのではないか。レビューするときに自動生成ファイルも全部見るかというと、疲れているときは読み飛ばすこともあると思う。

現実には、運悪くPATが漏洩することは多くないと思うけれども、起きてからでは遅いので、自分のコミットには署名を必ず付けよう、@lufia のコミットに署名がなければ弾いてほしい、という気持ちで手順を調べた。GPG鍵の運用については以下の記事を参考にした。

準備

まずは gnupg パッケージ(Arch Linuxの場合)が必要になるが、pacman の依存に入っているので普通はインストールされていると思う。

run0 pacman -S gnupg

XDG Base Directoryにこだわりがあるなら、GNUPGHOME を設定して ~/.local/share/gnupg に変更する。この環境変数が未設定の場合は ~/.gnupg にファイルが作られる。

export GNUPGHOME="${XDG_DATA_HOME:-~/.local/share}/gnupg"
mkdir -m 700 -p $GNUPGHOME

鍵を作成するためには gpg-agent が実行されている必要があるのだが、GNUPGHOMEディレクトリを変更すると、エージェントと通信するUNIXドメインソケットのパスが以下のように変わってしまう。

$ gpgconf --list-dirs | grep agent-socket
agent-socket:/run/user/60331/gnupg/S.gpg-agent

$ export GNUPGHOME=~/.local/share}/gnupg
$ gpgconf --list-dirs | grep agent-socket
agent-socket:/run/user/60331/gnupg/d.i11o9nniqjp5zmemejdfxw8f/S.gpg-agent

そうすると、gnupg パッケージが用意してくれている /usr/lib/systemd/user/gpg-agent.socket%t/gnupg/S.gpg-agent へのアクセスを扱う構成なので、GNUPGHOME 変更後のパスと食い違いが生じてしまって意図した動作をしない。なので systemctl edit --usergpg-agent.socket ユニットのパラメータを更新する。

[Socket]
ListenStream=
ListenStream=%t/gnupg/d.i11o9nniqjp5zmemejdfxw8f/S.gpg-agent

GPGのマスターキーとサブキー

GPGにはマスターキーと、マスターキーによって署名されたサブキーというものがある。雑にいえばUnixにおける root と一般ユーザーみたいなもので、普段はサブキーを利用する運用が安全らしい。マスターキーが漏洩してしまうともう何もできることは無いが、この運用なら仮にサブキーが漏洩してもマスターキーは無事なのでサブキーの失効も可能になる。

以下ではマスターキーと、コミット署名用のサブキー1つを作成するための手順を紹介する。

マスターキーの作成

対話モードでは以下のような流れとなる。デフォルトは ECC (sign and encrypt) だけど証明(Certify)のみ必要なので --expert オプションを与えて署名(Sign)を無効化した。それ以外は好きなものを選べば良い。

$ gpg --full-generate-key --expert
Please select what kind of key you want:
   (1) RSA and RSA
   (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 (sign and encrypt) *default*
  (10) ECC (sign only)
  (11) ECC (set your own capabilities)
  (13) Existing key
  (14) Existing key from card
Your selection? 11

Possible actions for this ECC key: Sign Certify Authenticate
Current allowed actions: Sign Certify

   (S) Toggle the sign capability
   (A) Toggle the authenticate capability
   (Q) Finished

Your selection? S <--- Signを無効化

Your selection? Q <--- 完了

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)

対話モードが不便な場合、gpg --batch オプションを使うと非対話モードで実行することもできるらしい。バッチ処理で必要となるファイルの内容は公式のマニュアルを参照。

鍵のリストを表示

gpg --list-keys コマンドを使う。うまく作れている様子がみえる。

$ gpg --list-keys
/home/lufia/.local/share/gnupg/pubring.kbx
------------------------------------------
pub   ed25519 2025-03-15 [C]
      BF6A9F34814124AE28BD01597C63237ED4C24B72
uid           [ultimate] example <user@example.com>

$ gpg --list-secret-keys
/home/lufia/.local/share/gnupg/pubring.kbx
------------------------------------------
sec   ed25519 2025-03-15 [C]
      BF6A9F34814124AE28BD01597C63237ED4C24B72
uid           [ultimate] example <user@example.com>

コマンド実行結果の BF6A9... がマスターキーのフィンガープリント(指紋)と呼ばれる。また、[C] と書かれている部分は鍵が持っている役割を表している。C 以外には以下のフラグが存在する。

  • S: Signing (署名)
  • C: Certify (証明)
  • E: Encrypt (暗号化)
  • A: Authenticate (認証)

上記の例は --full-generate-key のとき証明(Certify)のみを選んだのでマスターキーの秘密鍵と公開鍵の2つしか作られていないが、暗号化(Encrypt)を選んだ場合は追加で暗号化(E)の機能を持ったサブキーも作られる。

マスターキーをバックアップ

普段使う環境に置いておくと紛失した場合に困るので、マスターキーは安全な場所に置いておき必要なときにだけ取り出したい。そこでまずはマスターキーの秘密鍵をバックアップする。--armor オプションはテキスト形式で出力する。

gpg --armor --export-secret-keys -o secret-key 'user@example.com'

コマンドが完了すると secret-keyPGP PRIVATE KEY BLOCK ヘッダを持つ秘密鍵が出力されるので、これは物理的に安全な場所へ保管する。

参考: --export-secret-keys オプションはマスターキーだけではなく、サブキーの秘密鍵も全てエクスポートする。上から順に実行している場合は、この時点ではマスターキーしかないので問題ないけれど、他の鍵がある状態でマスターキーの秘密鍵だけバックアップしたい場合は困る。その場合はフィンガープリントの末尾に ! を付けて実行すればいい*2

gpg -a --export-secret-keys BF6A9F34814124AE28BD01597C63237ED4C24B72!

Git署名用サブキーの追加

次に、コミットの署名をするためのサブキーを追加する。サブキーの追加は gpg --edit-key を使う。--edit-key はユーザーIDを要求するが、ユーザーIDはArchWikiのGnuPGによると、鍵のフィンガープリント、メールアドレス、名前の一部などが使えて、結果的に鍵が一意に特定できればなんでもいいらしい。試した限りでは大文字小文字も区別しない。

$ gpg --edit-key 'user@example.com' --expert

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)
  (10) ECC (sign only)
  (12) ECC (encrypt only)
  (14) Existing key from card
Your selection? 10
Please select which elliptic curve you want:
   (1) Curve 25519 *default*
   (4) NIST P-384
   (6) Brainpool P-256
Your selection? 1
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) 1y
Key expires at Mon Mar 16 01:45:11 2026 JST
Is this correct? (y/N) y
Really create? (y/N) y

最後に save を忘れると保存されない。逆に中止したい場合は quit とする。

gpg> save

マスターキーを普段の環境から取り除く

マスターキーを削除する方法はいくつか存在するようだったが、簡単そう*3なのでKeygripを使う方法で取り除く。Keygripは --with-keygrip オプションを付けると出力内容に含まれる。

$ gpg --with-keygrip --list-key BF6A9F34814124AE28BD01597C63237ED4C24B72
pub   ed25519 2025-03-15 [C]
      BF6A9F34814124AE28BD01597C63237ED4C24B72
      Keygrip = 1D3FDCE6C25A225890E03665FA3EBEFE927D3337
uid           [ultimate] example <user@example.com>
sub   ed25519 2025-03-15 [S] [expires: 2027-03-15]
      Keygrip = 312EB9F5918CF8CDD8EAA1A0CDFCA906D1B67037

マスターキーのKeygripが分かったら、Keygripを使って鍵を削除する。

gpg-connect-agent 'DELETE_KEY 1D3FDCE6C25A225890E03665FA3EBEFE927D3337' /bye

これで --list-secret-keyssec# と表示され、利用できない状態となる。公開鍵の方は変わらない。

$ gpg --list-keys
/home/lufia/.local/share/gnupg/pubring.kbx
------------------------------------------
pub   ed25519 2025-03-15 [C]
      BF6A9F34814124AE28BD01597C63237ED4C24B72
uid           [ultimate] example <user@example.com>

$ gpg --list-secret-keys
/home/lufia/.local/share/gnupg/pubring.kbx
------------------------------------------
sec#  ed25519 2025-03-15 [C]
      BF6A9F34814124AE28BD01597C63237ED4C24B72
uid           [ultimate] example <user@example.com>

secpub の意味は次の通り。

  • sec: 秘密鍵(Secret key)
  • ssb: サブキーの秘密鍵(Secret Subkey)
  • pub: 公開鍵(Public key)
  • sub: サブキーの公開鍵(Public Subkey)
  • sec#: 秘密鍵だけど鍵がローカルに存在しない状態

Gitのコミットに署名する

GitでGPGを使って署名するためには、サブキーのIDが必要なので調べる。

$ gpg --list-keys --keyid-format=long
...
sub   ed25519/773E2B27856EDC91 2025-03-15 [S] [expires: 2027-03-15]

ここで表示された 773E2B27856EDC91 はサブキーのフィンガープリントから末尾8バイトだけを取り出したもの*4で、これをGitの設定に追加する。

git config --global user.signingkey 773E2B27856EDC91
git config --global commit.gpgsign true

公開鍵を以下のコマンドで出力してGitHubに登録する。

gpg --armor --export 773E2B27856EDC91

以上でサブキーを使ったコミットへの署名が行われる。

コミットの署名は --show-signature オプションで見れる。

git log --show-signature

GitHubリポジトリでコミットの署名を強制したい

署名されていないコミットを弾きたい場合は、GitHubの保護ブランチ機能に Require signed commits オプションがあるので有効にするといい。これを設定しておくと、未署名のコミットを保護されたブランチにpushしたとき以下のようなエラーで弾かれる。

$ git push
remote: error: GH013: Repository rule violations found for refs/heads/main.
remote: Review all repository rules at https://github.com/lufia/plug/rules?ref=refs%2Fheads%2Fmain
remote: 
remote: - Commits must have verified signatures.
remote:   Found 1 violation:
remote: 
remote:   b254e04b07d138e4bfbe6a9743a7e1941d1be5bc
remote: 
To https://github.com/lufia/plug.git
 ! [remote rejected] main -> main (push declined due to repository rule violations)

同様に、署名のないコミットがプルリクエストに含まれていたときも、以下のようなエラーでマージがブロックされる。

未署名のコミットが混ざっているのでマージがブロックされている様子

*1:HTTPでGitHubにpushしている場合は該当する

*2:付けない場合はマッチする鍵を広く選択するらしい

*3:後から知ったけど gpg --delete-secret-keys の方が簡単かも

*4:フルサイズを表示したければ --with-subkey-fingerprint オプションを使う




以上の内容はhttps://blog.lufia.org/entry/2025/03/16/195658より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14