GitHub CLI(ghコマンド)の拡張を最近作ってみて、ドキュメント通りに作成しリリースできたものの、バイナリがどこにどのようにアップロードされて、ユーザーにインストールする際には具体的にどのような挙動になるのかを理解したかったので調べた。
この記事では、gh-extension-precompile
がどのような処理を行い、バイナリがどこにどのようにアップロードされ、ユーザーがインストールする際にどのような挙動になるかを紹介する。
GitHub CLI拡張
https://docs.github.com/en/github-cli/github-cli/creating-github-cli-extensions
https://docs.github.com/en/github-cli/github-cli/using-github-cli-extensions
GitHub CLI拡張は、GitHub CLI(ghコマンド)の機能を拡張するためのカスタムコマンドを作成できる機能。
gh extension install <EXTENSION-NAME>
でインストールし、gh EXTENSION-NAME
で呼び出すことで、標準機能にない独自の処理を呼び出すことができる。
拡張機能を作る場合、方法として以下の2種類がある。
- インタプリタ型拡張: シェルスクリプトなどの実行可能ファイルを直接配置する。
gh extension create <EXTENSION-NAME>
で作成。 - プリコンパイル型拡張: コンパイルされたバイナリをリリースに添付する。
gh extension create --precompiled=<go or other> <EXTENSION-NAME>
で作成。
インタプリタ型拡張の場合には、ユーザーの環境依存の問題を減らすため、bashスクリプトが推奨されている。
プリコンパイル型拡張では、複数のOS・アーキテクチャ向けにバイナリをコンパイルする必要がある。これを手動で行うのを楽にするために、gh-extension-precompile
アクションが用意されている。
gh-extension-precompile の使い方
https://github.com/cli/gh-extension-precompile
gh-extension-precompile
は前述の通り、複数OS・アーキテクチャ向けのバイナリを一括で作成してassetsへアップロードしてくれるactions。
Goで書かれた拡張機能のケース
gh extension create --precompiled=go EXTENSION-NAME
で作成すると、Goベースのプロジェクトが自動で作成される。
この時、gh-extension-precompile
を利用した、リリース用のワークフローも自動で作成される。
name: release
on:
push:
tags:
- "v*"
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: cli/gh-extension-precompile@v2
with:
go_version_file: go.mod
これにより、タグを作成してプッシュするだけでリリースが自動化される。
git tag v1.0.0
git push origin v1.0.0
タグをプッシュすると自動的に以下が実行される。
- 複数のプラットフォーム向けにGoコードのクロスコンパイルを行う
- GitHubリリースを作成する
- コンパイルされたバイナリをリリースアセットとしてアップロードする
その他のコンパイル言語で書かれた拡張機能
Go以外の言語で拡張機能を実装する場合は、自前のビルドスクリプトを用意する必要がある。
- uses: cli/gh-extension-precompile@v2
with:
build_script_override: "script/build.sh"
gh-extension-precompile がやっていること
gh-extension-precompile
アクションの主な処理は、action.yml
とその中で実行される build_and_release.sh
シェルスクリプトで実装されている。下記のような動きとなる。
-
ビルド:
- https://github.com/cli/gh-extension-precompile/blob/561b19deda1228a0edf856c3325df87416f8c9bd/build_and_release.sh#L47-L77
- カスタムビルドスクリプトが指定されている場合はそれを実行する
- Goのデフォルトビルドの場合は、マトリックスビルドを実行して複数のOS・アーキテクチャ向けにクロスコンパイルする
- 各プラットフォーム向けに、命名規則に沿ったバイナリを生成する
-
チェックサム生成・署名(optional):
- https://github.com/cli/gh-extension-precompile/blob/561b19deda1228a0edf856c3325df87416f8c9bd/build_and_release.sh#L91-L95
- assetsに含まれるファイルの checksums.txt を生成
- detached署名により checksums.txt.sig を生成
-
リリース作成:
- https://github.com/cli/gh-extension-precompile/blob/561b19deda1228a0edf856c3325df87416f8c9bd/build_and_release.sh#L97-L103
- 既存のGitHub releaseがあるか確認し、なければ新規作成
- バイナリファイルをassetsとしてアップロードする
-
アーティファクト証明(optional):
- https://github.com/cli/gh-extension-precompile/blob/561b19deda1228a0edf856c3325df87416f8c9bd/action.yml#L113-L116
- ビルド証明書を生成し、サプライチェーン攻撃対策を強化する
チェックサムと署名の有効化
チェックサムファイルを一緒に公開することで、ファイルの改竄を防止できる。ユーザーはダウンロードしたファイルからチェックサムを計算し、公開されている値と比較することで、正しいバイナリであることを確認できる。
- id: import_gpg
uses: crazy-max/ghaction-import-gpg@v6
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
- uses: cli/gh-extension-precompile@v2
with:
gpg_fingerprint: ${{ steps.import_gpg.outputs.fingerprint }}
内部的には、checksums.txt
を出力して assets へ追加している他、gpg --output checksums.txt.sig --detach-sign checksums.txt
で出力された、checksum.sig.txt
もassetsへ追加される。
これにより、チェックサム自体が開発者によって正式に署名されたものであることを確認した上で、チェックサムを利用してバイナリが改竄されていないことを確認できる。
アーティファクト証明の有効化
generate_attestations: true
を指定することで、actions/attest-build-provenance
を利用して、アーティファクト証明を有効化できる。
https://github.com/actions/attest-build-provenance
permissions:
contents: write
id-token: write
attestations: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: cli/gh-extension-precompile@v2
with:
generate_attestations: true
actions/attest-build-provenance
は、GitHub ActionsのOIDCトークンから短期間の証明書を作成し、それを利用して改竄不可能な暗号署名を作成、証明書をGitHubのattestation apiへアップロードする。
これは、<OWNER>/<REPOSITORY>/attestations
で確認できる。
例: https://github.com/ntsk/gh-issue-bulk-create/attestations
ダウンロードしたバイナリが、この正しいRepositoryでビルドされたかを確認するには、gh attestation verify
コマンドを利用すると良い。
❯ gh attestation verify ~/Downloads/darwin-amd64 -R ntsk/gh-issue-bulk-create
Loaded digest sha256:5931396a28acd7b7a9541f9935aca49b76ef9e810353919b356406a9371b9ea9 for file:///Users/ntsk/Downloads/darwin-amd64
Loaded 1 attestation from GitHub API
The following policy criteria will be enforced:
- Predicate type must match:................ https://slsa.dev/provenance/v1
- Source Repository Owner URI must match:... https://github.com/ntsk
- Source Repository URI must match:......... https://github.com/ntsk/gh-issue-bulk-create
- Subject Alternative Name must match regex: (?i)^https://github.com/ntsk/gh-issue-bulk-create/
- OIDC Issuer must match:................... https://token.actions.githubusercontent.com
✓ Verification succeeded!
The following 1 attestation matched the policy criteria
- Attestation #1
- Build repo:..... ntsk/gh-issue-bulk-create
- Build workflow:. .github/workflows/release.yml@refs/tags/v1.1.0
- Signer repo:.... ntsk/gh-issue-bulk-create
- Signer workflow: .github/workflows/release.yml@refs/tags/v1.1.0
gh extension install はどこからバイナリをダウンロードするのか
ここまででリリースする方法は分かったが、そもそも、gh extension install
は具体的にどこからバイナリをダウンロードするのか。
cli/cli
リポジトリの ghコマンド本体の gh extension install
のコマンドについて確認する。
isBinExtension
関数でインタプリタ型かプリコンパイル(バイナリ)型かを特定、この時、GitHub releaseをチェックしてassetsがあればプリコンパイル型だと判定している。
インタプリタ型であれば、リポジトリ自体をcloneして利用し、プリコンパイル型であれば、installBin
関数にて、fetchLatestRelease
や fetchReleaseFromTag
を利用して最新バージョンをチェックした上で、プラットフォームに対応したバイナリを探し、ダウンロードするという動きになっているようだった。
まとめ
ghコマンドは、実際のユーザー相当の処理を実行できるためかなり権限が強い。
手順通りに作成すれば、extension自体は簡単に作れてしまうが、企業などで利用する場合、攻撃者が用意した悪意のある拡張などをインストールしてしまうと、private repositoryの情報が抜かれたりとリスクが伴う。
このため、どのようにダウンロード・インストールされ、どのように攻撃を回避・防止できるのか確認しておくと良いと考える。