Fetch Alpine.js at build time under a verified hash (closes #145)
All checks were successful
check / check (push) Successful in 3m29s

static/js/alpine.min.js was a committed minified bundle: unreviewable,
referenced by no hash, and forbidden by REPO_POLICIES.md on both counts.

It is now fetched by script/fetch-assets from a pinned npm registry
tarball, with the tarball sha256 and the extracted file's sha256 both
hardcoded and checked before anything is installed. The hash of every
installed asset lives in static/vendor.sha256, and static/vendor_test.go
re-hashes the bytes go:embed actually put in the binary against that
manifest, so the pin is enforced on what ships rather than merely
recorded. .gitignore keeps the artifact out of the repo and
.dockerignore keeps a host copy out of the build context, so the image
can only get it by fetching and verifying it.

Alpine 3.14.9 is byte-identical to the blob that was committed
(3ed1eed252488921df65e363d6715deb04d7f92aaedb9e52199fdf73cb1e0ad3), so
the served asset does not change; internal/server/static_assets_test.go
fetches every /s/ script base.html loads through the real router to
prove the page still gets it.
This commit is contained in:
clawbot
2026-08-17 20:53:06 +00:00
committed by sneak
parent 279effb4c2
commit b8272e158c
11 changed files with 302 additions and 9 deletions

92
static/vendor_test.go Normal file
View File

@@ -0,0 +1,92 @@
package static_test
import (
"bufio"
"crypto/sha256"
"encoding/hex"
"os"
"strings"
"testing"
"github.com/stretchr/testify/require"
"sneak.berlin/go/webhooker/static"
)
const manifestPath = "vendor.sha256"
// fetchHint is appended to every failure here: the assets the manifest
// covers are fetched by the build, not committed, so a fresh clone that
// has not run script/fetch-assets fails this test and should be told why.
const fetchHint = "run `script/fetch-assets` (or `make assets`) to install " +
"the pinned third-party assets"
// TestVendoredAssetsMatchManifest asserts that every asset listed in
// static/vendor.sha256 is embedded in the binary with exactly the pinned
// bytes. script/fetch-assets verifies the same hashes at download time;
// this test verifies them again on what actually ships, so a build that
// skipped, cached, or subverted the fetch cannot produce a binary serving
// unpinned third-party JavaScript.
func TestVendoredAssetsMatchManifest(t *testing.T) {
t.Parallel()
entries := readManifest(t)
require.NotEmpty(t, entries, "%s lists no assets", manifestPath)
for path, want := range entries {
t.Run(path, func(t *testing.T) {
t.Parallel()
data, err := static.Static.ReadFile(path)
require.NoErrorf(
t, err,
"%s is listed in %s but is not embedded; %s",
path, manifestPath, fetchHint,
)
sum := sha256.Sum256(data)
got := hex.EncodeToString(sum[:])
require.Equalf(
t, want, got,
"embedded %s does not match its pinned sha256 in %s; %s",
path, manifestPath, fetchHint,
)
})
}
}
// readManifest parses static/vendor.sha256, which is in sha256sum(1)
// format with paths relative to static/.
func readManifest(t *testing.T) map[string]string {
t.Helper()
f, err := os.Open(manifestPath)
require.NoError(t, err, "opening %s", manifestPath)
defer func() { require.NoError(t, f.Close()) }()
entries := make(map[string]string)
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
fields := strings.Fields(line)
require.Lenf(
t, fields, 2,
"%s: malformed entry %q, want \"<sha256> <path>\"",
manifestPath, line,
)
sum, path := fields[0], fields[1]
require.Lenf(t, sum, 64, "%s: %q is not a sha256", manifestPath, sum)
entries[path] = sum
}
require.NoError(t, scanner.Err(), "reading %s", manifestPath)
return entries
}