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>
92 lines
4.0 KiB
Bash
Executable File
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
|