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>
This commit is contained in:
Executable
+91
@@ -0,0 +1,91 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user