Skip to content

Invitation links: migrate DIP-13 invitations from custom URL scheme to Universal/App Links on invitations.dashpay.io (post-Firebase) #4096

Description

@shumkov

Summary

The new Platform DashPay invitation feature (PR #4041) currently delivers invitation links via a custom URL scheme: dashpay://invite?data=<self-contained payload>. The payload is a one-time bearer voucher key (whoever holds the link can claim the funded identity).

Custom URL schemes are not exclusive on iOS/Android — any installed app can register dashpay:// and intercept the link when the user taps it (URL-scheme hijacking), potentially claiming the voucher first. This was raised in the PR security review.

The standard industry mitigation is Universal Links (iOS) / App Links (Android): domain-verified https:// links that the OS cryptographically binds to our app via a hosted association file, so no other app can claim them. We own dash.org / invitations.dashpay.io, so this is fully within reach — and the classic wallets already did it.

This issue is primarily an infrastructure/hosting task, with a small companion app change.

Prior art in the org (already used this pattern)

  • iOS (dashpay/dashwallet-ios): entitlement applinks:invitations.dashpay.io; DWInvitationLinkBuilder.m built invitation links on https://invitations.dashpay.io/link.
  • Android (dashpay/dash-wallet): AndroidManifest.xml has an autoVerify="true" App Link intent-filter (https://dashpay.onelink.me).

Why it's not just "turn it back on": the classic iOS builder used Firebase Dynamic Links (FIRDynamicLinkComponents, domainURIPrefix = https://invitations.dashpay.io/link), and Google shut Firebase Dynamic Links down (Aug 2025) — which is exactly why the new implementation ships a self-contained custom-scheme link instead.

Key point: the anti-hijack property comes from the apple-app-site-association (AASA) file on the domain + the app entitlement — not from Firebase. Firebase was only a shortener/redirect/preview layer on top. Since we control the domain, we can host our own static AASA / assetlinks.json and get the full protection with no third-party dependency.

Infra team asks

Host static association files on a domain we control (reuse invitations.dashpay.io, or pick a dash.org subdomain):

  1. iOS AASAhttps://invitations.dashpay.io/.well-known/apple-app-site-association (served as application/json, no redirects), listing the invitation app's appID (TeamID.BundleID) and the /invite path.
  2. Android Digital Asset Linkshttps://invitations.dashpay.io/.well-known/assetlinks.json, listing the app package name + release signing-cert SHA-256 fingerprint.
  3. Invitation URL endpoint — serve https://invitations.dashpay.io/invite?data=<payload>; ideally also a web fallback/preview page for recipients without the app installed (mirroring the old invite-preview) + an app-store redirect. The data payload is the existing self-contained envelope — no server-side state needed.

(Which team/host now controls invitations.dashpay.io post-Firebase needs to be identified — that path used to resolve through Firebase.)

App-side companion change (tracked separately / follow-up PR)

  • Add the applinks:invitations.dashpay.io associated-domains entitlement to the app.
  • Build invitation links as https://invitations.dashpay.io/invite?data=… instead of dashpay://invite?data=….
  • Handle inbound links via continueUserActivity (iOS) / the verified App Link intent (Android).
  • Keep the dashpay:// custom scheme as an offline/side-channel fallback (e.g. QR shown in-person).

Scope of the benefit (be precise)

  • Closes the on-device app-hijack vector (the specific review finding).
  • ⚠️ Does not remove the inherent bearer-link transfer exposure (the key is still plaintext in the link wherever it travels — messaging, clipboard, screenshots). That remains mitigated by design: the amount is capped (MAX_INVITATION_DUFFS, 0.01 DASH), the voucher is single-use (deterministic on-chain rejection of a second consume), there's a short advisory expiry, and the UI treats the link as sensitive. Fully eliminating the transfer exposure would require a custodial/server-escrow model, which conflicts with the self-custodial, no-account design of Platform invitations — out of scope.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions