fix: align OTA install with DriveHub Dort
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
e72c247b52963445f6e2b2995300578ba35440ca8de120065040b12b6fe7904e FatihsMG4-unstable-2.2.10.apk
|
||||
@@ -36,7 +36,7 @@ release is a candidate only if all of these hold:
|
||||
| Asset name ends `-<version>.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
|
||||
|
||||
+2
-2
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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<String> 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<String> 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<String> 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<String> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<String> archive = ApkSignature.of(context, apk.getAbsolutePath());
|
||||
java.util.Set<String> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user