Document Android debug-signed rollout policy
This commit is contained in:
@@ -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: <https://developer.android.com/google/play/app-updates>.
|
||||
- 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.
|
||||
+12
-1
@@ -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 =
|
||||
|
||||
@@ -123,6 +123,8 @@
|
||||
<string name="update_version_error">Скачанное обновление не новее установленного приложения.</string>
|
||||
<string name="update_metadata_error">Скачанное обновление не совпадает с метаданными релиза в Аргус.</string>
|
||||
<string name="update_signature_error">Скачанное обновление подписано не сертификатом установленного приложения.</string>
|
||||
<string name="update_reinstall_required_title">Требуется переустановка</string>
|
||||
<string name="update_reinstall_required_message">Эта установленная сборка подписана другим сертификатом, чем текущий релиз QSfera в Аргус. Android не может обновить её поверх установленной. Сохраните несинхронизированные файлы, удалите эту сборку и установите QSfera из Аргус.</string>
|
||||
<string name="update_apk_parse_error">Скачанное обновление не удалось прочитать как Android-пакет.</string>
|
||||
<string name="update_release_not_found">Последний Android-релиз не найден.</string>
|
||||
<string name="pattern_enter_pattern">Пожалуйста, введите графический ключ</string>
|
||||
|
||||
@@ -125,6 +125,8 @@
|
||||
<string name="update_version_error">Downloaded update is not newer than the installed app.</string>
|
||||
<string name="update_metadata_error">Downloaded update does not match Argus release metadata.</string>
|
||||
<string name="update_signature_error">Downloaded update is not signed with the installed app certificate.</string>
|
||||
<string name="update_reinstall_required_title">Reinstall required</string>
|
||||
<string name="update_reinstall_required_message">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.</string>
|
||||
<string name="update_apk_parse_error">Downloaded update could not be read as an Android package.</string>
|
||||
<string name="update_release_not_found">Latest Android release was not found.</string>
|
||||
<string name="pattern_enter_pattern">Please enter your pattern</string>
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user