From daa377f7c3a68ead94995162f4ed402e4bd5d58e Mon Sep 17 00:00:00 2001 From: "@permadeath.com" Date: Sun, 9 Aug 2026 20:35:24 -0400 Subject: [PATCH] feat(build): pin digests for the MegaMek and Suramadu archives All three archives now carry a SHA256 in versions.env, taken from a double fetch that agreed with itself and enforced the way JDK_SHA256 is: a cached file that fails is refetched, a fresh one that fails fails the build. The source tarball's digest rests on GitHub keeping tag-archive generation stable; if that moves, the build names the line to refresh. --- Dockerfile | 30 +++++++++++++++++------------- TODO.md | 12 ++++++------ scripts/build.sh | 3 +++ versions.env | 13 +++++++++++++ 4 files changed, 39 insertions(+), 19 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5139e64..0941d9c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -125,6 +125,8 @@ FROM base AS megamek ARG MEGAMEK_VERSION ARG MEGAMEK_RELEASE_DIR ARG MEGAMEK_SRC_DIR +ARG MEGAMEK_RELEASE_SHA256 +ARG MEGAMEK_SRC_SHA256 COPY --from=jre /opt/jdk /opt/jdk ENV JAVA_HOME=/opt/jdk @@ -134,28 +136,28 @@ ENV MM_SRC=/opt/megamek-src # Both tarballs: the release supplies the jar and data/, the source is what the # patchset applies against. # -# Upstream publishes no checksum for either, so integrity is "does it still -# decompress" rather than "is it the exact bytes we pinned". That catches the -# truncated-download case, which is the one that actually happens; pinning real -# digests is on TODO. +# Upstream publishes no checksum for either, so the digests in versions.env +# come from a verified fetch, and are enforced here the way the JDK's is: a +# cached file that fails is deleted and refetched, a fresh one that fails +# fails the build naming the mismatch. RUN --mount=type=cache,target=/tmp/deps,sharing=locked \ set -eu; \ fetch_tgz() { \ - f="$1"; url="$2"; \ - if [ -f "$f" ] && ! tar -tzf "$f" >/dev/null 2>&1; then \ - echo "cached $(basename "$f") is not a readable tarball; refetching"; rm -f "$f"; \ + f="$1"; url="$2"; sha="$3"; \ + if [ -f "$f" ] && ! echo "$sha $f" | sha256sum -c - >/dev/null 2>&1; then \ + echo "cached $(basename "$f") failed its checksum; refetching"; rm -f "$f"; \ fi; \ if [ ! -f "$f" ]; then \ echo "fetching $(basename "$f")"; \ curl -fsSL --retry 3 --retry-delay 2 -o "$f.part" "$url"; \ - tar -tzf "$f.part" >/dev/null; \ + echo "$sha $f.part" | sha256sum -c -; \ mv "$f.part" "$f"; \ else echo "using cached $(basename "$f")"; fi; \ }; \ rel="/tmp/deps/MegaMek-${MEGAMEK_VERSION}.tar.gz"; \ src="/tmp/deps/megamek-src-${MEGAMEK_VERSION}.tar.gz"; \ - fetch_tgz "$rel" "https://github.com/MegaMek/megamek/releases/download/v${MEGAMEK_VERSION}/MegaMek-${MEGAMEK_VERSION}.tar.gz"; \ - fetch_tgz "$src" "https://github.com/MegaMek/megamek/archive/refs/tags/v${MEGAMEK_VERSION}.tar.gz"; \ + fetch_tgz "$rel" "https://github.com/MegaMek/megamek/releases/download/v${MEGAMEK_VERSION}/MegaMek-${MEGAMEK_VERSION}.tar.gz" "${MEGAMEK_RELEASE_SHA256}"; \ + fetch_tgz "$src" "https://github.com/MegaMek/megamek/archive/refs/tags/v${MEGAMEK_VERSION}.tar.gz" "${MEGAMEK_SRC_SHA256}"; \ mkdir -p /opt/extract; \ tar xzf "$rel" -C /opt/extract; \ tar xzf "$src" -C /opt/extract; \ @@ -208,20 +210,22 @@ RUN rm -rf "$MM_SRC" \ # --------------------------------------------------------------------------- FROM base AS suramadu ARG SURAMADU_VERSION +ARG SURAMADU_SHA256 # The distribution ships no LICENSE file. We redistribute it, so the AGPL text # has to travel with it, and a failure to fetch that text fails the build. +# The zip is pinned like every other archive; see versions.env. RUN --mount=type=cache,target=/tmp/deps,sharing=locked \ set -eu; \ zip="/tmp/deps/suramadu-${SURAMADU_VERSION}.zip"; \ - if [ -f "$zip" ] && ! unzip -qt "$zip" >/dev/null 2>&1; then \ - echo "cached suramadu zip is corrupt; refetching"; rm -f "$zip"; \ + if [ -f "$zip" ] && ! echo "${SURAMADU_SHA256} $zip" | sha256sum -c - >/dev/null 2>&1; then \ + echo "cached suramadu zip failed its checksum; refetching"; rm -f "$zip"; \ fi; \ if [ ! -f "$zip" ]; then \ echo "fetching suramadu-${SURAMADU_VERSION}.zip"; \ curl -fsSL --retry 3 --retry-delay 2 -o "$zip.part" \ "https://github.com/manticore-projects/suramadu/releases/download/${SURAMADU_VERSION}/suramadu-${SURAMADU_VERSION}.zip"; \ - unzip -qt "$zip.part" >/dev/null; \ + echo "${SURAMADU_SHA256} $zip.part" | sha256sum -c -; \ mv "$zip.part" "$zip"; \ else echo "using cached suramadu-${SURAMADU_VERSION}.zip"; fi; \ mkdir -p /opt/suramadu; \ diff --git a/TODO.md b/TODO.md index b316930..e3f4e4c 100644 --- a/TODO.md +++ b/TODO.md @@ -14,12 +14,12 @@ the finish is supposed to leave behind. module: without it Jetty bound the port and then threw on every accept(), which presents as a network fault. `tests/module-scan.py` reports what is referenced but absent. -- [ ] **Pin digests for MegaMek and Suramadu.** Only the JDK has a - `sha256` in `versions.env`; upstream publishes none for the other three - archives, so the Dockerfile checks "does it still decompress" instead. - That catches a truncated download — which is the failure that actually - happened — but not a substituted one. Take the digests from a known-good - fetch and enforce them the way `JDK_SHA256` is enforced. +- [x] **Pin digests for MegaMek and Suramadu.** All three archives now carry + a `*_SHA256` in `versions.env`, taken from a double fetch that agreed + with itself and enforced in the Dockerfile the way `JDK_SHA256` is. + The source tarball's digest rests on GitHub keeping tag-archive + generation stable, which it has committed to; if that ever moves, the + build fails naming the line to refresh. - [ ] **The tangled workflow has never run.** `.tangled/workflows/build.yml` is written to the documented schema but no runner is attached, so the schema is unverified. It also assumes the runner provides a Docker daemon. diff --git a/scripts/build.sh b/scripts/build.sh index 4d25dfb..70d3adf 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -41,8 +41,11 @@ docker build \ --build-arg "MEGAMEK_VERSION=$MEGAMEK_VERSION" \ --build-arg "MEGAMEK_RELEASE_DIR=$MEGAMEK_RELEASE_DIR" \ --build-arg "MEGAMEK_SRC_DIR=$MEGAMEK_SRC_DIR" \ + --build-arg "MEGAMEK_RELEASE_SHA256=$MEGAMEK_RELEASE_SHA256" \ + --build-arg "MEGAMEK_SRC_SHA256=$MEGAMEK_SRC_SHA256" \ --build-arg "SURAMADU_VERSION=$SURAMADU_VERSION" \ --build-arg "SURAMADU_WAR=$SURAMADU_WAR" \ + --build-arg "SURAMADU_SHA256=$SURAMADU_SHA256" \ --build-arg "BUILD_REF=$BUILD_REF" \ --target runtime \ --tag "$IMAGE" \ diff --git a/versions.env b/versions.env index fb8c1bc..7790fd5 100644 --- a/versions.env +++ b/versions.env @@ -50,6 +50,16 @@ MEGAMEK_VERSION="0.51.0" MEGAMEK_RELEASE_DIR="MegaMek-0.51.00" # upstream's tarball has a trailing 0 MEGAMEK_SRC_DIR="megamek-0.51.0" +# Upstream publishes no digest for either tarball, so these come from a +# double fetch that agreed with itself (2026-08-09) and are enforced the way +# JDK_SHA256 is. The release asset's bytes are fixed at publish. The source +# tarball is generated from the tag on demand - GitHub has committed to +# keeping that generation stable, but if it ever changes the build fails +# naming this line, and the fix is a fetch you trust and a new digest here. +# Bumping MEGAMEK_VERSION: refresh both from such a fetch. +MEGAMEK_RELEASE_SHA256="5d673cdaca0ead0e58d6e74db5b2d1e06bc25ccb5caaf332dad71624f6487b85" +MEGAMEK_SRC_SHA256="58847cf8413f67194036296d7b33493233cf5316c0ab5f89aaa8451e69c88ee3" + # --- Suramadu --------------------------------------------------------------- # The AGPL-3.0 fork by manticore-projects of Webswing v20.2.5, the last release # under that licence. Renamed from "Webswing Lite" to Suramadu in 26.4.6; see @@ -57,6 +67,9 @@ MEGAMEK_SRC_DIR="megamek-0.51.0" # config files and the frontend bundles are all suramadu-* from 26.4.6 on. SURAMADU_VERSION="26.4.7" SURAMADU_WAR="suramadu-server-26.4.7.war" +# Same story as MegaMek's: no published digest, so this is from the same +# verified double fetch, of a release asset whose bytes are fixed at publish. +SURAMADU_SHA256="4fbf166d633493669e1545b6427fa87c40c826a63a18b559e5487d9a95c2cd79" # --- async-profiler --------------------------------------------------------- # Apache-2.0. Staged in the image but never loaded unless ARENA_PROFILE is set; -- 2.51.2