Files
claudeandbit 3f14c01b00 Build the shared F-Droid publishing tooling: sign, index, publish
Adds sign-apk.sh (align-check + apksigner), fdroid-update.sh (fdroid
update over a binary repo, signing the index with its own separate
key), publish.sh (S3-compatible upload) and the composite action
tying them together, matching weblib-ci's shape.

zipalign is not a standalone nixpkgs attribute; resolved by measurement
rather than assumption that Gradle/AGP already aligns its output (Android's
own docs, corroborated by fdroidserver never shelling out to zipalign) and
added check_zip_alignment.py as the safety net, since apksigner itself does
not check alignment at all. nixpkgs is pinned via a committed nixpkgs-pin
revision, since this repo has no flake.lock of its own for with-nixpkgs.sh
to read (fdroid-repo-ci#1, weblib-ci#9) -- a missing or malformed pin fails
loudly rather than degrading to an unpinned nix shell.

No consumer repo, bucket, hostname or channel name is hard-coded: which
repo holds the keys and runs the publish has already changed once during
this issue and may again, so every example uses a placeholder.

42 offline/local-only test assertions across five suites (run_tests.sh),
including fault injection on the alignment check, the password/argument
validation, and the with-nixpkgs.sh fail-loud paths.

Co-authored-by: bit <bit@das-labor.org>
2026-09-09 06:34:36 +00:00

92 lines
4.0 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# End-to-end test of sign-apk.sh: not just check_zip_alignment.py in
# isolation, but the whole script as a caller would invoke it -- a throwaway
# keystore generated fresh in a temp dir, a fixture APK, the real `apksigner`
# resolved through with-nixpkgs.sh/nixpkgs-pin.
#
# Needs the toolchain with-nixpkgs.sh pins (apksigner, and a JDK for
# `keytool` to generate the throwaway keystore) to already be resolvable --
# from the local nix store if this pin's packages were fetched before (as
# they were while developing this repo), otherwise from cache.nixos.org over
# the network. It does not need any *other* network access: no live host,
# no credential, nothing beyond the nixpkgs substituter.
set -euo pipefail
here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
source "$here/test_lib.sh"
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
cd "$work"
echo "generating throwaway keystore..."
bash "$here/with-nixpkgs.sh" jdk21 keytool -genkeypair \
-keystore app.jks -storepass apppass123 -keypass apppass123 \
-alias appkey -keyalg RSA -keysize 2048 -validity 1 \
-dname "CN=Test, OU=Test, O=Test, L=Test, S=Test, C=US" >/dev/null 2>&1
assert_file_exists "throwaway keystore was created" app.jks
echo "building fixture APKs..."
# python3 is not assumed to be on PATH -- the real runner this repo targets
# has none either (weblib-ci's own README says so), so even fixture-building
# goes through the same pin the scripts under test use.
bash "$here/with-nixpkgs.sh" python3 python3 - "$work" <<'PYEOF'
import struct, sys, zipfile
def add_stored(zf, name, data, align=None):
info = zipfile.ZipInfo(name)
info.compress_type = zipfile.ZIP_STORED
if align:
header_len = 30 + len(name.encode())
start = zf.fp.tell() + header_len
pad = (-start) % align
if pad:
if pad < 4:
pad += align
extra_len = pad - 4
info.extra = struct.pack("<HH", 0xD935, extra_len) + b"\x00" * extra_len
zf.writestr(info, data)
work = sys.argv[1]
with zipfile.ZipFile(f"{work}/aligned.apk", "w") as zf:
add_stored(zf, "AndroidManifest.xml", b"0123456789", 4)
with zipfile.ZipFile(f"{work}/misaligned.apk", "w") as zf:
add_stored(zf, "AndroidManifest.xml", b"0123456789")
PYEOF
assert_file_exists "aligned fixture built" aligned.apk
assert_file_exists "misaligned fixture built (fault to inject)" misaligned.apk
export APP_KS_PASS=apppass123
echo "--- fault: signing the misaligned fixture must be refused ---"
assert_failure "sign-apk.sh refuses a misaligned APK" \
bash "$here/sign-apk.sh" misaligned.apk misaligned-signed.apk app.jks appkey APP_KS_PASS \
--min-sdk-version 21 --max-sdk-version 34
if [ ! -e misaligned-signed.apk ]; then
pass "no signed output was produced for the misaligned fixture"
else
fail "no signed output was produced for the misaligned fixture (misaligned-signed.apk exists!)"
fi
echo "--- revert: the same content, properly aligned, must sign and verify ---"
assert_success "sign-apk.sh signs the aligned APK" \
bash "$here/sign-apk.sh" aligned.apk aligned-signed.apk app.jks appkey APP_KS_PASS \
--min-sdk-version 21 --max-sdk-version 34
assert_file_exists "signed APK was produced" aligned-signed.apk
echo "--- independent verification, not just trusting sign-apk.sh's own exit code ---"
verify_out=$(bash "$here/with-nixpkgs.sh" apksigner apksigner verify --print-certs \
--min-sdk-version 21 --max-sdk-version 34 aligned-signed.apk 2>&1) && rc=0 || rc=$?
if [ "${rc:-1}" -eq 0 ]; then pass "apksigner verify (run independently) passes"; else
fail "apksigner verify (run independently) passes"; echo "$verify_out" >&2
fi
assert_contains "verify output names the certificate we signed with" "$verify_out" "CN=Test"
echo "--- missing password env var is refused, not silently empty ---"
assert_failure "sign-apk.sh refuses when the password env var is unset" \
env -u APP_KS_PASS -u WRONG_VAR bash "$here/sign-apk.sh" aligned.apk out2.apk app.jks appkey WRONG_VAR \
--min-sdk-version 21 --max-sdk-version 34
report_and_exit