Skip to content

Commit 3969c0a

Browse files
authored
Merge pull request #28 from billy-lau/uraniborg/improve-signer-deduction
uraniborg: Distinguish signing lineage from co-signing in Hubble 2.2.0
2 parents a7d4ac8 + 1337133 commit 3969c0a

10 files changed

Lines changed: 1185 additions & 69 deletions

File tree

‎uraniborg/AndroidStudioProject/Hubble/app/build.gradle‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ android {
66
applicationId "com.uraniborg.hubble"
77
minSdkVersion 23
88
targetSdkVersion 35
9-
versionCode 11
10-
versionName "2.1.0"
9+
versionCode 12
10+
versionName "2.2.0"
1111
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
1212
}
1313
buildTypes {

‎uraniborg/AndroidStudioProject/Hubble/app/src/main/java/com/uraniborg/hubble/MainActivity.java‎

Lines changed: 169 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public class MainActivity extends AppCompatActivity {
4949
// semantically tie the notion of app version to versionName, which we will update for every
5050
// major and minor release. Unfortunately, for now, we have to independently and separately
5151
// update these values everytime we do any revisions because BuildConfig is phased out.
52-
private final String VERSION = "2.1.0";
52+
private final String VERSION = "2.2.0";
5353

5454
// We're changing to TreeMap so that package names are sorted. This would ease output comparison.
5555
private TreeMap<String, PackageMetadata> mAllPackages;
@@ -128,6 +128,106 @@ private void getInstalledPackagesInformation() {
128128
Log.d(tag, String.format("There are %d packages (including APEX)", mAllPackages.size()));
129129
}
130130

131+
// The Android framework ("platform") package. Its signing identity is what defines a
132+
// "platform-signed" package and gates access to the system shared UIDs.
133+
static final String PLATFORM_PACKAGE_NAME = "android";
134+
135+
/**
136+
* Returns {@link PackageManager#checkSignatures(String, String)}'s verdict comparing
137+
* {@code pkgName} against the platform ({@code android}) package.
138+
*
139+
* <p>Recorded verbatim as an observation. Per AOSP
140+
* {@code ComputerEngine#checkSignaturesInternal} and
141+
* {@code PackageManagerServiceUtils#compareSignatures}, the algorithm is:
142+
*
143+
* <ol>
144+
* <li>Compare the two packages' <em>current</em> signer sets for exact set equality.</li>
145+
* <li>If that fails and either side has a signing lineage, retry using only the
146+
* <em>oldest</em> ancestor of each ({@code getPastSigningCertificates()[0]}) - an
147+
* explicit backwards-compatibility path for callers predating key rotation.</li>
148+
* </ol>
149+
*
150+
* <p>IMPORTANT: this is <em>not</em> a capability-aware trust decision, and it is not a
151+
* sound oracle for "is this platform-signed?". It never consults the per-ancestor
152+
* {@code SigningDetails.CertCapabilities} flags ({@code PERMISSION},
153+
* {@code SHARED_USER_ID}); those are evaluated elsewhere in the framework (shared-UID join
154+
* logic and the permission subsystem) and are not reachable from any public API. Two known
155+
* divergences follow directly from the algorithm above:
156+
*
157+
* <ul>
158+
* <li>A package that has rotated <em>away</em> from the platform key still reports
159+
* {@code MATCH}, because the oldest-ancestor retry compares the retired platform
160+
* certificate.</li>
161+
* <li>A package co-signed by the platform key <em>plus</em> another key reports
162+
* {@code NO_MATCH}, because step 1 requires exact set equality.</li>
163+
* </ul>
164+
*
165+
* <p>Consumers should therefore treat this as descriptive metadata (what
166+
* {@code PackageManager} itself would report to an app), not as the basis for platform
167+
* trust. See {@code HubbleParser.is_platform_signed()}.
168+
*
169+
* @param pkgName the package to compare against the platform package.
170+
* @return one of {@code MATCH}, {@code NO_MATCH}, {@code NEITHER_SIGNED},
171+
* {@code FIRST_NOT_SIGNED}, {@code SECOND_NOT_SIGNED}, {@code UNKNOWN_PACKAGE}, or
172+
* {@code UNKNOWN} if the query itself failed.
173+
*/
174+
@NotNull
175+
private String getPlatformSignatureMatch(@NotNull String pkgName) {
176+
final String tag = TAG + "-CERT";
177+
try {
178+
int result = mPackageManager.checkSignatures(pkgName, PLATFORM_PACKAGE_NAME);
179+
switch (result) {
180+
case PackageManager.SIGNATURE_MATCH:
181+
return "MATCH";
182+
case PackageManager.SIGNATURE_NO_MATCH:
183+
return "NO_MATCH";
184+
case PackageManager.SIGNATURE_NEITHER_SIGNED:
185+
return "NEITHER_SIGNED";
186+
case PackageManager.SIGNATURE_FIRST_NOT_SIGNED:
187+
return "FIRST_NOT_SIGNED";
188+
case PackageManager.SIGNATURE_SECOND_NOT_SIGNED:
189+
return "SECOND_NOT_SIGNED";
190+
case PackageManager.SIGNATURE_UNKNOWN_PACKAGE:
191+
// Expected for entries (e.g. some APEXes) that PackageManager does not track as a
192+
// signature-comparable package. Consumers should fall back to digest comparison.
193+
return "UNKNOWN_PACKAGE";
194+
default:
195+
Log.e(tag, String.format("Unexpected checkSignatures result %d for package: %s", result,
196+
pkgName));
197+
return "UNKNOWN";
198+
}
199+
} catch (RuntimeException e) {
200+
Log.e(tag, String.format("Failed to checkSignatures against platform for package %s: %s",
201+
pkgName, e.getMessage()));
202+
return "UNKNOWN";
203+
}
204+
}
205+
206+
@NotNull
207+
private JSONArray extractAndRegisterCertificates(@NotNull String pkgName,
208+
@Nullable Signature[] signatures) {
209+
final String tag = TAG + "-CERT";
210+
JSONArray digests = new JSONArray();
211+
if (signatures == null) {
212+
return digests;
213+
}
214+
for (Signature signature : signatures) {
215+
if (signature == null) {
216+
continue;
217+
}
218+
String encodedSignatureDigest = Utilities.computeSHA256DigestOfCertificate(signature);
219+
if (encodedSignatureDigest == null) {
220+
Log.e(tag, String.format("Failed to compute hash for cert of package: %s", pkgName));
221+
continue;
222+
}
223+
if (!mAllCertificates.containsKey(encodedSignatureDigest)) {
224+
mAllCertificates.put(encodedSignatureDigest, signature.toByteArray());
225+
}
226+
digests.put(encodedSignatureDigest);
227+
}
228+
return digests;
229+
}
230+
131231
@SuppressWarnings("deprecation")
132232
private void getAllCertificates() {
133233
final String tag = TAG + "-CERT";
@@ -138,35 +238,82 @@ private void getAllCertificates() {
138238
continue;
139239
}
140240
PackageInfo pkgInfo = pkgMetadata.ref;
141-
Signature[] signatures;
241+
JSONObject signingInfoJson = new JSONObject();
242+
// Recorded verbatim as an observation; see getPlatformSignatureMatch().
243+
String platformSignatureMatch = getPlatformSignatureMatch(pkgName);
244+
142245
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
143-
signatures = pkgInfo.signatures;
246+
Signature[] signatures = pkgInfo.signatures;
247+
if (signatures == null) {
248+
Log.e(tag, String.format("Failed to grab signature for package: %s", pkgName));
249+
continue;
250+
}
251+
// Count the declared signers BEFORE computing digests: a single
252+
// computeSHA256DigestOfCertificate() failure drops an entry, and must not silently
253+
// demote a co-signed APK to a single-signer one.
254+
int declaredSignerCount = 0;
255+
for (Signature signature : signatures) {
256+
if (signature != null) {
257+
declaredSignerCount++;
258+
}
259+
}
260+
JSONArray activeSigners = extractAndRegisterCertificates(pkgName, signatures);
261+
pkgMetadata.certIds = activeSigners;
262+
try {
263+
signingInfoJson.put("hasMultipleSigners", declaredSignerCount > 1);
264+
// NOTE: pre-P PackageManager exposes no v3 lineage API at all, so rotation is
265+
// UNOBSERVABLE here rather than known to be absent. Emit null (not false) and no
266+
// lineage, so consumers classify these as UNKNOWN instead of asserting "never
267+
// rotated". See docs/hubble_results.md.
268+
signingInfoJson.put("hasPastSigningCertificates", JSONObject.NULL);
269+
signingInfoJson.put("apkContentsSigners", activeSigners);
270+
signingInfoJson.put("signingCertificateLineage", new JSONArray());
271+
signingInfoJson.put("platformSignatureMatch", platformSignatureMatch);
272+
pkgMetadata.signingInfo = signingInfoJson;
273+
} catch (JSONException e) {
274+
Log.e(tag, String.format("Failed to build signingInfo JSON for package %s: %s",
275+
pkgName, e.getMessage()));
276+
}
144277
} else {
145278
SigningInfo signingInfo = pkgInfo.signingInfo;
146-
if (signingInfo.hasMultipleSigners()) {
147-
signatures = signingInfo.getApkContentsSigners();
148-
} else {
149-
signatures = signingInfo.getSigningCertificateHistory();
279+
if (signingInfo == null) {
280+
Log.e(tag, String.format("Failed to grab signingInfo for package: %s", pkgName));
281+
continue;
150282
}
151-
}
152-
if (signatures == null) {
153-
Log.e(tag, String.format("Failed to grab signature for package: %s", pkgName));
154-
continue;
155-
}
156-
157-
JSONArray signaturesJSONArray = new JSONArray();
158-
for (Signature signature : signatures) {
159-
String encodedSignatureDigest = Utilities.computeSHA256DigestOfCertificate(signature);
160-
if (encodedSignatureDigest == null) {
161-
Log.e(tag, String.format("Failed to compute hash for cert of package: %s", pkgName));
283+
boolean hasMultipleSigners = signingInfo.hasMultipleSigners();
284+
boolean hasPastSigningCertificates = signingInfo.hasPastSigningCertificates();
285+
Signature[] activeSignatures = signingInfo.getApkContentsSigners();
286+
Signature[] lineageSignatures =
287+
hasMultipleSigners ? null : signingInfo.getSigningCertificateHistory();
288+
289+
if (activeSignatures == null && lineageSignatures == null) {
290+
Log.e(tag, String.format("Failed to grab signature for package: %s", pkgName));
162291
continue;
163292
}
164-
if (!mAllCertificates.containsKey(encodedSignatureDigest)) {
165-
mAllCertificates.put(encodedSignatureDigest, signature.toByteArray());
293+
294+
JSONArray apkContentsSigners = extractAndRegisterCertificates(pkgName, activeSignatures);
295+
JSONArray signingCertificateLineage =
296+
extractAndRegisterCertificates(pkgName, lineageSignatures);
297+
298+
if (hasMultipleSigners) {
299+
pkgMetadata.certIds = apkContentsSigners;
300+
} else {
301+
pkgMetadata.certIds = (signingCertificateLineage.length() > 0)
302+
? signingCertificateLineage : apkContentsSigners;
303+
}
304+
305+
try {
306+
signingInfoJson.put("hasMultipleSigners", hasMultipleSigners);
307+
signingInfoJson.put("hasPastSigningCertificates", hasPastSigningCertificates);
308+
signingInfoJson.put("apkContentsSigners", apkContentsSigners);
309+
signingInfoJson.put("signingCertificateLineage", signingCertificateLineage);
310+
signingInfoJson.put("platformSignatureMatch", platformSignatureMatch);
311+
pkgMetadata.signingInfo = signingInfoJson;
312+
} catch (JSONException e) {
313+
Log.e(tag, String.format("Failed to build signingInfo JSON for package %s: %s",
314+
pkgName, e.getMessage()));
166315
}
167-
signaturesJSONArray.put(encodedSignatureDigest);
168316
}
169-
pkgMetadata.certIds = signaturesJSONArray;
170317
}
171318
}
172319

‎uraniborg/AndroidStudioProject/Hubble/app/src/main/java/com/uraniborg/hubble/PackageMetadata.java‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ public class PackageMetadata extends BaseInfo {
4949
protected CharSequence description = null;
5050
protected int versionCode;
5151
protected String versionName;
52-
protected JSONArray certIds;
52+
protected JSONArray certIds = new JSONArray();
53+
protected JSONObject signingInfo = null;
5354
protected boolean isEnabled = false;
5455
protected boolean isTestOnly = false;
5556
protected boolean isFactoryTest = false;

‎uraniborg/README.md‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ Unit tests for the Python automation and verification scripts are located in
3737
parsing `preinstalled_packages.txt`, 6-state package classification
3838
(`FACTORY_PREINSTALLED_APK`, `FACTORY_PREINSTALLED_APEX`,
3939
`UPDATED_SYSTEM_APP`, `UPDATED_MAINLINE_MODULE`, `USER_INSTALLED`,
40-
`UNKNOWN`), and package query/filtering methods.
40+
`UNKNOWN`), signing certificate lineage vs. co-signing classification
41+
(`SINGLE_SIGNER`, `KEY_ROTATION_LINEAGE`, `MULTIPLE_SIGNERS`), and package
42+
query/filtering methods.
4143
- `test_inclusion_proof_check.py`: Tests pre-fetching transparency log entries
4244
(`--cache_prefetch_concurrency`, `--cache_prefetch_timeout`, `--cache_dir`),
4345
opt-out (`--no_prefetch`), fail-open fallback on pre-fetch errors/timeouts,
@@ -70,6 +72,11 @@ build.gradle file of the Hubble app.
7072
> intentionally break compatibility; output files produced by Hubble `1.0.0`
7173
> (or any version `< 2.1.0`) and higher major versions (`>= 3.0.0`) are **not
7274
> supported**.
75+
>
76+
> Minor versions within `2.x` are **additive** and are read on a best-effort
77+
> basis, so previously collected corpora stay readable. For example `2.2.0`
78+
> adds the `signingInfo` object; when reading `2.1.0` output the signing-mode
79+
> helpers report `UNKNOWN` instead of rejecting the observation.
7380
7481
<!-- TODO: Remove legacy risk-scoring documentation (docs/Uraniborg's Preloaded App Risks Scoring Metrics (2020-08) v1.0.pdf) and remaining legacy result categorization helpers in hubble_parser.py / automate_observation.py. -->
7582

‎uraniborg/VERSION‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.1.0
1+
2.2.0

0 commit comments

Comments
 (0)