Skip to content

Commit ab0d6b1

Browse files
CryptoFewkaclaude
andcommitted
harden the app-artifact build: hash-pin wheels, split build from publish
Two supply-chain hardenings for build-app.yml: 1. Hash-pin the bundled PyPI wheels. The .deb and AppImage previously pip-installed version-pinned wheels with no integrity check, so a mutated wheel of the same version would be accepted. Move the curated closures into third_party/requirements-{deb,appimage}.txt with a SHA-256 per wheel and install with --require-hashes --only-binary=:all: --no-deps, so pip rejects any mismatch and never builds from an sdist. 2. Least-privilege CI. The build job held contents:write + id-token + attestations while running build scripts and downloading deps. Drop it to contents:read, upload the artifacts, and attest/publish them from a separate job that holds the elevated scopes and touches only the already-built files. Update the packaging policy tests to assert the hardened install flags and that the requirement files carry both the version pins and the hashes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014pmkUWS6teoMZUUnMJG7Gr
1 parent 2bd48eb commit ab0d6b1

6 files changed

Lines changed: 120 additions & 17 deletions

File tree

.github/workflows/build-app.yml

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,13 @@ permissions:
4242
contents: read
4343

4444
jobs:
45+
# Build with least privilege: read-only token, no attest/publish scopes. The
46+
# artifacts are handed to the elevated `publish` job below, so nothing that
47+
# runs build scripts / downloads deps ever holds a write or signing token.
4548
build:
4649
runs-on: ubuntu-24.04
4750
permissions:
48-
contents: write
49-
id-token: write
50-
attestations: write
51+
contents: read
5152
env:
5253
GH_TOKEN: ${{ github.token }}
5354
APPIMAGE_EXTRACT_AND_RUN: "1"
@@ -128,8 +129,41 @@ jobs:
128129
scripts/build-release.sh
129130
echo "VER=$(grep -m1 '^VERSION = ' bol/config.py | cut -d'"' -f2)" >> "$GITHUB_ENV"
130131
132+
- name: Upload the built artifacts for the publish job
133+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
134+
with:
135+
name: app-artifacts
136+
if-no-files-found: error
137+
retention-days: 1
138+
path: |
139+
dist/bedrock-on-linux_*.deb
140+
dist/bedrock-on-linux-*.pyz
141+
dist/BedrockOnLinux-*-x86_64.AppImage
142+
dist/BedrockOnLinux-*-x86_64.flatpak
143+
dist/BedrockOnLinux-*-SHA256SUMS
144+
145+
# Attest + publish only. Runs on tag push or an explicit publish request, holds
146+
# the write/id-token/attestations scopes the build job does not, and touches
147+
# only the already-built artifacts (never build scripts or third-party deps).
148+
publish:
149+
needs: build
150+
if: ${{ github.event_name == 'push' || inputs.publish }}
151+
runs-on: ubuntu-24.04
152+
permissions:
153+
contents: write
154+
id-token: write
155+
attestations: write
156+
env:
157+
GH_TOKEN: ${{ github.token }}
158+
steps:
159+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
160+
- name: Download the built artifacts
161+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
162+
with:
163+
name: app-artifacts
164+
path: dist
165+
131166
- name: Attest application artifacts
132-
if: ${{ github.event_name == 'push' || inputs.publish }}
133167
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2
134168
with:
135169
subject-path: |
@@ -140,7 +174,6 @@ jobs:
140174
dist/BedrockOnLinux-*-SHA256SUMS
141175
142176
- name: Compute release metadata + bill of materials
143-
if: ${{ github.event_name == 'push' || inputs.publish }}
144177
run: |
145178
ver="$(grep -m1 '^VERSION = ' bol/config.py | cut -d'"' -f2)"
146179
eng_rev="$(grep -m1 '^WINEGDK_BUILD_REV = ' bol/config.py | cut -d'"' -f2)"
@@ -173,7 +206,7 @@ jobs:
173206
} >> "$GITHUB_ENV"
174207
175208
- name: Roll the nightly tag to the current commit
176-
if: ${{ (github.event_name == 'push' || inputs.publish) && env.RELEASE_TAG == 'nightly' }}
209+
if: ${{ env.RELEASE_TAG == 'nightly' }}
177210
run: |
178211
# softprops does not move an existing tag, so delete the rolling nightly
179212
# release + tag first; the publish step recreates it at the current commit
@@ -185,7 +218,6 @@ jobs:
185218
fi
186219
187220
- name: Publish the application release
188-
if: ${{ github.event_name == 'push' || inputs.publish }}
189221
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
190222
with:
191223
tag_name: ${{ env.RELEASE_TAG }}

