fix(#62): read service signers from the APK Signing Block (0.4.1) - #69
Merged
Merged
Conversation
On the NP02J/K68 (Android 13) 0.4.0 downloaded the pinned display services (size + sha256 OK) and then refused device-service: "Android could not read the signing certificate". Both service APKs are signed with APK Signature Scheme v2 only, and their certificates are the pinned ones. Root cause is the platform: in android13-release, PackageManager.getPackageArchiveInfo() calls collectCertificates() only when the flags contain the deprecated GET_SIGNATURES. GET_SIGNING_CERTIFICATES alone leaves SigningDetails.UNKNOWN and generatePackageInfo() returns signingInfo = null. Android 12 (ApplicationPackageManager) and 13 QPR1+ test either flag, which is why this only shows on some framework builds. Fix: - ApkSignatureReader: a pure-Kotlin v2/v3/v3.1 verifier. Finds the EOCD, the APK Signing Block, picks the scheme the platform would use for the SDK (v3.1 -> v3 -> v2), verifies the signer's signature over its signed data, that the public key is the certificate's, and the whole-file content digest; returns the SHA-256 of each signer certificate. - archiveIdentity also asks Android with GET_SIGNATURES | GET_SIGNING_CERTIFICATES and DisplayServices.combineSigners() cross-checks the two: a block that does not verify, or a disagreement, is refused; v1-only falls back to Android. archiveProblem still requires exactly the pinned (== manifest) signer. - Tests sign synthetic APKs at test time with apksig + bouncycastle (v1-only, v2-only, ECDSA, v2+v3, rotation via v3 and v3.1, two signers, tampered contents / central directory / certificate, truncated, unsigned). Every guard was mutation-checked. The real vendor APKs are checked when DXR_SERVICE_APK_DIR is set locally (never committed). - installer 0.4.1 (versionCode 6). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
Device-verified 2026-09-30 on NP02J (K68, Android 13 / API 33) from FACTORY display services (0.8.29 / 0.8.20), using a local debug build of this branch:
🤖 Generated with Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What broke
On an NP02J/K68 (Android 13, API 33), installer 0.4.0 downloaded the pinned display services from
updates.displayxr.org/services/cnsdk/v0.10.69/. Size and sha256 matched. It then refused device-service with "Android could not read the signing certificate of device-service.apk … Refused.", and skipped head tracking because the two install as a pair. The final status correctly said NOT READY.Both service APKs are signed with APK Signature Scheme v2 only (apksigner: v1 false, v2 true, v3 false; 1 signer), and their certificates are the pinned ones.
Root cause: an Android 13 platform bug, not the file
android13-releasePackageManager.getPackageArchiveInfo:The certificates are collected only for the deprecated
GET_SIGNATURESflag. WithGET_SIGNING_CERTIFICATESalone, which is what 0.4.0 passed,SigningDetailsstaysUNKNOWNandgeneratePackageInfosetssigningInfo = null.android13-qpr1-releaseonward, Android 14, and Android 12 (ApplicationPackageManager,collectCertificates = GET_SIGNATURES || GET_SIGNING_CERTIFICATES) all accept either flag. So the result depends on which framework build the tablet runs.Fix
ApkSignatureReader: a pure-Kotlin verifier for APK Signature Scheme v2, v3 and v3.1. It works through EOCD → central directory offset →APK Sig Block 42→ pair0x1b93ad61/0xf05368c0/0x7109871a, choosing the scheme the platform uses for the device's SDK (v3.1, then v3, then v2, which gives the current key after a rotation). It does not only parse. For each signer it verifies:It returns the SHA-256 of each signer certificate. A certificate pasted into a forged block is refused, and so is a single flipped byte anywhere in the file.
archiveIdentityalso asks Android, now withGET_SIGNING_CERTIFICATES | GET_SIGNATURES.DisplayServices.combineSignersthen combines the two answers:archiveProblemis unchanged in substance: exactly one signer, equal to the pin (and to the manifest). Behind it, Android still refuses any update to a system app that is not signed with that app's key.Installer 0.4.1 (versionCode 6). The README's trust section and layout are updated.
Tests
ApkSignatureReaderTest(19 tests) signs synthetic APKs at test time withapksig+bcpkix, both test-only dependencies. No vendor bytes and no key are committed. The APKs are larger than 2 MiB, so the digest spans several chunks. Coverage:NoSigningBlock), unsigned, not a ZIP, a content byte changed, a central-directory byte changed, the certificate changed inside the block, a truncated filecombineSignersbranchesMutation-checked: removing the content-digest check, the signature check, v3.1 selection, v3 selection, the disagreement refusal or the invalid-block refusal each fails at least one test.
The real vendor APKs were run locally through
DXR_SERVICE_APK_DIR(the test is skipped when that is unset; the files are never committed). Result:v2at API 29/31/33/35, device-servicedbc2792f775902a74302d500ab00c6224a4aeb89adb7d5dfdd4c3eeba763811c, head tracking113ec0526354587c047183f7f6c1d90ee06d1286b0e848a168b38ad131c05096.archiveProblemreturns null for both, with Android's empty answer simulated.Not done
🤖 Generated with Claude Code