@@ -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
0 commit comments