scripts/build-appimage.sh

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -134,12 +134,11 @@ for library in "$PYLIB/libtcl8.6.so" "$PYLIB/libtk8.6.so"; do
134134
done
135135

136136
echo "== installing portable cryptography + certifi + customtkinter + python-xlib into the bundle"
137+
# Hash-pinned, wheels only, no sdist builds: the closure + SHA-256s live in
138+
# third_party/requirements-appimage.txt (--require-hashes rejects any mismatch).
137139
"$PYBIN" -m pip install --no-cache-dir --no-compile \
138-
--no-deps \
139-
'cryptography==43.0.3' 'certifi==2026.6.17' \
140-
'cffi==2.0.0' 'pycparser==3.0' \
141-
'customtkinter==5.2.2' 'darkdetect==0.8.0' 'packaging==26.2' \
142-
'python-xlib==0.33' 'six==1.17.0' \
140+
--no-deps --require-hashes --only-binary=:all: \
141+
-r "$SRC/third_party/requirements-appimage.txt" \
143142
>/dev/null
144143

145144
PY3="$PYLIB/python3.12"

scripts/build-deb.sh

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,12 @@ cp -r "$SRC/bol" "$PKG/usr/lib/bedrock-on-linux/bol"
3131
# customtkinter's theme/font assets load fine. cryptography/tk stay apt deps.
3232
# python-xlib (+ six) is bundled the same way for bol.x11's primary-monitor
3333
# lookup; it's optional (bol.x11 falls back to the xrandr CLI without it).
34-
python3 -m pip install --quiet --no-cache-dir --no-compile --no-deps --target \
34+
# Hash-pinned, wheels only, no sdist builds: closure + SHA-256s live in
35+
# third_party/requirements-deb.txt (--require-hashes rejects any mismatch).
36+
python3 -m pip install --quiet --no-cache-dir --no-compile --no-deps \
37+
--require-hashes --only-binary=:all: --target \
3538
"$PKG/usr/lib/bedrock-on-linux" \
36-
'customtkinter==5.2.2' 'darkdetect==0.8.0' 'packaging==26.2' \
37-
'python-xlib==0.33' 'six==1.17.0'
39+
-r "$SRC/third_party/requirements-deb.txt"
3840
rm -rf "$PKG/usr/lib/bedrock-on-linux"/bin 2>/dev/null || true
3941
find "$PKG/usr/lib/bedrock-on-linux" -name __pycache__ -type d -exec rm -rf {} +
4042
for metadata in \

tests/test_application_packaging.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,30 @@ def test_appimage_is_relocatable_licensed_and_version_pinned(self):
2828
self.assertIn('rm -f "$DYN"/_crypt.*.so', script)
2929
self.assertIn('"libcrypt.so.1", "libXss.so.1"', script)
3030
self.assertIn('runtime is not statically linked', script)
31+
# Wheels are hash-pinned, binary-only, no sdist builds; the pinned
32+
# closure lives in the requirements file the script installs from.
33+
self.assertIn("--require-hashes --only-binary=:all:", script)
34+
self.assertIn("third_party/requirements-appimage.txt", script)
35+
reqs = (ROOT / "third_party/requirements-appimage.txt").read_text(
36+
encoding="utf-8")
3137
for requirement in (
3238
"cryptography==43.0.3", "cffi==2.0.0", "pycparser==3.0",
3339
"customtkinter==5.2.2", "darkdetect==0.8.0",
3440
"packaging==26.2"):
35-
self.assertIn(requirement, script)
41+
self.assertIn(requirement, reqs)
42+
self.assertIn("--hash=sha256:", reqs)
3643

