|
| 1 | +--- |
| 2 | +title: "JIT Account-Access Permission for ProductAccountId Methods" |
| 3 | +owner: "@filvecchiato" |
| 4 | +--- |
| 5 | + |
| 6 | +# RFC 0012 — JIT Account-Access Permission for ProductAccountId Methods |
| 7 | + |
| 8 | +## Summary |
| 9 | + |
| 10 | +This RFC introduces a per-account just-in-time (JIT) permission check for all Host API methods that accept a `ProductAccountId`. Today, any product can call `host_account_get`, `host_account_get_alias`, `host_account_create_proof`, signing methods, `host_create_transaction`, `remote_statement_store_create_proof`, and `host_payment_top_up` (via the `ProductAccount` source variant) with an arbitrary `ProductAccountId` — including identifiers belonging to other products — without user awareness or consent. This RFC requires the host to obtain explicit user approval before granting access to a specific `ProductAccountId`, preventing cross-product identity leakage. |
| 11 | + |
| 12 | +## Motivation |
| 13 | + |
| 14 | +`ProductAccountId` is a `(DotNsIdentifier, DerivationIndex)` tuple. The `DotNsIdentifier` component is a product's registered DotNS name, and the `DerivationIndex` selects a specific derived account under that product's namespace. The host derives cryptographic keys, aliases, and proofs from this identifier — all of which are sensitive, user-specific material scoped to a product domain. |
| 15 | + |
| 16 | +Currently, none of the methods that accept `ProductAccountId` enforce that the calling product is authorized to access the requested domain. A malicious product can: |
| 17 | + |
| 18 | +1. Call `host_account_get(["legitimate-product.dot", 0])` to obtain the user's derived public key for another product's domain. |
| 19 | +2. Call `host_account_get_alias` to learn the user's contextual alias in another product's ring VRF context. |
| 20 | +3. Call `host_account_create_proof` to generate valid ring VRF proofs under another product's identity. |
| 21 | +4. Call `host_sign_raw` or `host_sign_payload` with another product's account to produce signatures. |
| 22 | +5. Call `host_create_transaction` to create signed transactions using another product's derived key. |
| 23 | +6. Call `remote_statement_store_create_proof` to create statement proofs under another product's account. |
| 24 | + |
| 25 | +This enables cross-product user tracking and identity correlation without the user's knowledge — the same class of privacy concern that motivated the JIT permission model for `host_account_get_root` (RFC-0010). |
| 26 | + |
| 27 | +## Detailed Design |
| 28 | + |
| 29 | +### Permission model |
| 30 | + |
| 31 | +The host MUST maintain a per-product, per-`ProductAccountId` permission grant. When a product calls any method that includes a `ProductAccountId` (either as a direct parameter or embedded in a request struct), the host MUST check whether the calling product has been granted access to that specific `ProductAccountId`. |
| 32 | + |
| 33 | +The permission key is the full `ProductAccountId` tuple `(DotNsIdentifier, DerivationIndex)`. A grant for `("example.dot", 0)` does NOT extend to `("example.dot", 1)`. |
| 34 | + |
| 35 | +### Permission lifecycle |
| 36 | + |
| 37 | +1. **First call** — When a product calls a `ProductAccountId`-bearing method for the first time with a given `ProductAccountId`, the host presents an approval dialog to the user. The dialog MUST identify the requesting product and the target `ProductAccountId` (at minimum the `DotNsIdentifier`). |
| 38 | +2. **Approved** — The host caches the grant and proceeds with the method. Subsequent calls from the same product with the same `ProductAccountId` resolve immediately without prompting. |
| 39 | +3. **Denied** — The method returns its domain-specific rejection error: |
| 40 | + - `host_account_get`, `host_account_get_alias`: `RequestCredentialsErr::Rejected` |
| 41 | + - `host_account_create_proof`: `CreateProofErr::Rejected` |
| 42 | + - `host_sign_raw`, `host_sign_payload`: `SigningErr::PermissionDenied` |
| 43 | + - `host_create_transaction`: `CreateTransactionErr::PermissionDenied` |
| 44 | + - `remote_statement_store_create_proof`: `StatementProofErr::Rejected` |
| 45 | + - `host_payment_top_up` (with `PaymentTopUpSource::ProductAccount`): `PaymentTopUpErr::Rejected` |
| 46 | +4. **Grant persistence** — The host SHOULD persist grants across sessions for the same product identity. Session-scoped grants are acceptable as a minimum conforming implementation. |
| 47 | + |
| 48 | +### Same-domain optimization |
| 49 | + |
| 50 | +When the calling product's own `DotNsIdentifier` matches the `DotNsIdentifier` in the `ProductAccountId`, the host MAY skip the permission prompt and grant access implicitly. This is the common case: a product accessing its own derived accounts. The host MUST still prompt when the `DotNsIdentifier` differs from the calling product's identity. |
| 51 | + |
| 52 | +Hosts that cannot reliably determine the calling product's identity (e.g. during development or in permissive sandbox modes) MUST fall back to prompting for all `ProductAccountId` requests. |
| 53 | + |
| 54 | +### Affected methods |
| 55 | + |
| 56 | +| Method | `ProductAccountId` location | Rejection error | |
| 57 | +|--------|---------------------------|-----------------| |
| 58 | +| `host_account_get` | Direct parameter | `RequestCredentialsErr::Rejected` | |
| 59 | +| `host_account_get_alias` | Direct parameter | `RequestCredentialsErr::Rejected` | |
| 60 | +| `host_account_create_proof` | First element of tuple parameter | `CreateProofErr::Rejected` | |
| 61 | +| `host_sign_raw` | `SigningPayloadRaw.account` field | `SigningErr::PermissionDenied` | |
| 62 | +| `host_sign_payload` | `SigningPayload.account` field | `SigningErr::PermissionDenied` | |
| 63 | +| `host_create_transaction` | First element of tuple parameter | `CreateTransactionErr::PermissionDenied` | |
| 64 | +| `remote_statement_store_create_proof` | First element of tuple parameter | `StatementProofErr::Rejected` | |
| 65 | +| `host_payment_top_up` | `PaymentTopUpSource::ProductAccount` variant | `PaymentTopUpErr::Rejected` | |
| 66 | + |
| 67 | +### API changes |
| 68 | + |
| 69 | +No new methods or types are introduced. The existing error variants (`Rejected`, `PermissionDenied`) already cover the denial case. The change is purely behavioral: the host MUST perform the permission check before dispatching to the handler. |
| 70 | + |
| 71 | +### Interaction with existing permission systems |
| 72 | + |
| 73 | +- **Remote permissions (RFC-0002)**: Account-access permission is orthogonal to remote permissions. A product that has `RemotePermission::ChainSubmit` still needs account-access permission before calling `host_create_transaction` with a cross-domain `ProductAccountId`. |
| 74 | +- **Signing confirmation flow**: The per-operation signing confirmation (the dialog that shows "sign this payload?") remains unchanged. Account-access permission is checked first; if granted, the signing confirmation flow proceeds as before. |
| 75 | +- **`host_account_get_root` (RFC-0010)**: Root account access has its own independent JIT permission. This RFC does not affect it. |
| 76 | + |
| 77 | +### Implementation guidance for `host-container` |
| 78 | + |
| 79 | +In the `host-container` package, the affected slots should be changed from `makeNotImplementedSlot` to a new pattern (e.g. `makeAccountGatedRequestSlot`) that: |
| 80 | + |
| 81 | +1. Extracts the `ProductAccountId` from the incoming request payload. |
| 82 | +2. Checks the permission cache. |
| 83 | +3. If not cached, delegates to a host-provided permission callback to prompt the user. |
| 84 | +4. On approval, caches the grant and calls the handler. |
| 85 | +5. On denial, returns the appropriate error without calling the handler. |
| 86 | + |
| 87 | +## Drawbacks |
| 88 | + |
| 89 | +**Prompt fatigue.** Products that legitimately need cross-domain account access will trigger a permission prompt on each new `ProductAccountId`. For products that access many accounts across different domains, this could be disruptive. The same-domain optimization mitigates the common case. |
| 90 | + |
| 91 | +**Per-derivation-index granularity may be too fine.** Requiring separate grants for `("example.dot", 0)` and `("example.dot", 1)` provides maximum privacy but could annoy users when a product uses multiple derivation indices under the same domain. An alternative would be to grant per-`DotNsIdentifier` (see Alternatives). |
| 92 | + |
| 93 | +**No revocation signal.** Like other permission-based methods (RFC-0002, RFC-0010), there is no push notification to the product if the user later revokes the grant. Products should handle rejection errors at any point. |
| 94 | + |
| 95 | +## Alternatives |
| 96 | + |
| 97 | +### Per-DotNsIdentifier grants (instead of per-ProductAccountId) |
| 98 | + |
| 99 | +A simpler model would grant access to all derivation indices under a `DotNsIdentifier` with a single prompt. This reduces prompt frequency but allows a product to enumerate all derivation indices under a domain once access is granted. This may be acceptable if the privacy concern is primarily about cross-domain leakage rather than intra-domain enumeration. |
| 100 | + |
| 101 | +### Domain-scoping enforcement (no prompts) |
| 102 | + |
| 103 | +Instead of a JIT prompt, the host could silently reject any request where the `DotNsIdentifier` does not match the calling product's registered identity. This is simpler but prevents legitimate cross-domain use cases (e.g. a product that aggregates accounts across multiple DotNS domains with user consent). |
| 104 | + |
| 105 | +### Combine with `host_account_get_root` permission |
| 106 | + |
| 107 | +If a product has already received `host_account_get_root` approval (RFC-0010), the host could implicitly grant access to the root account's `ProductAccountId` without an additional prompt. This RFC does not mandate this optimization but hosts MAY implement it. |
| 108 | + |
| 109 | +## Unresolved Questions |
| 110 | + |
| 111 | +1. **Grant granularity.** Should grants be per-`ProductAccountId` (as proposed) or per-`DotNsIdentifier`? The former is more private; the latter is more ergonomic. Feedback from host implementors is needed. |
| 112 | + |
| 113 | +2. **`host_payment_top_up` with `PrivateKey` source.** The `PaymentTopUpSource` enum also has a `PrivateKey` variant that does not use `ProductAccountId`. Should payment top-ups from private keys have their own permission gate? This is out of scope for this RFC but worth considering. |
| 114 | + |
| 115 | +3. **Batch consent.** Should there be a mechanism for a product to declare all the `ProductAccountId`s it intends to use upfront, so the user gets a single prompt? This would parallel `remote_permission`'s batching model. |
| 116 | + |
| 117 | +4. **Re-prompt policy.** If the user denied access in a previous session, should the product be able to trigger a new prompt in a new session? The protocol is silent on this; it is left to host implementations. |
0 commit comments