Release signing
Official release APKs are signed with a permanent, project-owned key. Android refuses to upgrade an installed app when the signing certificate changes, so losing or replacing this key strands every installed user. Store builds (Play, F-Droid) are out of scope until a signed GitHub Release has been verified end to end.
Build behavior
| Build | Signed with | Official |
|---|---|---|
v* tag CI |
release key (required, build fails without it) | yes |
branch / PR CI, local assembleRelease without key |
Android debug key | no, never published |
local assembleRelease with keystore.properties |
release key | only if the fingerprint matches the pin |
app/build.gradle.kts reads ANDROID_KEYSTORE_FILE, ANDROID_KEYSTORE_PASSWORD,
ANDROID_KEY_ALIAS, ANDROID_KEY_PASSWORD from the environment, or the same values
(storeFile, storePassword, keyAlias, keyPassword) from a git-ignored
keystore.properties. A partial configuration fails the build rather than falling back.
-PrequireReleaseSigning=true fails the build when no key is configured.
One-time setup (maintainer)
Follow the Release keystore manual: it covers key generation, the
certificate fingerprint, encrypted backups with a restore drill, the GitHub secrets and
variable, the first signed release, rotation and troubleshooting. In short, the repository
needs four secrets (ANDROID_KEYSTORE_BASE64, ANDROID_KEYSTORE_PASSWORD,
ANDROID_KEY_ALIAS, ANDROID_KEY_PASSWORD) and one variable (ANDROID_RELEASE_CERT_SHA256).
Verification
scripts/verify_release_signature.py APK runs apksigner verify, requires exactly one signer
and a v2/v3 signature, rejects the Android debug certificate, and compares the SHA-256
fingerprint with ANDROID_RELEASE_CERT_SHA256 (or --expected-sha256). CI runs it on the tag
build, and release.yml runs it again on the downloaded artifact before publishing; the
fingerprint is recorded in release-provenance.json. Only the CI run triggered by the tag
itself may publish, because a main run at the same commit is debug-signed.
Local dry run with a throwaway key (do not use the real key for experiments):
keytool -genkeypair -storetype PKCS12 -keystore /tmp/test.jks -alias t -keyalg RSA -keysize 2048 \
-validity 30 -storepass testpass -dname 'CN=test'
ANDROID_KEYSTORE_FILE=/tmp/test.jks ANDROID_KEYSTORE_PASSWORD=testpass \
ANDROID_KEY_ALIAS=t ANDROID_KEY_PASSWORD=testpass \
./gradlew :app:assembleRelease -PrequireReleaseSigning=true
python3 scripts/verify_release_signature.py app/build/outputs/apk/release/app-release.apk \
--expected-sha256 <fingerprint of /tmp/test.jks>
Upgrading from earlier builds
Earlier GitHub APKs (and v0.2.0-rc.1 sideloads) are debug-signed. They cannot be upgraded in
place to a release-signed build: uninstall once, then install the signed release.
Key rotation
Use APK Signature Scheme v3 rotation (apksigner rotate) only with a documented plan; do not
rotate ad hoc. Never reuse the release key for the Play upload key without deciding that
deliberately.