
id:Windymelt は日頃からgit commitをするときはデジタル署名をつけるようにしている。仕事の上では必須ではないのだけれど、OSSとかでは求められることがある。なんとGitHubはコミットに書かれたメールアドレスとユーザ名をそのまま信用して表示するので、なりすますのは簡単なのだ*1。そこで、デジタル署名することでより強く本人のコミットであるということを主張できる。また、GitHubでは署名を行うことをリポジトリ単位で強制することができて、自作のOSS用リポジトリでも基本的に署名を要求するように設定している。こうすることで、もし自分のアカウントが乗っ取られても署名用の鍵まで同時に盗むことは困難だろうから、コミットやリリースがなんかおかしいことに気付くことができる。
自分は普段はYubikeyでssh認証とGPG署名をまかなっていて、これは秘密鍵がトークンの外に出ることが絶対にない設計なので、鍵の漏洩を心配しなくても良い*2。Yubikeyはセキュアな設計になっていて、署名には毎回PINが必要である。
毎回PINを入力するのはいいのだが、とはいえ、AI Agentに作業させっぱなしにしたいこともある。AIにやらせて出掛けるとか、寝るとか、AI Agentの使いどころは色々ある。しかしながら、AIにやらせる場合であってもYubikeyはしっかりPINを求めてくるので、そこで作業は停止してしまう。
これだと地味に困るので、なんとかすることにした。
パスワードマネージャがssh-agentになる時代
便利な時代なもので、最近は各種のパスワードマネージャがssh-agentを兼ねてくれるようになっている。いちおう説明しておくと、ssh-agentとはsshに利用する秘密鍵をあらかじめメモリにロードしておき、毎回パスワードを入力する手間を省いたり、ssh先のホストからさらにsshする際に秘密鍵を取り次いでくれたりするプログラム。
例えば、1PasswordやProton Pass、Bitwardenにssh-agentとして振る舞うための機能が用意されている。
id:Windymelt はプライバシーを守る運動を行っているProtonの熱心な支持者なので、プライベートではProton Passを利用している。このため今回はProton Passで説明するが、ssh-agentの仕組みは共通なので他のパスワードマネージャを利用している場合でも問題なく読めると思うし、gitがどのようにコミットに署名を行うかについても軽く触れる。
Proton Passにはロック機能があり、一定時間経過するとロックしてssh agent機能も一時的に停止してしまうのだが、ある程度の時間的猶予を設定できるのでいったんこれでいいことにする。もちろん、セキュリティと天秤にかけて無効化もできる。
ssh-agent機能の設定を有効化したら、Proton Passが/home/windymelt/.ssh/proton-pass-ssh-agent.sock使ってね〜と言ってきたので、ここではこのパスをssh-agentのパスとして用いる。
鍵を生成する
署名と認証には鍵が必要だ。 Proton Passでは、モバイルアプリを利用すると直接鍵を生成できるので今回はそうした。既存の鍵を利用したい場合はテキスト形式で保存することになる。
github.com用にssh鍵を設定する
さて、あとは$SSH_AUTH_SOCKが/home/windymelt/.ssh/proton-pass-ssh-agent.sockを指すように~/.zshrcで設定すればシェル全体でProton Passの鍵が利用されるようになるのだが、今回はgithub.comに対してだけこの鍵を利用したい。自宅サーバへのsshは別にAIに預けたくはないからだ。
ここで活躍するのが~/.ssh/configだ。manでssh_config(5)のマニュアルを読むと、以下のように記述されている:
IdentityAgent
Specifies the Unix-domain socket used to communicate with the authentication agent.
This option overrides the SSH_AUTH_SOCK environment variable and can be used to se‐
lect a specific agent. Setting the socket name to none disables the use of an au‐
thentication agent. If the string "SSH_AUTH_SOCK" is specified, the location of
the socket will be read from the SSH_AUTH_SOCK environment variable. Otherwise if
the specified value begins with a ‘$’ character, then it will be treated as an en‐
vironment variable containing the location of the socket.
Arguments to IdentityAgent may use the tilde syntax to refer to a user's home di‐
rectory, the tokens described in the “TOKENS” section and environment variables as
described in the “ENVIRONMENT VARIABLES” section.
要するに、ホスト単位でどのエージェントを利用するかを切り替えられる。今回はこの機能を利用して、以下のようにgithub.comに対して先程のソケットを割り当てる:
Host github.com IdentityAgent /home/windymelt/.ssh/proton-pass-ssh-agent.sock
これで、github.comにsshアクセスするときは自動的にProton Passのssh-agentが利用されるようになった。
署名もsshに切り替える
先程はsshのpushのための鍵を設定した。しかし、commit署名のための鍵の設定がまだ残っている。これまでYubikeyでgpg-agentを利用して行ってきた署名だが、sshに移行したい。git 2.34以降はgpg.format sshという設定項目を利用することでssh鍵によるコミット署名に対応している。
さて、そもそもgitはなぜssh鍵でコミットに署名できるのだろうか?なんと、gitはその署名にsshを利用する際、内部でssh-keygen -Y signを呼んでいる。署名の仕事を外注していたのだ。GIT_TRACE=1をつけてssh署名付きコミットするとこの事実を確認できる。
λ GIT_TRACE=1 git commit -m 'yo' --allow-empty 00:34:15.766515 git.c:476 trace: built-in: git commit -m yo --allow-empty 00:34:15.775811 run-command.c:673 trace: run_command: /home/windymelt/.ssh/proton-pass-ssh-keygen.sh -Y sign -n git -f /tmp/.git_signing_key_tmp35lphK -U /tmp/.git_signing_buffer_tmp46HNAb 00:34:15.775836 run-command.c:765 trace: start_command: /home/windymelt/.ssh/proton-pass-ssh-keygen.sh -Y sign -n git -f /tmp/.git_signing_key_tmp35lphK -U /tmp/.git_signing_buffer_tmp46HNAb 00:34:15.819674 run-command.c:673 trace: run_command: git maintenance run --auto --no-quiet --detach 00:34:15.819876 run-command.c:765 trace: start_command: /usr/libexec/git/git maintenance run --auto --no-quiet --detach 00:34:15.821756 git.c:476 trace: built-in: git maintenance run --auto --no-quiet --detach [main fffffffff] yo
ssh-keygen -Y signはssh-keygen -Y verifyすることで検証できる。
ところで、ssh-keygenは~/.ssh/configを読まない。ssh_configはあくまで接続時に使う設定ということだ。このため、ssh-keygenで特定のエージェントを利用するには直接$SSH_AUTH_SOCKを指示しないといけない。しかしこのリポジトリでだけgit操作をするたび$SSH_AUTH_SOCKを指示するのは面倒だ。
そこでラッパースクリプトを書くことで対応する。~/.ssh/proton-pass-ssh-keygen.shを用意する:
#!/bin/sh SSH_AUTH_SOCK=/home/windymelt/.ssh/proton-pass-ssh-agent.sock exec ssh-keygen "$@"
あとは、リポジトリの.git/configに以下の設定を書き込むことで、Proton Passのエージェントを利用して署名するように仕向ける:
gpg.format ssh user.signingkey key::ssh-ed25519 (ここにssh-add -Lで得たProton Passの公開鍵を入力 ssh-ed25519といったプリアンブルは不要) gpg.ssh.program /home/windymelt/.ssh/proton-pass-ssh-keygen.sh
gitにはホスト単位でsshプログラムを制御する機能がなく、sshには(当然)gitのリポジトリ単位で制御する機能がないので、gitではリポジトリ単位で、sshではホスト単位で設定する。これが折衷案。
GitHubに鍵を設定する
GitHubにpushでき、またその上でVerifiedと表示されるためには、https://github.com/settings/keys でsigning keyとauth keyの両方にさっきの鍵を設定する必要がある。
ぜんぶProton Passに署名させてよいものか・・・
さて、自動的にProton Passを使って署名とpushができるようになった。しかしながら「即時ではないがそのうちPINを再入力しないといけない」という形で問題は先送りされてしまった。
ところで、コミットの署名ならまだしも、リリースのタグ打ちに使う署名(git tag -s)くらいはYubikeyとかでやったほうがいいと思う。
さらに言うと、リリースフローでタグを検証して、おかしなタグは拒絶する、みたいな設定にしてもいいと思う(リポジトリ単位でコミット署名を要求できるから、過剰かも)。
ところが、どの鍵を使うかはgitリポジトリで共有なので、タグは別の鍵、みたいな設定はできない。
もっと良い構成を思い付いたらまた記事にしたい。
そういえば、Proton Passにはアクセストークン機能があり、pass-cliと組み合わせてプログラマブルに鍵にアクセスできるので、これを利用できるかもしれない。
というか、セッションごとに使い捨ての鍵をメモリ上に生成して、最初の一回だけ鍵をGitHubに登録する、みたいな発想があってもいいと思う。ghにはssh鍵を追加/削除するコマンドもある。でもこれだとコミット用の鍵はずっと残さないといけない。うーん。
他の研究
id:mizdra は、YubikeyやパスワードマネージャのかわりにSecure Enclaveを利用している。Secure EnclaveはMacbook Proにしかなさそう。自分はLinuxを普段使っているので、読者は可搬性や利便性を判断して使い分けられるとよさそう。
