Skip to content

feat(verify): detect Play App Signing certificate mismatch in Android App Links #7

Description

@mohn93

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)

  1. 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.
  2. 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.
  3. 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.
  • Hook point: extend the existing Android assetlinks.json check; emit a warning (advisory) or error (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

  • 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.

Related: #3 (verify disclosure), #4 (per-target bundle id), #6 (--strict).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions