From 4f69012249118e6e23f61addb3470dac292af1f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D1=83=D1=80=D0=BD=D0=B0=D1=82=20=D0=90=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=B5=D0=B9?= Date: Wed, 10 Jun 2026 06:52:29 +0300 Subject: [PATCH] Document Android debug-signed rollout policy --- Android/docs/argus-rollout-policy.md | 23 +++++++++++++++++++ .../settings/update/AppUpdateManager.kt | 13 ++++++++++- .../src/main/res/values-ru/strings.xml | 2 ++ .../qsferaApp/src/main/res/values/strings.xml | 2 ++ PRODUCTION_READINESS.md | 2 +- 5 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 Android/docs/argus-rollout-policy.md diff --git a/Android/docs/argus-rollout-policy.md b/Android/docs/argus-rollout-policy.md new file mode 100644 index 00000000..c09646c7 --- /dev/null +++ b/Android/docs/argus-rollout-policy.md @@ -0,0 +1,23 @@ +# QSfera Android Argus Rollout Policy + +## Sources + +- Android update requirement: Android Developers says an update must use the same application ID and the same signing certificate as the installed app, unless it contains a valid proof-of-rotation: . +- QSfera update implementation: `Android/qsferaApp/src/main/java/eu/qsfera/android/presentation/settings/update/AppUpdateManager.kt`. +- QSfera Android version and release signing env names: `Android/qsferaApp/build.gradle`. +- Current Argus release and certificate facts verified for this production-readiness pass: `PRODUCTION_READINESS.md`, section `Android Argus Publication Check`. + +## Policy + +- Stable Argus releases for `qsfera-mobile` must keep the same package name and production signing certificate after the production-signed `1.3.9` / `37` release. +- The in-app updater must not bypass certificate mismatch. `AppUpdateManager` validates package name, monotonically newer `versionCode`, Argus `androidVersionCode`, and signer digest before starting Android package installation. +- A client installed from the debug-signed `1.3.5` / `33` Argus APK cannot be moved to the production-signed `1.3.9` / `37` APK by the in-app updater because the signer digest check intentionally fails. +- For debug-signed `1.3.5` installs, the supported migration path is: back up unsynced local files, uninstall the debug-signed build, then install the current stable QSfera APK from Argus. +- On certificate mismatch, the Android app must show an explicit reinstall-required dialog instead of a generic update error. + +## Release Checklist + +1. Build the stable APK with the same release key material through `QSFERA_RELEASE_KEYSTORE`, `QSFERA_RELEASE_KEYSTORE_PASSWORD`, `QSFERA_RELEASE_KEY_ALIAS`, and `QSFERA_RELEASE_KEY_PASSWORD`. +2. Verify the APK certificate before upload with `apksigner verify --print-certs`. +3. Publish to Argus only after certificate, package name, version code, and SHA-256 checks pass. +4. Verify the published manifest and download with `scripts/verify-production-state.ps1 -VerifyAndroidDownload` from the repository root. diff --git a/Android/qsferaApp/src/main/java/eu/qsfera/android/presentation/settings/update/AppUpdateManager.kt b/Android/qsferaApp/src/main/java/eu/qsfera/android/presentation/settings/update/AppUpdateManager.kt index 0aeda06e..7593a550 100644 --- a/Android/qsferaApp/src/main/java/eu/qsfera/android/presentation/settings/update/AppUpdateManager.kt +++ b/Android/qsferaApp/src/main/java/eu/qsfera/android/presentation/settings/update/AppUpdateManager.kt @@ -123,6 +123,15 @@ class AppUpdateManager(private val fragment: Fragment) { } private fun showErrorDialog(context: Context, error: Throwable) { + if (error is SignatureMismatchException) { + AlertDialog.Builder(context) + .setTitle(R.string.update_reinstall_required_title) + .setMessage(R.string.update_reinstall_required_message) + .setPositiveButton(android.R.string.ok, null) + .show() + return + } + AlertDialog.Builder(context) .setTitle(R.string.common_error) .setMessage(context.getString(R.string.update_error, error.message ?: error.javaClass.simpleName)) @@ -216,7 +225,7 @@ class AppUpdateManager(private val fragment: Fragment) { } if (!hasTrustedSignature(context, downloadedPackageInfo)) { - error(context.getString(R.string.update_signature_error)) + throw SignatureMismatchException(context.getString(R.string.update_signature_error)) } } @@ -324,6 +333,8 @@ class AppUpdateManager(private val fragment: Fragment) { val sha256: String ) + private class SignatureMismatchException(message: String) : IllegalStateException(message) + companion object { private const val ARGUS_BASE_URL = "https://argus.kusoft.xyz" private const val ARGUS_MANIFEST_URL = diff --git a/Android/qsferaApp/src/main/res/values-ru/strings.xml b/Android/qsferaApp/src/main/res/values-ru/strings.xml index 42c59b77..27f7f2d3 100644 --- a/Android/qsferaApp/src/main/res/values-ru/strings.xml +++ b/Android/qsferaApp/src/main/res/values-ru/strings.xml @@ -123,6 +123,8 @@ Скачанное обновление не новее установленного приложения. Скачанное обновление не совпадает с метаданными релиза в Аргус. Скачанное обновление подписано не сертификатом установленного приложения. + Требуется переустановка + Эта установленная сборка подписана другим сертификатом, чем текущий релиз QSfera в Аргус. Android не может обновить её поверх установленной. Сохраните несинхронизированные файлы, удалите эту сборку и установите QSfera из Аргус. Скачанное обновление не удалось прочитать как Android-пакет. Последний Android-релиз не найден. Пожалуйста, введите графический ключ diff --git a/Android/qsferaApp/src/main/res/values/strings.xml b/Android/qsferaApp/src/main/res/values/strings.xml index 9b5395f5..379dc8ff 100644 --- a/Android/qsferaApp/src/main/res/values/strings.xml +++ b/Android/qsferaApp/src/main/res/values/strings.xml @@ -125,6 +125,8 @@ Downloaded update is not newer than the installed app. Downloaded update does not match Argus release metadata. Downloaded update is not signed with the installed app certificate. + Reinstall required + This installed build was signed with a different certificate than the current QSfera release in Argus. Android cannot update it in place. Back up unsynced files, uninstall this build, then install QSfera from Argus. Downloaded update could not be read as an Android package. Latest Android release was not found. Please enter your pattern diff --git a/PRODUCTION_READINESS.md b/PRODUCTION_READINESS.md index 9df90fa8..d7c13a0a 100644 --- a/PRODUCTION_READINESS.md +++ b/PRODUCTION_READINESS.md @@ -70,12 +70,12 @@ Date: 2026-06-08. - The previously published `1.3.5`/`33` APK matched Argus SHA-256 `b7d5bdcb49527c00983a15849252fa42189dee4287754803c46cf2f32e2130f3`, but `apksigner` reported `CN=Android Debug` with certificate SHA-256 `b86d745cecb01ca24bda03092fe94ca7801678513b97edd1175bcabfdea4e941`. - The new `1.3.9`/`37` APK is signed by `CN=QSfera Argus Upload, O=QSfera, C=RU` with certificate SHA-256 `ea5c304c78ee333f6586f54c933d986c1f865da9c4e3595f8fe0df274c1ab877`. - `AppUpdateManager` validates downloaded APK package name, greater `versionCode`, Argus `androidVersionCode`, and signer digest against the installed app signature. Because of that local code path, devices that installed debug-signed `1.3.5` should be treated as needing reinstall or an explicit migration plan before they can move to the production-signed `1.3.9`. +- The rollout policy for debug-signed `1.3.5` devices is documented in `Android/docs/argus-rollout-policy.md`: the app does not bypass signer mismatch; users must back up unsynced local files, uninstall the debug-signed build, then install the current stable APK from Argus. The Android app now shows a reinstall-required dialog for this certificate-mismatch path. ## Remaining P0 - Back up the gitignored Android release signing files created under `Android/signing/` and `Android/argus-signing.local.properties` in a secure external secret store. Without this key material, future in-place Android updates from the production-signed `1.3.9` cannot be signed with the same certificate. - Provide CI/Argus signing secrets derived from the same release key: `QSFERA_RELEASE_KEYSTORE`, `QSFERA_RELEASE_KEYSTORE_PASSWORD`, `QSFERA_RELEASE_KEY_ALIAS`, and `QSFERA_RELEASE_KEY_PASSWORD`. -- Decide the rollout policy for any devices that installed the debug-signed Argus `1.3.5` APK. The production-signed `1.3.9` APK is the correct current release, but installed debug-signed clients will not satisfy the app's own signer-digest check for an in-place update. - Fill production domains, ACME email, admin passwords, Keycloak passwords, and storage secrets from a secret manager. Do not commit real values. - Run `./validate-production-env.sh .env` and then `docker compose config` for `Server/devtools/deployments/qsfera_full` with a filled production `.env`; Docker CLI is not available in this local environment, so compose rendering is not locally verified. - Provide real Windows signing secrets in CI: `QSFERA_DESKTOP_SIGN_PACKAGE=True` and `QSFERA_DESKTOP_CODESIGN_CERTIFICATE` with the Craft-compatible signing certificate reference. The current local MSI is intentionally not published as a production update because `Get-AuthenticodeSignature` reports `NotSigned` and `Desktop/scripts/validate-production-release.ps1 -RequireSigning` fails until signing is configured.