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):
- iOS AASA —
https://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.
- Android Digital Asset Links —
https://invitations.dashpay.io/.well-known/assetlinks.json, listing the app package name + release signing-cert SHA-256 fingerprint.
- 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
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 owndash.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)
dashpay/dashwallet-ios): entitlementapplinks:invitations.dashpay.io;DWInvitationLinkBuilder.mbuilt invitation links onhttps://invitations.dashpay.io/link.dashpay/dash-wallet):AndroidManifest.xmlhas anautoVerify="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.jsonand 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 adash.orgsubdomain):https://invitations.dashpay.io/.well-known/apple-app-site-association(served asapplication/json, no redirects), listing the invitation app'sappID(TeamID.BundleID) and the/invitepath.https://invitations.dashpay.io/.well-known/assetlinks.json, listing the app package name + release signing-cert SHA-256 fingerprint.https://invitations.dashpay.io/invite?data=<payload>; ideally also a web fallback/preview page for recipients without the app installed (mirroring the oldinvite-preview) + an app-store redirect. Thedatapayload is the existing self-contained envelope — no server-side state needed.(Which team/host now controls
invitations.dashpay.iopost-Firebase needs to be identified — that path used to resolve through Firebase.)App-side companion change (tracked separately / follow-up PR)
applinks:invitations.dashpay.ioassociated-domains entitlement to the app.https://invitations.dashpay.io/invite?data=…instead ofdashpay://invite?data=….continueUserActivity(iOS) / the verified App Link intent (Android).dashpay://custom scheme as an offline/side-channel fallback (e.g. QR shown in-person).Scope of the benefit (be precise)
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