前置き
以前、BuildInfoからバージョンを取得する方法を紹介しました。
go install
で正規の公開されたバージョンをインストールした場合は、以下の出力においてmodの行が示すように、sum.golang.orgでチェックサム等が検証されてバイナリのメタデータに埋め込まれます。
$ go version -m dotsync dotsync: go1.22.2 path github.com/lufia/dotsync mod github.com/lufia/dotsync v0.0.2 h1:JWm92Aw8pSKJ4eHiQZIsE/4rgwk3h5CjEbJ/S30wiOU= build -buildmode=exe build -compiler=gc build -trimpath=true build DefaultGODEBUG=httplaxcontentlength=1,httpmuxgo121=1,panicnil=1,tls10server=1,tlsrsakex=1,tlsunsafeekm=1 build CGO_ENABLED=0 build GOARCH=amd64 build GOOS=linux build GOAMD64=v1
上記の出力から「dotsyncのバージョン0.0.2をgo1.22.2でビルドした」ことを読み取れますね。チェックサムは https://sum.golang.org/lookup/<module-path>@<version>
のようなURLにアクセスすると、正規のものかどうかの確認を行えます。完全なURLの仕様はGo Modules Reference/Checksum databaseをみてください。
このチェックサムが一度でも登録されてしまった後は、消したり変更したりできません。消せないのは困ると思うかもしれませんが、proxy.golang.orgに
I removed a bad release from my repository but it still appears in the mirror, what should I do?
への回答があるので、不備などではなく意図してデザインされていることが読み取れます。削除はできないものの、Goで非推奨(Deprecated)や撤回(Retracted)を明示する方法のようにすれば意思を表明することは可能です。
正しくソースコードからビルドされたことを検証する
主に以下の条件を満たす*1場合、Go 1.21以降では生成するバイナリが完全に一致するので、同じパラメータを与えて手元でビルドしてみると検証できます。
- Goコンパイラのバージョンが同じ
- GOOS, GOARCH, GOAMD64 などターゲットが同じ
- cgoを使わない
- os/user, netなどで動的リンクをしない
- ビルドするディレクトリ名が同じ、または
-trimpath
オプションを与える
以下の例はlegoコマンドをLinuxとPlan 9でビルドしたものですが、同じハッシュ値になっている様子が分かると思います。
Linuxでビルド
$ go version go version go1.22.2 linux/amd64 $ export GOTOOLCHAIN=go1.22.1 $ export GOOS=plan9 $ export GOARCH=amd64 $ export GOAMD64=v1 $ export CGO_ENABLED=0 $ go install -trimpath github.com/go-acme/lego/v4/cmd/lego@v4.16.1 $ sha1sum lego ee2e9c121604c1f52cb53c0d0824288d772de1e7
Plan 9でビルド
% go version go version go1.22.1 plan9/386 % GOTOOLCHAIN=go1.22.1 % GOOS=plan9 % GOARCH=amd64 % GOAMD64=v1 % CGO_ENABLED=0 % go install -trimpath github.com/go-acme/lego/v4/cmd/lego@v4.16.1 % sha1sum lego ee2e9c121604c1f52cb53c0d0824288d772de1e7
再現可能なビルド
このように、第三者が特定のソースコードから生成されたものであると検証できるような概念は「再現可能なビルド」とか「再現性のあるビルド」と呼ばれるようです。
また、Go 1.21以降は、Goのコンパイラやライブラリも再現可能になっているようです。
コード署名とは違うのか
コード署名は、誰がビルドしたものなのかを検証できますが、特定のソースコードから生成されたものかどうかは保証しません。例えばビルドプロセスの途中で改ざんが行われた場合、コードの署名は正しく検証を通ってしまいます。
go buildの場合は正規のバージョンが入らない
ようやく本題です。この記事の冒頭で挙げたエントリでも書いたように、手元にソースコードを置いてgo build
した場合などでは、メインモジュールのバージョンやチェックサムが埋め込まれずに (devel) という文字列になります。
$ go version -m dotsync dotsync: go1.22.2 path github.com/lufia/dotsync mod github.com/lufia/dotsync (devel) ...
(devel) の代わりに(おそらくv1.0.1-0.20240418xxxのような)疑似バージョンを埋め込む提案がcmd/go: stamp the pseudo-version in builds generated by go buildで承認されていますが、それでも正規のバージョンとは区別されていますし、少なくともGo 1.22の時点ではまだ実装されていません。
GoReleaserでビルドしたバイナリはメインモジュールのチェックサムを持たない
上記と同様に、2024年5月時点では、GoReleaserやGoReleaser Actionでビルドしたバイナリは公開された正規なバージョンを持ちません。
実用上は致命的に困るものではないけれど、どうせなら検証可能になっていたほうが嬉しいですね。
GitHub Releasesでリリースしたらビルドして成果物に追加するワークフロー
というわけで、正規のバージョンが埋め込まれたバイナリをリリースするためのワークフローを作ってみました。以下のワークフローは、GitHub Releasesで新しいバージョンをPublishすると開始して、最終的にバイナリをリリースに添付します。
name: Release on: release: types: - published jobs: release: strategy: matrix: os: - linux - darwin - windows arch: - amd64 - arm64 include: - format: tgz - os: windows format: zip exclude: - os: darwin arch: amd64 - os: windows arch: arm64 runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: stable - name: Build the package uses: lufia/workflows/.github/actions/go-install@v0.4.0 with: package-path: github.com/lufia/dotsync version: ${{ github.ref_name }} env: GOOS: ${{ matrix.os }} GOARCH: ${{ matrix.arch }} CGO_ENABLED: 0 id: build - name: Create the asset consists of the build artifacts uses: lufia/workflows/.github/actions/upload-asset@v0.4.0 with: tag: ${{ github.ref_name }} path: > ${{ steps.build.outputs.target }} LICENSE README.md name: dotsync-${{ github.ref_name }}.${{ matrix.os }}-${{ matrix.arch }} format: ${{ matrix.format }} upload: needs: release permissions: contents: write runs-on: ubuntu-22.04 steps: - uses: actions/download-artifact@v4 with: path: assets merge-multiple: true - name: Upload the assets to the release run: gh release upload -R "$GITHUB_REPOSITORY" "$GITHUB_REF_NAME" assets/* env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
どうでしょうか。記述量は多いですが、1/3くらいはビルド用のマトリクスを作っているところなので、第一印象ほど複雑ではないかなと思います。
ワークフローの途中で読んでいる複合アクションは以下の2つなので、興味があれば眺めてみてください。
*1:他にもあるかもしれないけど、これだけ揃えればだいたい同じになるはず