3744
def test_deb_preserves_dependency_licenses_and_normalizes_modes(self):
3845
script = (ROOT / "scripts/build-deb.sh").read_text(encoding="utf-8")
46+
self.assertIn("--require-hashes --only-binary=:all:", script)
47+
self.assertIn("third_party/requirements-deb.txt", script)
48+
reqs = (ROOT / "third_party/requirements-deb.txt").read_text(
49+
encoding="utf-8")
3950
for requirement in (
4051
"customtkinter==5.2.2", "darkdetect==0.8.0",
4152
"packaging==26.2"):
42-
self.assertIn(requirement, script)
53+
self.assertIn(requirement, reqs)
54+
self.assertIn("--hash=sha256:", reqs)
4355
self.assertNotIn('*.dist-info', script)
4456
self.assertIn("usr/share/doc/bedrock-on-linux/copyright", script)
4557
self.assertIn("-iname 'LICENSE*'", script)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Hash-pinned wheel closure bundled into the AppImage (scripts/build-appimage.sh).
2+
# Install with: pip install --require-hashes --only-binary=:all: --no-deps -r <this>
3+
# --require-hashes every wheel must match a pin below (no silent substitution)
4+
# --only-binary=:all: never build from an sdist (no arbitrary setup.py at build)
5+
# --no-deps the list is the full, curated runtime closure; do not resolve more
6+
# Target interpreter: the bundled CPython 3.12 on the glibc-2.31 (Bullseye) base.
7+
# Regenerate the hashes from PyPI (pypi.org/pypi/<pkg>/<ver>/json) when a version
8+
# bumps; keep versions in step with build-deb.sh and the Flatpak manifest.
9+
10+
# cryptography signs the MSA login; kept <44 for the glibc-2.31 baseline. abi3
11+
# wheels, so one set covers every CPython >= 3.7 on this base.
12+
cryptography==43.0.3 \
13+
--hash=sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f \
14+
--hash=sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18 \
15+
--hash=sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73 \
16+
--hash=sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4 \
17+
--hash=sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405 \
18+
--hash=sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73 \
19+
--hash=sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83 \
20+
--hash=sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa
21+
22+
certifi==2026.6.17 \
23+
--hash=sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db
24+
25+
# cffi/pycparser back cryptography's FFI; cffi is compiled, so its wheels are
26+
# CPython-3.12 + linux/x86_64 specific.
27+
cffi==2.0.0 \
28+
--hash=sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba \
29+
--hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187
30+
pycparser==3.0 \
31+
--hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992
32+
33+
# GUI toolkit + X11 primary-monitor lookup (pure Python).
34+
customtkinter==5.2.2 \
35+
--hash=sha256:14ad3e7cd3cb3b9eb642b9d4e8711ae80d3f79fb82545ad11258eeffb2e6b37c
36+
darkdetect==0.8.0 \
37+
--hash=sha256:a7509ccf517eaad92b31c214f593dbcf138ea8a43b2935406bbd565e15527a85
38+
packaging==26.2 \
39+
--hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e
40+
python-xlib==0.33 \
41+
--hash=sha256:c3534038d42e0df2f1392a1b30a15a4ff5fdc2b86cfa94f072bf11b10a164398
42+
six==1.17.0 \
43+
--hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274

third_party/requirements-deb.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Hash-pinned wheels bundled into the .deb (scripts/build-deb.sh), installed with
2+
# --require-hashes --only-binary=:all: --no-deps into the package tree. These are
3+
# the pure-Python GUI/X11 helpers Debian does not package; cryptography and Tk
4+
# stay apt dependencies (see build-deb.sh). Keep versions in step with
5+
# requirements-appimage.txt; regenerate hashes from PyPI when a version bumps.
6+
customtkinter==5.2.2 \
7+
--hash=sha256:14ad3e7cd3cb3b9eb642b9d4e8711ae80d3f79fb82545ad11258eeffb2e6b37c
8+
darkdetect==0.8.0 \
9+
--hash=sha256:a7509ccf517eaad92b31c214f593dbcf138ea8a43b2935406bbd565e15527a85
10+
packaging==26.2 \
11+
--hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e
12+
python-xlib==0.33 \
13+
--hash=sha256:c3534038d42e0df2f1392a1b30a15a4ff5fdc2b86cfa94f072bf11b10a164398
14+
six==1.17.0 \
15+
--hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274

0 commit comments

Comments
 (0)