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
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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user