No description
  • Makefile 52.5%
  • Dockerfile 38.3%
  • Shell 9.2%
Find a file
2026-10-05 22:51:02 -04:00
bootstrap.sh init commit 2026-10-05 22:25:42 -04:00
Containerfile init commit 2026-10-05 22:25:42 -04:00
Makefile init commit 2026-10-05 22:25:42 -04:00
README.md README added 2026-10-05 22:51:02 -04:00

Go

Builds an official Go release from source, starting from a C compiler, and checks the result against the tarball published on go.dev.

The idea is to avoid trusting a prebuilt Go binary. Go is written in Go, so building it normally requires an earlier Go. This project follows the chain back to Go 1.4, the last release whose toolchain was written in C, and builds forward from there inside podman. If the tarball that comes out has the same SHA-256 as the one Google publishes, the official binary contains exactly what the source produces.

The chain

C compiler (Alpine gcc)
  -> Go 1.4.3
  -> Go 1.17.13
  -> Go 1.20.14
  -> Go 1.22.12
  -> Go 1.24.6
  -> Go 1.27.1

Each Go release is built by the one before it. Go 1.24.6 is used because Go 1.27 needs a bootstrap toolchain of 1.24.6 or newer. Check go.dev/doc/install/source if that requirement changes.

Requirements

  • podman
  • make
  • network access, for the Alpine packages and the Go source tarballs
  • an amd64 machine, or an emulated one. Every stage is pinned to linux/amd64, and Go 1.4 has no arm64 port. On arm64 hardware the build runs under emulation and is much slower.

The first build is slow and the stage images are large, because each stage carries a full Go tree.

Usage

make all

This builds the chain, copies the release archives out of the image into ./distpack, and compares the tarball hash to the official one.

Individual targets:

Target What it does
make build Build the image with podman
make extract Copy the archives out of the image into ./distpack
make verify Compare distpack/go1.27.1.linux-amd64.tar.gz to the official hash
make clean Remove ./distpack
make clean-image Remove the image and prune dangling images
make help List targets

Variables can be overridden on the command line, for example make OUT_DIR=out all. The main ones are IMAGE_NAME, GO_VERSION, GO_PLATFORM, OUT_DIR, OFFICIAL_SHA256, and the GO*_SHA256 checksums for each source tarball.

A successful verify looks like this:

built:    63d339f0da5ab53635a56f2490a7984dfe12dfcff22ad749f63edaf590168445
official: 63d339f0da5ab53635a56f2490a7984dfe12dfcff22ad749f63edaf590168445
MATCH: go1.27.1.linux-amd64.tar.gz is bit-for-bit identical to the official release.

make verify exits with an error on a mismatch. make extract deletes ./distpack before copying, and make stops at the first failing step, so an old tarball can never be verified by accident.

Files

  • Containerfile: one stage per Go version, plus a shared base stage with the C compiler.
  • bootstrap.sh: downloads a Go source tarball, checks its SHA-256, and runs make.bash.
  • Makefile: wraps the build, extraction, and verification.

Decisions worth knowing about

The final stage has no C compiler and does not set CGO_ENABLED. The earlier stages build with CGO_ENABLED=0, which is fine because their output is only used as a bootstrap. The final stage has to match the official build exactly. During make.bash, Go writes src/internal/buildcfg/zbootstrap.go, which records the CGO_ENABLED value from the build environment. The official build leaves it empty. Setting it to 0 changes that one constant, and because nearly every package in the toolchain imports internal/buildcfg, it changes every build ID and every hash. An earlier version of this project hit exactly that.

Go 1.4 is built with -std=gnu17. Alpine 3.22 ships GCC 15, which defaults to C23. In C23 bool is a keyword, and Go 1.4 contains typedef int bool;. The Go 1.4 stage sets CC="gcc -std=gnu17" to avoid the error. This only affects the throwaway C tools in that stage.

-Werror is removed from make.bash. Go 1.4 builds its C tools with -Wall -Werror, which fails on warnings that newer compilers add. bootstrap.sh strips the flag before building. This runs in every stage, not only the Go 1.4 one. For later Go versions it should do nothing, and the final hash match shows it had no effect on the result, but it is a modification to upstream files and worth knowing about.

GOTOOLCHAIN=local. This stops the go command from downloading a toolchain on its own during the build.

-distpack. The final make.bash run writes release-style archives to pkg/distpack. These are the files compared against the official downloads.

Pinned base image and packages. The Alpine image is pinned by digest and the apk packages by version, so the build environment does not drift between runs.

What a match shows, and what it does not

A match shows that the official go1.27.1.linux-amd64.tar.gz is what the 1.27.1 source produces when built through a chain that never used a Go binary from Google. A compromised official binary would have to also match a build that started from C, which is very unlikely.

It does not show:

  • That the source tarball matches what the Go maintainers committed. To check that, clone the repository at the go1.27.1 tag and diff it against go1.27.1.src.tar.gz. This project does not do that step.
  • That the C side is clean. Alpine's GCC, musl, and the base image are still trusted binaries. They come from a different lineage than Go, which is the point, but they were not built from scratch. A full-source bootstrap like the one Guix does goes further.
  • That the source is free of malicious changes that went through review. This catches tampering with the build, not a bad commit.
  • Anything about other platforms. Only linux-amd64 is covered.

The SHA-256 values for the source tarballs and the official release are copied from go.dev/dl. Cross-check them against a second source, since a verification script is only as good as the values it compares against.

If the hash does not match

A mismatch does not automatically mean tampering. The usual cause is a difference in build settings. To narrow it down, extract both tarballs and compare:

go tool buildid official/go/bin/go
go tool buildid mine/go/bin/go
diff -rq official/go/src mine/go/src
go version -m official/go/bin/go
go version -m mine/go/bin/go

If the src trees differ only in internal/buildcfg/zbootstrap.go, look at the diff in that file. It shows which build setting differed. If the compiler binaries in pkg/tool/linux_amd64 have matching build IDs and bin/go does not, the difference is further up. The Go team's gorebuild tool in golang.org/x/build follows the official release process exactly and is a good second opinion.

Updating to a new Go version

  1. Update the version and checksum for the new release in the Containerfile and the Makefile (GO_VERSION, GO127_SHA256, OFFICIAL_SHA256). The final stage in the Containerfile has the version hardcoded in its bootstrap.sh call, and the Makefile's GO_VERSION only affects the tarball name used for verification.
  2. If the new release needs a newer bootstrap toolchain, add a stage to the chain, or bump the 1.24 stage.
  3. Run make all and compare.

If a pinned apk package disappears from the Alpine repositories, the build will fail at the package install step. Update the pins, or move to a newer pinned base image.