From bad747a23006a4aaf5a9b7ff080e3177bb64d48f Mon Sep 17 00:00:00 2001 From: TCFDONMEZ Date: Wed, 30 Sep 2026 10:29:57 +0300 Subject: [PATCH] fix: align OTA install with DriveHub Dort --- CHANGELOG.md | 8 ++ FatihsMG4-unstable-2.2.10.apk.sha256 | 1 + OTA.md | 2 +- app/build.gradle | 4 +- .../java/com/evsuite/abrp/ApkSignature.java | 87 ------------------- .../java/com/evsuite/abrp/OtaUpdater.java | 29 +------ .../java/com/evsuite/abrp/UpdateHook.java | 8 +- 7 files changed, 19 insertions(+), 120 deletions(-) create mode 100644 FatihsMG4-unstable-2.2.10.apk.sha256 delete mode 100644 app/src/unstable/java/com/evsuite/abrp/ApkSignature.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b4de6c..dcb52e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.2.10] - 2026-09-30 + +### Fixed + +- Matched DriveHub_Dort's proven install path: verify SHA-256 and package identity, then + let Android PackageInstaller perform the authoritative signing-certificate check. This + avoids the head unit's false signature mismatch while remaining fail-closed. + ## [2.2.9] - 2026-09-30 ### Changed diff --git a/FatihsMG4-unstable-2.2.10.apk.sha256 b/FatihsMG4-unstable-2.2.10.apk.sha256 new file mode 100644 index 0000000..cdf7dec --- /dev/null +++ b/FatihsMG4-unstable-2.2.10.apk.sha256 @@ -0,0 +1 @@ +e72c247b52963445f6e2b2995300578ba35440ca8de120065040b12b6fe7904e FatihsMG4-unstable-2.2.10.apk diff --git a/OTA.md b/OTA.md index 3af83ef..5dc5c23 100644 --- a/OTA.md +++ b/OTA.md @@ -36,7 +36,7 @@ release is a candidate only if all of these hold: | Asset name ends `-.apk` | `ASSET_VERSION` regex | No version, asset ignored | | Version strictly higher than installed | `isNewer()` | Nothing to do, reports "Güncel" | | Download host on the allowlist | `isAllowedUrl()` | Refused and logged | -| APK signed with the **same certificate** as the running app | `signatureMatchesRunningApp()` | Deleted, never installed | +| APK signed with the **same certificate** as the running app | Android `PackageInstaller` | Installation is rejected by the platform | | Matching `.sha256` asset or `SHA256SUMS` entry | `fetchExpectedSha256()` | Update ignored/refused | The last one is the one that bites. The car runs a platform-signed build; an APK signed diff --git a/app/build.gradle b/app/build.gradle index 038db82..d11b53a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -34,8 +34,8 @@ android { applicationId "com.evsuite.abrp" minSdk 27 targetSdk 36 - versionCode 26 - versionName "2.2.9" + versionCode 27 + versionName "2.2.10" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/unstable/java/com/evsuite/abrp/ApkSignature.java b/app/src/unstable/java/com/evsuite/abrp/ApkSignature.java deleted file mode 100644 index 20e197a..0000000 --- a/app/src/unstable/java/com/evsuite/abrp/ApkSignature.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.evsuite.abrp; - -import android.content.Context; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.Signature; -import android.content.pm.SigningInfo; -import android.os.Build; -import android.util.Log; - -import java.security.MessageDigest; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -/** SHA-256 fingerprints of the certificates signing an APK. Unstable builds only. */ -final class ApkSignature { - - private static final String TAG = "ApkSignature"; - - private ApkSignature() { } - - /** Fingerprints of the APK file at [path]; empty when it cannot be read. */ - static Set of(Context context, String path) { - try { - PackageInfo info = context.getPackageManager() - .getPackageArchiveInfo(path, flags()); - return digests(info); - } catch (Exception e) { - Log.w(TAG, "Cannot read archive signature: " + e.getMessage()); - return Collections.emptySet(); - } - } - - /** Fingerprints of the running app; empty when they cannot be read. */ - static Set ofPackage(Context context) { - try { - PackageInfo info = context.getPackageManager() - .getPackageInfo(context.getPackageName(), flags()); - return digests(info); - } catch (Exception e) { - Log.w(TAG, "Cannot read our own signature: " + e.getMessage()); - return Collections.emptySet(); - } - } - - @SuppressWarnings("deprecation") - private static int flags() { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P - ? PackageManager.GET_SIGNING_CERTIFICATES - : PackageManager.GET_SIGNATURES; - } - - @SuppressWarnings("deprecation") - private static Set digests(PackageInfo info) { - if (info == null) return Collections.emptySet(); - - Signature[] signatures; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - SigningInfo signingInfo = info.signingInfo; - if (signingInfo == null) return Collections.emptySet(); - signatures = signingInfo.hasMultipleSigners() - ? signingInfo.getApkContentsSigners() - : signingInfo.getSigningCertificateHistory(); - } else { - signatures = info.signatures; - } - if (signatures == null) return Collections.emptySet(); - - Set out = new HashSet<>(); - try { - MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); - for (Signature signature : signatures) { - if (signature == null) continue; - StringBuilder hex = new StringBuilder(); - for (byte b : sha256.digest(signature.toByteArray())) { - hex.append(String.format("%02x", b)); - } - out.add(hex.toString()); - } - } catch (Exception e) { - Log.w(TAG, "Digest failed: " + e.getMessage()); - return Collections.emptySet(); - } - return out; - } -} diff --git a/app/src/unstable/java/com/evsuite/abrp/OtaUpdater.java b/app/src/unstable/java/com/evsuite/abrp/OtaUpdater.java index 67bc62a..f1bd7a5 100644 --- a/app/src/unstable/java/com/evsuite/abrp/OtaUpdater.java +++ b/app/src/unstable/java/com/evsuite/abrp/OtaUpdater.java @@ -406,13 +406,10 @@ final class OtaUpdater { } /** - * What {@link #install} did. Every value except {@link #SESSION_STARTED} is a refusal, - * and they are distinct because the single "imza uyuşmuyor?" message they used to share - * sent a perfectly well-signed build to the wrong diagnosis. + * What {@link #install} did. The PackageInstaller is the authority for signing-certificate + * compatibility, matching the proven DriveHub_Dort flow on this head unit. */ enum InstallResult { - /** The archive is signed by a different certificate than the running app. */ - SIGNATURE_MISMATCH, /** The package manager cannot parse the archive, or it is for another package. */ UNREADABLE_ARCHIVE, /** Handed to the platform. The outcome arrives at {@link OtaInstallResultReceiver}. */ @@ -435,7 +432,6 @@ final class OtaUpdater { * file as soon as this returns. */ static InstallResult install(Context context, File apk) { - if (!signatureMatchesRunningApp(context, apk)) return InstallResult.SIGNATURE_MISMATCH; if (!archiveIsThisPackage(context, apk)) return InstallResult.UNREADABLE_ARCHIVE; try { commitSession(context, apk); @@ -449,9 +445,8 @@ final class OtaUpdater { /** * True if the archive parses and declares our own package name. * - * A signature match alone does not say the APK is this app: the same platform key signs - * every app on the unit, so a correctly signed archive for some other package would pass - * the certificate check and then fail deep inside the installer. + * The package name is checked here before any bytes reach an installer session. Android's + * PackageInstaller then performs the authoritative signing-certificate and downgrade checks. */ private static boolean archiveIsThisPackage(Context context, File apk) { try { @@ -516,20 +511,4 @@ final class OtaUpdater { } } - /** - * True if [apk] is signed by the same certificate as the running app. - * - * Fail closed: an unreadable archive, a missing signature or a failed API call all - * return false. The caller must delete the file rather than offer it for install. - */ - static boolean signatureMatchesRunningApp(Context context, File apk) { - java.util.Set archive = ApkSignature.of(context, apk.getAbsolutePath()); - java.util.Set installed = ApkSignature.ofPackage(context); - boolean ok = !archive.isEmpty() && !installed.isEmpty() && archive.equals(installed); - if (!ok) { - Log.w(TAG, "Signature mismatch — refusing update (" - + archive.size() + " vs " + installed.size() + " cert(s))"); - } - return ok; - } } diff --git a/app/src/unstable/java/com/evsuite/abrp/UpdateHook.java b/app/src/unstable/java/com/evsuite/abrp/UpdateHook.java index 11d7bb9..837617b 100644 --- a/app/src/unstable/java/com/evsuite/abrp/UpdateHook.java +++ b/app/src/unstable/java/com/evsuite/abrp/UpdateHook.java @@ -17,9 +17,9 @@ import java.util.concurrent.atomic.AtomicBoolean; * Installation goes through the platform {@link android.content.pm.PackageInstaller}, not * {@code pm install -r}: that shelled out with a path inside our private cache, which the * package manager service cannot read from its own process, and the generic failure was - * reported as a signature mismatch it never was. {@link OtaUpdater#install} still checks the - * downloaded APK's certificate against the running app's first and refuses a mismatch, so an - * update signed with any other key is deleted rather than installed. + * reported as a signature mismatch it never was. As in DriveHub_Dort, PackageInstaller is the + * authority for signing-certificate compatibility; the app verifies SHA-256 and package name + * before handing it the archive. * * Committing a session is asynchronous, so the message this returns says an install has been * started, never that one finished. {@link OtaInstallResultReceiver} has the verdict. @@ -128,8 +128,6 @@ final class UpdateHook { // platform may still want the driver to confirm, and either way the verdict // arrives at OtaInstallResultReceiver, not here. return "Kuruluyor: " + update.versionName + " — ekrandaki onayı bekleyin"; - case SIGNATURE_MISMATCH: - return "Kurulum reddedildi: imza uyuşmuyor"; case UNREADABLE_ARCHIVE: return "Kurulum reddedildi: APK okunamadı"; default: