Git のリポジトリ認証とコミット署名は 2 つの独立した段階です。前者はコードのプルやプッシュが可能かどうかを決め、後者は受信者がどのキーによってコミットが署名されたかを検証できるようにします。本文では 2 組の独立した FIDO2 SSH キーを使用し、この 2 つの用途を分けて管理します。
Git は SSH キーによるコミットとタグの署名をサポートしているため、Git の開発だけを行う場合は、YubiKey の FIDO2 機能を直接使用でき、GnuPG を追加で導入する必要はありません。OpenPGP による署名検証手順に従う必要があるプロジェクトでは、プロジェクトの要件に従って GPG 署名を選択してください。
方針と適用範囲
| 用途 | 方式 |
|---|---|
| リポジトリ SSH 認証 | 独立した ed25519-sk 資格情報 |
| コミット・タグの署名 | 別の独立した ed25519-sk 資格情報 |
| パソコン間での使用 | 駐留資格情報 resident |
| 使用時の検証 | verify-required、デフォルトのタッチ要件を維持 |
| ハードウェア冗長性 | 2 本目の YubiKey で資格情報を独立して生成し、公開鍵を事前登録 |
本文は macOS、Homebrew、Gitea を対象とします。コマンド内のメールアドレス、ID 名、ドメイン名、リポジトリアドレスはすべて例です。認証側には OpenSSH の FIDO キータイプに対応している必要があります。古い SSH サーバーでは ed25519-sk を受け付けられない場合があります。
FIDO2 資格情報は PIV の 9a、9c、9d スロットを使用しません。本文では YubiKey のリセットも行いません。
一、ソフトウェアとハードウェアを確認する
最初に実際のバージョンとプログラムのパスを確認します。
git --version
command -v git
brew --prefix openssh
"$(brew --prefix openssh)/bin/ssh" -V
ykman info
ykman fido info
Git の SSH 署名には 2.34 以降が必要です。より新しく、ビルド時に FIDO サポートを有効にした OpenSSH を使用することを推奨します。バージョン番号が新しいだけでは、そのビルドがハードウェアキーをサポートしているとはいえません。本文では Homebrew の OpenSSH を使用し、macOS 標準のプログラムと混同しないよう、プログラムのパスを明示的に指定します。
ed25519-sk には、対応するアルゴリズムを実装するハードウェアが必要です。YubiKey の対応するファームウェアは 5.2.3 以降である必要があります。デバイスが対応していない場合は、公式のアルゴリズム一覧を確認してください。「FIDO2 に対応している」だけでは、Ed25519 に対応しているとは判断できません。
ツールがインストールされていない場合は、次のようにインストールします。
brew install openssh git ykman
すでにインストールされており、バージョン条件も満たしている場合は、インストールを省略できます。古い macOS では、現在の Homebrew バイナリパッケージのサポート範囲外となる場合があります。その場合、インストールにはソースからのコンパイルや依存関係のアップグレードが必要です。これはソフトウェアのインストール問題であり、キーを再生成する必要はありません。
FIDO2 PIN をまだ設定していない場合は、対話形式で設定します。
ykman fido access change-pin
すでに PIN を設定済みの場合は省略できます。FIDO2 PIN、PIV PIN、ローカルファイルのパスワードはそれぞれ別の概念であり、混同しないでください。
二、認証用と署名用の独立した資格情報を生成する
まずメインの YubiKey だけを挿入します。同じターミナルで変数を設定し、メールアドレスをホスティングプラットフォームのアカウントで検証済みのものに変更します。
CX_GIT_EMAIL='developer@example.com'
CX_KEY_ROLE=primary
CX_SSH_DIR="$(brew --prefix openssh)/bin"
mkdir -p "$HOME/.ssh"
chmod 700 "$HOME/.ssh"
CX_AUTH_KEY="$HOME/.ssh/id_ed25519_sk_git_auth_${CX_KEY_ROLE}"
CX_SIGN_KEY="$HOME/.ssh/id_ed25519_sk_git_sign_${CX_KEY_ROLE}"
以下のループは、認証キーと署名キーをそれぞれ生成します。同名のファイルがすでに存在する場合はスキップします。以前に一部のファイルしか生成していない場合は、まずファイルとハードウェア内の資格情報の状態を確認し、対処方法を決める必要があります。
for CX_KEY_USE in auth sign; do
CX_KEY_FILE="$HOME/.ssh/id_ed25519_sk_git_${CX_KEY_USE}_${CX_KEY_ROLE}"
if [ -e "$CX_KEY_FILE" ] || [ -e "$CX_KEY_FILE.pub" ]; then
printf '跳过已有文件:%s\n' "$CX_KEY_FILE"
continue
fi
"$CX_SSH_DIR/ssh-keygen" \
-t ed25519-sk \
-O resident \
-O verify-required \
-O "application=ssh:git-${CX_KEY_USE}-${CX_KEY_ROLE}" \
-O "user=developer-${CX_KEY_ROLE}" \
-C "git-${CX_KEY_USE}-${CX_KEY_ROLE}" \
-f "$CX_KEY_FILE" || break
done
生成処理に失敗した場合は、先にエラーを解決してから設定を続けます。ハードウェア内の既存の資格情報を上書きする旨の確認vigor参加了,表示された n を選択し、資格情報の用途を先に確認してください。
各パラメータの意味は次のとおりです。
| パラメータ | 作用 |
|---|---|
-t ed25519-sk | ハードウェアがサポートする Ed25519 SSH キーを作成 |
-O resident | 資格情報の参照もハードウェアに保存し、別パソコンから再取得しやすくする |
-O verify-required | 署名ごとにユーザー確認を要求する。通常の YubiKey では通常 PIN を使用 |
-O application=ssh:… | 資格情報のアプリケーション識別子を区別する。必ず ssh: で始める必要がある |
-O user=… | 駐留資格情報に関連付けるユーザー名を設定する |
-C … | ローカル SSH キーのコメントを設定する |
タッチはユーザーの在席確認、PIN はユーザー認証であり、役割が異なります。resident は主にパソコン間での使用を容易にするもので、アルゴリズムの強度を高めるものではありません。
生成時に表示される Enter passphrase は、ローカル参照ファイルの暗号化パスワードです。必要に応じて設定,也可以空のままにできます。ローカル参照ファイルにパスワードを設定しなくても、ハードウェア署名には対応する YubiKey が必要であり、資格情報の検証ポリシーに従う必要があります。
.pub を付けないローカルファイルはハードウェアの資格情報を参照するためのものであり、通常のソフトウェアの秘密鍵のようにハードウェアから独立して署名することはできません。.pub ファイルは公開鍵の配布に使用します。
三、Git の認証と署名を設定する
以下ではグローバルな既定設定を使用します。これは現在のユーザーの Git リポジトリに影響します。既存のリポジトリのローカル設定がグローバル設定を上書きする場合があります。プロジェクトごとに識別子を分ける必要がある場合は、リポジトリ内で git config --local を使用してください。
git config --global user.name 'Developer'
git config --global user.email "$CX_GIT_EMAIL"
git config --global core.sshCommand \
"\"$CX_SSH_DIR/ssh\" -i \"$CX_AUTH_KEY\" -o IdentitiesOnly=yes"
git config --global gpg.format ssh
git config --global gpg.ssh.program "$CX_SSH_DIR/ssh-keygen"
git config --global user.signingkey "$CX_SIGN_KEY"
git config --global commit.gpgsign true
git config --global tag.gpgsign true
混同しやすい点が 3 つあります。
core.sshCommandは Git の SSH トランスポートに使用され、メールアドレスやコミット署名はこれを通じて設定されません。認証ファイルをグローバルに指定すると、Git は既定で各 SSH ホスティングプラットフォームに対して同じ認証キーを使用します。プラットフォームごとに分ける場合は、リポジトリ単位のローカル設定または SSH のHost設定を使用してください。gpg.format = sshは署名形式を指定します。設定名にはgpgが含まれますが、実際の署名プログラムはssh-keygenです。user.signingkeyは.pubを付けないローカルキー参照ファイルを指すため、署名処理から直接使用できます。.pubを指定する場合は、対応するキーが利用可能なssh-agentにすでに存在している必要があります。
本文の主手順は署名キーを固定で指定し、自動選択には依存せず、先に ssh-add を実行する必要もありません。
四、ローカルでの署名検証を設定する
署名が有効であることと、署名者が信頼されていることは別の問題です。Git は allowedSignersFile を使用して、信頼済みの SSH 公開鍵と識別子を関連付けます。
既存のグローバル信頼リストを引き継ぎ、設定がない場合は ~/.ssh/allowed_signers を使用します。
CX_ALLOWED_SIGNERS="$(git config --global --path --get gpg.ssh.allowedSignersFile)"
CX_ALLOWED_SIGNERS="${CX_ALLOWED_SIGNERS:-$HOME/.ssh/allowed_signers}"
mkdir -p "$(dirname "$CX_ALLOWED_SIGNERS")"
printf '%s namespaces="git" %s\n' \
"$CX_GIT_EMAIL" \
"$(cat "$CX_SIGN_KEY.pub")" >> "$CX_ALLOWED_SIGNERS"
git config --global gpg.ssh.allowedSignersFile "$CX_ALLOWED_SIGNERS"
この操作は追記であり、既存のエントリを消去しません。繰り返し実行すると重複行会产生るため、手動で重複除去してください。公開鍵部分は .pub ファイルから直接読み取り、実際のアルゴリズム識別子はそのまま保持されます。ed25519-sk に対応する公開鍵のタイプは通常 sk-ssh-ed25519@openssh.com であり、手動で ssh-ed25519 に変更しないでください。
namespaces="git" は、この信頼エントリを Git の署名用途に限定します。このファイルは現在のパソコンでのローカル署名検証のみを制御し、Gitea に自動的に同期することも、公開鍵がすでにアップロードされていることも意味しません。
五、一時的なリポジトリで検証する
CX_TEST_REPO="$(mktemp -d "${TMPDIR:-/tmp}/git-sign-test.XXXXXX")"
git init "$CX_TEST_REPO"
git -C "$CX_TEST_REPO" commit --allow-empty -m "Test hardware SSH signing"
git -C "$CX_TEST_REPO" verify-commit HEAD
git -C "$CX_TEST_REPO" tag -m "Test signed tag" yubikey-test
git -C "$CX_TEST_REPO" verify-tag yubikey-test
署名時には、表示に従って PIN の入力とタッチ操作を行います。2 回の検証が成功すると、次のような情報が表示され、コマンドの終了ステータスは 0 であるべきです。
Good "git" signature for developer@example.com ...
直近のコミットの署名を確認することもできます。
git -C "$CX_TEST_REPO" log -1 --show-signature
これはローカルテストであり、リモートには何もプッシュしません。この時点で askpass エラーが発生した場合は、まず後述の方法で対話環境を確認し、失敗したコミットまたはタグの作成コマンドを再実行してください。
六、Gitea に公開鍵を登録して検証する
個人設定の SSH/GPG 鍵管理ページを開きます。認証公開鍵と署名公開鍵はいずれも SSH 公開鍵であるため、SSH 鍵の領域にそれぞれ追加します。
まず認証公開鍵をコピーし、貼り付けて git-auth-primary として保存します。
pbcopy < "$CX_AUTH_KEY.pub"
保存したら、署名公開鍵をコピーして git-sign-primary として追加します。
pbcopy < "$CX_SIGN_KEY.pub"
認証と署名を別々に生成すると、独立したローテーションに有利です。ただし、これを理由に、ホスティングプラットフォームが 2 つの公開鍵に対して厳格な用途分離を提供していると仮定しないでください。Gitea のアカウント SSH 公開鍵は通常、リポジトリへのアクセス権限も持ちます。実際の挙動はインストールしたバージョンの実装に従います。
署名公開鍵に対して、ページに記載された所有権検証を実行します。token には、そのページで今回生成された値を使用し、namespace はページのコマンドに従います。ページが -n gitea を使用している場合、次のようになります。
printf '%s' '替换为页面当前显示的token' |
"$CX_SSH_DIR/ssh-keygen" -Y sign -n gitea -f "$CX_SIGN_KEY"
出力の -----BEGIN SSH SIGNATURE----- から -----END SSH SIGNATURE----- までの完全な署名ブロックをページに戻して貼り付けます。コマンドのプロンプト、PIN のプロンプト、タッチのプロンプトを一緒にコピーしないでください。
Gitea の token 検証と、ローカルの allowed_signers 設定は別々のものです。コミットに記録されるメールアドレスも、アカウントで有効化されているメールアドレスに対応している必要があります。ローカルでの署名検証が成功しても、プラットフォームが必ず検証済みとして表示するとは限りません。
リポジトリへのアクセスをテストする場合は、リポジトリページから完全な SSH クローンアドレスをコピーします。
git ls-remote 'ssh://git@git.example.com:2222/developer/project.git'
例.domain、SSH ポート、リポジトリパスは、すべて置き換える必要があります。SSH ポートはクローンアドレスに従い、HTTPS ページのポートから推測しないでください。既存のリポジトリでは git remote -v を確認し、SSH アドレスを使用していることも確認できます。
七、予備の YubiKey を設定する
メインキーを抜き、予備キーだけを挿入し、その FIDO2 PIN が設定済みであることを確認します。元のターミナルで次を実行します。
CX_KEY_ROLE=backup
その後、第 2 節の CX_AUTH_KEY=…、CX_SIGN_KEY=… の 2 行から開始して、生成、設定、ローカルテスト、プラットフォーム登録の手順をもう一度実行します。CX_KEY_ROLE を primary に戻すしないでください。新しいファイルには backup サフィックスが付けられ、Gitea の対応する公開鍵にも異なる名前を付け、署名公開鍵の検証も完了させる必要があります。
予備キーは独立したキーを保持するため、ローカル参照ファイルをコピーしてもメインキーの FIDO2 資格情報を複製することはできません。両方のキーの公開鍵を事前登録する必要があります。両方の署名公開鍵を信頼リストに追加した後、ローカルで両方の署名を認識できるようになります。
予備キーのテストが完了したら、メインキーを戻します。今後使用するキーを切り替えるには、次のコマンドを実行し、1 行目のロールだけを変更します。
CX_KEY_ROLE=primary
CX_SSH_DIR="$(brew --prefix openssh)/bin"
CX_AUTH_KEY="$HOME/.ssh/id_ed25519_sk_git_auth_${CX_KEY_ROLE}"
CX_SIGN_KEY="$HOME/.ssh/id_ed25519_sk_git_sign_${CX_KEY_ROLE}"
git config --global core.sshCommand \
"\"$CX_SSH_DIR/ssh\" -i \"$CX_AUTH_KEY\" -o IdentitiesOnly=yes"
git config --global user.signingkey "$CX_SIGN_KEY"
切り替えによって変更されるのは、今後の認証と署名で使用するキーだけです。履歴のコミットは変更されません。
八、パソコンを替えた後に参照を復元する
新しいパソコンに FIDO 対応 OpenSSH をインストールし、新規の空ディレクトリに入ります。
CX_SSH_DIR="$(brew --prefix openssh)/bin"
CX_RESTORE_DIR="$(mktemp -d "${TMPDIR:-/tmp}/ssh-resident-restore.XXXXXX")"
cd "$CX_RESTORE_DIR"
"$CX_SSH_DIR/ssh-keygen" -K
表示に従って PIN を入力し、ハードウェアを操作します。ssh-keygen -K は、駐留資格情報に対応する参照と公開鍵を現在のディレクトリに書き込みます。YubiKey から実際の秘密鍵をエクスポートするものではありません。
登録済みの公開鍵のフィンガープリントと照合して用途を識別し、対応するファイルを ~/.ssh に移動して、所望の名前に変更します。
"$CX_SSH_DIR/ssh-keygen" -lf '实际恢复出的公钥文件.pub'
復元後のローカルファイル名、コメント、暗号化で包まれた内容は、古いパソコンとは異なっている可能性があります。参照ファイルがバイト単位で一致しているかどうかを比較するのではなく、公開鍵とフィンガープリントで身元を確認してください。復元後は、Git とローカルの信頼リストを再設定する必要があります。
九よくある質問
1. ssh-askpass が存在しない、または PIN を入力できない
たとえば、次のようなエラーが表示されます。
ssh_askpass: exec(/usr/X11R6/bin/ssh-askpass): No such file or directory
Couldn't sign message: incorrect passphrase supplied to decrypt private key
まず、署名プログラム、実行環境、完全なエラーを確認します。askpass が存在しないことは、対話式入力の経路に問題があることを示しています。後続の汎用的なエラーメッセージだけでは、PIN の入力に誤りがあったと証明できません。ssh-keygen によるハードウェア署名での PIN 要求は、askpass を通じて行われる場合があります。IDE、パイプ、agent などの環境によっては、プロンプトの表示方法も変わります。
利用可能な askpass がない場合は、macOS で次の最小限のヘルパーを使用できます。システムダイアログからパスワードを読み取り、呼び出し元プロセスに出力します。パスワードはファイルに書き込みません。これは本文が提供する補助実装であり、OpenSSH に付属するプログラムではありません。
mkdir -p "$HOME/.local/bin"
cat > "$HOME/.local/bin/ssh-askpass-macos" <<'SH'
#!/bin/sh
case "${SSH_ASKPASS_PROMPT:-}" in
none) exit 0 ;;
confirm)
exec /usr/bin/osascript - "$@" <<'APPLESCRIPT'
on run argv
display dialog (item 1 of argv) buttons {"Cancel", "OK"} default button "OK" cancel button "Cancel" with title "OpenSSH"
return ""
end run
APPLESCRIPT
;;
esac
exec /usr/bin/osascript - "$@" <<'APPLESCRIPT'
on run argv
set resultDialog to display dialog (item 1 of argv) default answer "" with hidden answer buttons {"Cancel", "OK"} default button "OK" cancel button "Cancel" with title "OpenSSH"
return text returned of resultDialog
end run
APPLESCRIPT
SH
chmod 700 "$HOME/.local/bin/ssh-askpass-macos"
export SSH_ASKPASS="$HOME/.local/bin/ssh-askpass-macos"
export SSH_ASKPASS_REQUIRE=force
失敗した署名操作をもう一度実行します。force によって、関連するパスワード入力が優先的に askpass 経由で完了するため、ポップアップ表示のために不用意に DISPLAY を設定する必要はありません。ダイアログをキャンセルすると現在の操作は失敗しますが、これは正常な動作です。
利用可能になったことを確認したら、最後の 2 行の export をターミナルの起動ファイルに含められます。GUI から起動した IDE はターミナルの起動ファイルを読み取らない場合があるため、実際に Git を実行するプロセスがこの 2 つの変数を継承できることを確認してください。設定済みの環境を持つターミナルから、コマンドライン起動に対応した IDE を起動して検証できます。署名が agent によって実行される場合、agent 自身の環境も正しく設定する必要があります。クライアント側の変数だけを変更しても、実行中の agent には反映されません。
2. キーを挿入しても、ssh-add -L にキーが表示されない
ハードウェアを挿入しただけでは、すべての駐留資格情報が現在の agent に自動追加されるわけではありません。まず、使用している agent とプログラムのパスを確認します。
printf '%s\n' "$SSH_AUTH_SOCK"
"$CX_SSH_DIR/ssh-add" -L
実際に agent が必要なら、特定のローカル参照ファイルを追加できます。
"$CX_SSH_DIR/ssh-add" "$CX_SIGN_KEY"
"$CX_SSH_DIR/ssh-add" -L
ハードウェアから駐留資格情報を読み込むこともできます。
"$CX_SSH_DIR/ssh-add" -K
ここでは、FIDO をサポートする OpenSSH の ssh-add を使用する必要があります。macOS 標準の ssh-add にあった過去の同名オプションと混同しないでください。現在の agent がこのタイプをサポートしていない場合は、まず agent の互換性と環境の問題を解決する必要があります。参照を追加することは、実際のハードウェア秘密鍵をエクスポートすることではなく、以降の PIN やタッチの要件が免除されることもありません。
本文の主手順がローカル参照ファイル固定で使用する流れは、ssh-add -L に署名キーが表示されていることに依存しません。
3. defaultKeyCommand が no keys を返す
たとえば、次のようなエラーが表示されます。
warning: gpg.ssh.defaultKeyCommand succeeded but returned no keys
error: user.signingKey needs to be set for ssh signing
gpg.ssh.defaultKeyCommand は、user.signingkey が設定されていない場合に候補キーを照会するためのメカニズムです。挿入されたハードウェアを自動的に検出することはありません。照会コマンドが成功しても公開鍵が出力されなければ、署名を完了できません。
本文の固定キー方式では、有効な設定を次のように確認できます。
git config --show-origin --get user.signingkey
git config --show-origin --get gpg.ssh.defaultKeyCommand
git config --show-origin --get gpg.ssh.program
その後、ローカルの参照ファイルを明示的に指定します。
git config --global user.signingkey "$CX_SIGN_KEY"
リポジトリローカル設定はグローバル設定を上書きするため、--show-origin で示される設定元に基づいて調整してください。引き続き agent の自動選択を使用する場合は、先に agent 内に実際に鍵があることを確認し、その後でクエリスクリプトに対象の署名公開鍵だけを出力させてください。ssh-add -L の最初の鍵を安易に選ばないでください。認証鍵、他のアカウントの鍵、または拔出されたハードウェアへの参照が混在している可能性があるためです。
4. ローカルでは成功したが、プラットフォームではまだ検証されない
順番に、署名公開鍵が登録されているか、所有権検証が完了しているか、コミット邮箱が対応するアカウントのものであるか,以及服务端のバージョン、署名検証設定、ページ上の表示を確認してください。実際の .pub 内容を固定して使用し、アルゴリズム名を手動で変更しないでください。
既存リポジトリが期待どおりに署名されていない場合は、設定元を確認できます。
git config --show-origin --get gpg.format
git config --show-origin --get user.signingkey
git config --show-origin --get commit.gpgsign
git config --show-origin --get gpg.ssh.allowedSignersFile
5. Rebase または一括再署名では繰り返し操作が必要になる
署名はコミットオブジェクトに紐づきます。Rebase や Amend などコミットオブジェクトを変更する操作では、新しいオブジェクトに再署名する必要があるため、PIN の入力やタッチ操作が複数回必要になる可能性があります。ハードウェアによる保護は秘密鍵の使用とエクスポートを制限しますが、コードレビューを代替するものでも、署名された内容に悪意がないことを証明するものでもありません。
無人で運用される CI では、専用のボット ID と制限付き資格情報を使用し、日常使用の个人用 YubiKey や人手によるタッチ操作が必要な YubiKey に依存してはいけません。
参考資料
- Git:git-config(SSH 署名鍵、信頼リスト、失効設定を含む)
- OpenSSH:ssh-keygen(FIDO 資格情報、リカバリー、署名オプションを含む)
- OpenSSH:ssh-add(常駐資格情報の読み込みと agent 環境を含む)
- OpenSSH:ssh(SSH_ASKPASS と SSH_ASKPASS_REQUIRE を含む)
- Yubico:SSH と FIDO2 を使用した Git 署名
- Yubico:FIDO2 SSH の設定とプラットフォーム要件
- Yubico:ykman FIDO コマンド
- Gitea:GPG/SSH コミット署名
- Gitea:SSH 鍵検証コマンドの説明に関する議論