You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Postponed from the deep-link integration review (see PRs #3, #4, #6). Capturing the full context so it's actionable later.
Why this matters (the bug it would catch)
Android App Links (the https:// links that open the app directly, no browser/chooser) are verified by certificate fingerprint. The domain hosts /.well-known/assetlinks.json listing the package name and the SHA-256 of the certificate the installed app is signed with. On device, Android checks whether the installed app's signing cert SHA-256 is in that file. Match → links open the app. No match → verification silently fails and the link opens in the browser — no error, nothing user-visible.
Play App Signing breaks the naive setup. Mandatory for new apps since Aug 2021: you sign your upload with your upload key, but Google Play re-signs the delivered app with a different key that Google holds (the app signing key). So the cert on the user's device is Google's, not your upload key.
The trap: developers run keytool on their local keystore, register the upload key's SHA-256 in assetlinks.json, and every Play-installed app is signed with Google's key instead → fingerprint never matches → App Links silently never open. The two hashes are unrelated, so nothing errors. This is believed to be the single most common Android App Links failure.
The correct fingerprint to register is the app signing key SHA-256 (Play Console → App integrity → App signing, or the Play Developer API), ideally alongside the upload key so it works whether installed from Play or sideloaded. (Example of the healthy state: a project with 3 fingerprints registered — upload + Play app signing + debug.)
verify already fetches and parses the hosted assetlinks.json (WellKnownTester.testAssetLinksFile) and compares it against ulinkConfig.androidSha256Fingerprints. This feature adds a check that the fingerprint which will actually validate on device is present.
Approaches (pick during implementation)
Advisory only (credential-free). When checking Android assetlinks.json, if the app looks Play-distributed and the registered set looks like it holds only an upload key, warn: "Play App Signing re-signs your app with a different certificate — register the Play App Signing SHA-256, not just your upload key." No setup; helps everyone; advises but can't confirm.
Full Play Developer API check (opt-in).ulink verify --play-service-account key.json authenticates to the Google Play Developer API, fetches the real app signing key certificate for the package, and does a definitive compare against the hosted assetlinks.json. Definitive, but requires a Google service account with Play Console access; larger build.
Both (layered) — recommended. Advisory fires by default for everyone; --play-service-account upgrades it to the definitive comparison.
Implementation notes
Play API:androidpublisher/v3 — the app signing certificate SHA-256 is exposed on generated APKs (e.g. .../applications/<packageName>/generatedApks/<versionCode>, field certificateSha256Hash). Needs OAuth2 via a service account (JWT signed with the SA private key → token exchange, scope https://www.googleapis.com/auth/androidpublisher).
Credentials: accept a service-account JSON via --play-service-account <path> and/or GOOGLE_APPLICATION_CREDENTIALS. Never require it — the check must degrade to advisory (or skip) when absent, and must not break unauthenticated runs.
versionCode resolution: needs a strategy (latest production APK? a flag?). Document the choice.
Given a hosted assetlinks.json missing the Play app signing SHA-256, verify surfaces it (warning by default; error with --play-service-account when the mismatch is confirmed).
Given the Play app signing SHA-256 IS present, it passes.
No credentials / no Play access → the deep check is reported as skipped, not passed, and the run is unaffected otherwise.
Postponed from the deep-link integration review (see PRs #3, #4, #6). Capturing the full context so it's actionable later.
Why this matters (the bug it would catch)
Android App Links (the
https://links that open the app directly, no browser/chooser) are verified by certificate fingerprint. The domain hosts/.well-known/assetlinks.jsonlisting the package name and the SHA-256 of the certificate the installed app is signed with. On device, Android checks whether the installed app's signing cert SHA-256 is in that file. Match → links open the app. No match → verification silently fails and the link opens in the browser — no error, nothing user-visible.Play App Signing breaks the naive setup. Mandatory for new apps since Aug 2021: you sign your upload with your upload key, but Google Play re-signs the delivered app with a different key that Google holds (the app signing key). So the cert on the user's device is Google's, not your upload key.
The trap: developers run
keytoolon their local keystore, register the upload key's SHA-256 inassetlinks.json, and every Play-installed app is signed with Google's key instead → fingerprint never matches → App Links silently never open. The two hashes are unrelated, so nothing errors. This is believed to be the single most common Android App Links failure.The correct fingerprint to register is the app signing key SHA-256 (Play Console → App integrity → App signing, or the Play Developer API), ideally alongside the upload key so it works whether installed from Play or sideloaded. (Example of the healthy state: a project with 3 fingerprints registered — upload + Play app signing + debug.)
verifyalready fetches and parses the hostedassetlinks.json(WellKnownTester.testAssetLinksFile) and compares it againstulinkConfig.androidSha256Fingerprints. This feature adds a check that the fingerprint which will actually validate on device is present.Approaches (pick during implementation)
assetlinks.json, if the app looks Play-distributed and the registered set looks like it holds only an upload key, warn: "Play App Signing re-signs your app with a different certificate — register the Play App Signing SHA-256, not just your upload key." No setup; helps everyone; advises but can't confirm.ulink verify --play-service-account key.jsonauthenticates to the Google Play Developer API, fetches the real app signing key certificate for the package, and does a definitive compare against the hostedassetlinks.json. Definitive, but requires a Google service account with Play Console access; larger build.--play-service-accountupgrades it to the definitive comparison.Implementation notes
androidpublisher/v3— the app signing certificate SHA-256 is exposed on generated APKs (e.g..../applications/<packageName>/generatedApks/<versionCode>, fieldcertificateSha256Hash). Needs OAuth2 via a service account (JWT signed with the SA private key → token exchange, scopehttps://www.googleapis.com/auth/androidpublisher).--play-service-account <path>and/orGOOGLE_APPLICATION_CREDENTIALS. Never require it — the check must degrade to advisory (or skip) when absent, and must not break unauthenticated runs.assetlinks.jsoncheck; emit awarning(advisory) orerror(confirmed mismatch), consistent with the skipped/PARTIAL reporting from fix(verify): disclose skipped cross-checks and report URL schemes per platform #3 (a skipped Play check should surface as skipped, not silently pass).Acceptance criteria
assetlinks.jsonmissing the Play app signing SHA-256,verifysurfaces it (warning by default; error with--play-service-accountwhen the mismatch is confirmed).Related: #3 (verify disclosure), #4 (per-target bundle id), #6 (
--strict).