ci(android): pin the release certificate in the signer check (#5116)
release / govulncheck (push) Successful in 17s
release / web (push) Successful in 1m20s
release / go (push) Successful in 1m36s
release / integration (push) Successful in 5m25s
release / android (push) Successful in 6m25s
release / Build signed APK (releases and dev) (push) Successful in 7m0s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Verify release artifacts (tag releases only) (push) Canceled after 0s
release / Build + push container image (push) Canceled after 1m3s

The check failed only on a debug signer, so an APK signed by any other
wrong key (a regenerated keystore, a swapped secret) would publish and
then reach no installed phone: Android updates in place only when the
signer matches. The step now requires exactly one signer whose
SHA-256 digest is the release certificate's (CN=Minstrel,
O=FabledSword, read from run 8446), and names a debug key or the
digest it got when it fails. Rotating the key on purpose changes the
digest in the same commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 22:51:33 -04:00
co-authored by Claude Opus 5.5
parent a1e9de2c84
commit 40dd5bb52c
+25 -9
View File
@@ -523,23 +523,39 @@ jobs:
-PMINSTREL_VERSION_NAME=${{ steps.ver.outputs.name }} \ -PMINSTREL_VERSION_NAME=${{ steps.ver.outputs.name }} \
-PMINSTREL_VERSION_CODE=${{ steps.ver.outputs.code }} -PMINSTREL_VERSION_CODE=${{ steps.ver.outputs.code }}
# The APK every phone updates from must carry the release key: Android # The APK every phone updates from must carry THE release key: Android
# updates an app in place only when the signer matches. Gradle signs # updates an app in place only when the signer matches, so an APK
# with the release key or leaves the APK unsigned, so this catches a # signed by any other key (debug, a regenerated keystore, a swapped
# wrong key, an unsigned build and a debug signer before anything # secret) reaches no installed phone. The certificate's digest is
# publishes (family idea #5103, practice 3). apksigner, not keytool: # pinned below; it is public, not a secret. Gradle signs with the
# keytool prints nothing for a v2-only APK. # release key or leaves the APK unsigned, and an unsigned build fails
# here too, as there is no app-release.apk to verify (family idea
# #5103, practice 3). apksigner, not keytool: keytool prints nothing
# for a v2-only APK.
#
# Rotating the key on purpose means every install must be removed and
# reinstalled; change the digest here in the same commit.
- name: The APK carries the release key - name: The APK carries the release key
shell: bash shell: bash
env:
# CN=Minstrel, O=FabledSword. Read from run 8446 (#5116).
RELEASE_CERT_SHA256: 43d183307bc46b821789d90444a960b137f78f2166ff431efb2406d0fceaf612
run: | run: |
set -euo pipefail set -euo pipefail
sdk="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}" sdk="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}"
signer="$(ls "$sdk"/build-tools/*/apksigner 2>/dev/null | sort -V | tail -1 || true)" signer="$(ls "$sdk"/build-tools/*/apksigner 2>/dev/null | sort -V | tail -1 || true)"
test -n "$signer" || { echo "::error::no apksigner under '$sdk/build-tools'"; exit 1; } test -n "$signer" || { echo "::error::no apksigner under '$sdk/build-tools'"; exit 1; }
certs="$("$signer" verify --print-certs app/build/outputs/apk/release/app-release.apk)" certs="$("$signer" verify --print-certs app/build/outputs/apk/release/app-release.apk)"
printf '%s\n' "$certs" | grep -E 'Signer #1 certificate (DN|SHA-256 digest)' printf '%s\n' "$certs" | grep -E '^Signer #[0-9]+ certificate (DN|SHA-256 digest)'
if printf '%s' "$certs" | grep -q 'CN=Android Debug'; then # One signer, and it is ours. A second signer would be a lineage or
echo "::error::the release APK is signed with a debug key" # a mistake; either way not something to ship unexamined.
digests="$(printf '%s\n' "$certs" | sed -n 's/^Signer #[0-9]* certificate SHA-256 digest: //p')"
if [ "$digests" != "$RELEASE_CERT_SHA256" ]; then
if printf '%s' "$certs" | grep -q 'CN=Android Debug'; then
echo "::error::the release APK is signed with a debug key"
else
echo "::error::the release APK is not signed by the release key: got '${digests//$'\n'/ }', want ${RELEASE_CERT_SHA256}"
fi
exit 1 exit 1
fi fi