feat(#330): pin the expected signing certificate SHA-256

Rejecting only CN=Android Debug catches the #111 fallback but not a
DIFFERENT key being substituted, which breaks cross-channel updates
against Play just as badly. Now asserts the exact fingerprint.

Established from artifacts users have already installed, with no access
to the keystore password (reading a certificate off a signed APK needs
none):

  release b55  (2026-07-06)  e7da8cd5...
  release b58  (2026-07-10)  e7da8cd5...
  local release build        e7da8cd5...

Three independent sources agree, and the owner confirmed the same
strycher-personal.jks is enrolled for Play App Signing, so this is the
fingerprint that must hold for a GitHub download to update over a Play
install.

Found while establishing it: published release b59 (2026-07-20) is
DEBUG-SIGNED (765fb469, CN=Android Debug), unlike b55 and b58. That is
#111 occurring in production. Filed separately.

Part of epic #312, plan #321 (Phase 5).
pull/344/head
Strycher 2 days ago
parent 8d60b00b1b
commit 29ca744667

@ -110,6 +110,17 @@ jobs:
# which is exactly the failure this workflow exists to prevent. So we # which is exactly the failure this workflow exists to prevent. So we
# read the certificate off the built APK and fail if it is the debug key. # read the certificate off the built APK and fail if it is the debug key.
- name: Verify APK is signed with the release certificate - name: Verify APK is signed with the release certificate
env:
# SHA-256 of the strycher-personal.jks signing certificate, taken
# from artifacts already published and installed by users: releases
# b55 and b58, and a local release build. Established without the
# keystore password, since reading a certificate off a signed APK
# needs no password.
#
# Pinning the exact value (rather than only rejecting the debug key)
# also catches a DIFFERENT key being substituted, which would break
# cross-channel updates against Play just as badly as debug signing.
EXPECTED_SHA256: e7da8cd5bf22ac3eda7cb954b8b120a18b6c4382d1b6e6fdd204ddedddaf5488
run: | run: |
set -eu set -eu
APK=build/app/outputs/flutter-apk/app-release.apk APK=build/app/outputs/flutter-apk/app-release.apk
@ -117,13 +128,35 @@ jobs:
APKSIGNER="${BUILD_TOOLS}/apksigner" APKSIGNER="${BUILD_TOOLS}/apksigner"
echo "Using ${APKSIGNER}" echo "Using ${APKSIGNER}"
CERTS=$("${APKSIGNER}" verify --print-certs "${APK}") CERTS=$("${APKSIGNER}" verify --print-certs "${APK}")
echo "${CERTS}"
# Debug fallback is the #111 failure mode and the reason this check
# exists: build.gradle.kts silently signs with the debug config when
# key.properties is absent, and a green build hides it completely.
if echo "${CERTS}" | grep -qi "CN=Android Debug"; then if echo "${CERTS}" | grep -qi "CN=Android Debug"; then
echo "::error::APK is DEBUG-SIGNED. key.properties was not picked up; see #111." echo "::error::APK is DEBUG-SIGNED. key.properties was not picked up; see #111."
exit 1 exit 1
fi fi
echo "${CERTS}" | grep -i "SHA-256 digest" | head -1
echo "Release certificate confirmed (not the Android Debug key)." ACTUAL=$(echo "${CERTS}" \
| grep -i "Signer #1 certificate SHA-256 digest" \
| head -1 \
| awk -F': ' '{print $2}' \
| tr -d '[:space:]')
if [ -z "${ACTUAL}" ]; then
echo "::error::Could not read a certificate SHA-256 from the APK."
exit 1
fi
if [ "${ACTUAL}" != "${EXPECTED_SHA256}" ]; then
echo "::error::Signing certificate MISMATCH."
echo "::error::expected ${EXPECTED_SHA256}"
echo "::error::actual ${ACTUAL}"
echo "::error::This APK would not install over an existing Offband install."
exit 1
fi
echo "Signing certificate verified: ${ACTUAL}"
- name: Upload signed APK - name: Upload signed APK
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4

Loading…
Cancel
Save

Powered by TurnKey Linux.