Skip to content

feat(dashpay): select App Connect encryption keys by the DIP rotation rules - #1258

Open
PastaPastaPasta wants to merge 2 commits into
feat/app-connect-dashpay-carrierfrom
feat/app-connect-encryption-rotation
Open

PastaPastaPasta wants to merge 2 commits into
feat/app-connect-dashpay-carrierfrom
feat/app-connect-encryption-rotation

Conversation

@PastaPastaPasta

Copy link
Copy Markdown
Member

Problem

App Connect encryption keys are derived at m/9'/coin'/5'/7'/0'/identityId'/contractId'/purpose'. The DIP adds a rotation' level after purpose'. That path becomes permanent once the first mainnet key is registered, so it has to be right in the first mainnet release. Testnet keys that today's builds registered at the old 8-level path have to keep working.

The wallet also derived just one key and compared it with Platform's current key. That broke DIP rule 2: under MultipleReferenceToLatest, a disabled current key from another wallet read as .absent, and the login registered a new key next to it. The DIP says to leave the slot out and register nothing.

Fix

For each granted encryption binding, the wallet follows the DIP "Encryption Keys" rules:

  • Derivable keys (AppConnect.derivableEncryptionKeys): first rotation 0' at the new 9-level path, then 1', 2', and so on. The search stops at the first rotation whose key is not on the identity in any state, and that key is wiped. The identity's key count caps the search. On testnet and devnet only, the wallet also derives the legacy 8-level key with deriveLegacyAppEncryptionKey. Mainnet never had keys at the legacy path, so the wallet doesn't derive it there.
  • Rule 1: if a current key is live, nothing is registered. The wallet sends that key's private key if it matches one of the derivable keys, and leaves the slot out otherwise.
  • Rule 2: if a current key is recorded but disabled, the identity has a key for this contract and purpose and none of them is live. The slot is left out and nothing is registered, under both unique and latest. Under latest, a confirming re-read is still required, as before, because a newer live key may not show up yet on a node that is behind.
  • Rule 3: if there is no current key and no key for the binding, the wallet registers rotation 0' at the new path and sends it.

encryptionKeyState now takes every derivable public key, not just one. A new encryptionSlot(for:…) maps a settled state to send / register / omit and picks which derived private key to send. Encryption keys are now derived after the identity's keys are fetched, because the rotation search needs them. Every derived private key is wiped when the approval finishes.

Stacked on #1257

Base is feat/app-connect-dashpay-carrier (#1257). Merge that first.

Platform bump

.platform-ref moves from 063589bf to 03945d3a (dashpay/platform#5649, squash-merged into v5.1-dev), which adds the rotation level to deriveConnectKey and adds deriveLegacyAppEncryptionKey. The only compile break across the 55 commits is the call site that used the removed deriveConnectKey(subFeature: .appEncryption, …, purpose:, network:) overload, and this change replaces it. The other Swift SDK changes in that range are additive (SDK.protocolVersionInfo(), ProtocolVersionInfo) or internal (getEpochsInfo keeps its signature). The range also brings in the Rust side of several ! platform changes (dashpay/platform#4899, dashpay/platform#5533, dashpay/platform#5538, dashpay/platform#5561, dashpay/platform#5578, dashpay/platform#5579, dashpay/platform#5584, dashpay/platform#5632). Most of them are gated on protocol version 15. This PR depends on none of them, and only the App Connect suites were run against the bump.

The first commit pins the platform and passes rotation: 0 at the old call site, so it builds on its own. The second commit applies the rules.

Out of scope

  • Mainnet enablement of App Connect, plus its gates, session keys, budgets, signing-key selection and brand map. The mobile team owns those.
  • A rotate-key action. The DIP makes it a MAY, and it must never be part of a login.

How tested

On the Mac Studio, headless. I built the DashSDKFFI.xcframework simulator slice from platform 03945d3a, then ran xcodebuild test (scheme dashwallet-dashpay, Debug, iOS Simulator, arm64) with -only-testing on the App Connect suites: DashConnectUriTests, AppConnectV2LoginTests, PlatformDashConnectDataSourceTests, DashConnectStoreTests, LoginKeyDerivationTests and the new AppConnectEncryptionRotationTests.

Result: ** TEST SUCCEEDED **. 186 tests ran with 0 failures:

Suite Tests
AppConnectEncryptionRotationTests (new) 12
AppConnectV2LoginTests 46
PlatformDashConnectDataSourceTests 47
DashConnectUriTests 59
DashConnectStoreTests 17
LoginKeyDerivationTests 5

New tests in AppConnectEncryptionRotationTests:

  • Current key at rotation 0: sent, nothing registered.
  • Current key at rotation 1 with rotation 0 disabled: rotation 1 sent.
  • Legacy current key on testnet and devnet: sent.
  • Legacy key on mainnet: not derived, slot left out.
  • Current key the wallet can't derive: left out.
  • Only a disabled foreign key: left out, nothing registered (the bug).
  • Nothing on the identity: rotation 0 registered at the new path.
  • The rotation search stops at the first gap and ignores keys of the other purpose.
  • The DIP "Key Derivation Vectors" through the wallet's own call path (PlatformDashConnectDataSource.derivableEncryptionKeys → ManagedPlatformWallet): ENCRYPTION rotations 0'/1' for 0x35…/0x6b…, and DECRYPTION rotations 0'/1' for asc/desc.

Two existing AppConnectV2LoginTests assertions expected the old rule-2 behaviour (.absent / .absentIfSettled behind a disabled current key). They now expect .notThisWallets / .notThisWalletsIfSettled.

🤖 Generated with Claude Code

PastaPastaPasta and others added 2 commits October 29, 2026 02:05
dashpay/platform#5649 (03945d3a on v5.1-dev) adds the DIP-13 rotation'
level to App Connect encryption keys: deriveConnectKey for .appEncryption
now requires a rotation, and deriveLegacyAppEncryptionKey derives the
earlier 8-level path. The path becomes permanent with the first mainnet
key, so the wallet has to derive it before mainnet ships.

The only compile break in the 55 commits since 063589bf is the removed
rotation-less .appEncryption overload; the call site passes rotation 0
here. Until the next commit selects keys by the DIP rules, this derives
only rotation 0', so testnet keys at the old path read as another
wallet's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… rules

The wallet derived one encryption key per binding and compared it with
Platform's current key. With the rotation level, the identity's current
key may be any rotation this wallet registered, or on testnet a key at
the earlier 8-level path that today's builds registered. And a disabled
current key of another wallet's read as absent under
MultipleReferenceToLatest, so the login registered a new key beside it,
where DIP rule 2 says to leave the slot out.

Per granted binding the wallet now derives rotation 0', then 1', 2', ...
while each is on the identity in any state (stopping at the first that is
not), plus the earlier path on testnet and devnet only. A live current
key among them is sent and nothing is registered; any other live current
key, or a disabled one, leaves the slot out; only an identity with no key
for the contract and purpose gets rotation 0' registered. The keys are
derived after the identity's keys are fetched, and the candidates no slot
carries are wiped before the update and the publish.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 29, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e291fb9b-3ad0-490b-a080-aee30fe7f018

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thepastaclaw

thepastaclaw commented Oct 29, 2026 •

Copy link
Copy Markdown

✅ Final review complete — no blockers (commit 41b4f17)

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Final validation — Phase 1 + Phase 2

The rotation-path selection, legacy compatibility, disabled-current-key handling, cleanup, and target membership are consistent with the stated DIP rules. One non-blocking performance issue remains: approval derives every contiguous rotation candidate synchronously on the main actor, and the simulator test check is still in progress.

🟡 1 suggestion(s)

Review provenance

Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)

  • Triage: critical by gpt-6.1-sol (effort low) — This is a large, intricate change to App Connect encryption-key derivation, rotation, private-key selection, and wiping in AppConnectV2Login.swift and PlatformDashConnectDataSource.swift, directly changing cryptographic key handling.
  • Phase 1 reviewers: muse-spark-1.3-contributor — general (completed, effort xhigh); agent phase1-reviewer
  • Phase 1 model: muse-spark-1.3-contributor — not quota-gated; passed over gemini-3.8-flash-high (antigravity quota lookup failed), glm-5.3-flash (not used above high effort; tier asks max)
  • Single stage: Phase 1 and Phase 2 reviewed this head side by side, with no blocker gate between them (triage tier)
  • Fresh verifier: gpt-6.1-sol — final-verifier; agent sol-verifier
  • Phase 2 reviewers: gpt-6.1-sol — general (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — ffi-engineer (completed, effort xhigh); agent phase2-reviewer, gpt-6.1-sol — security-auditor (completed, effort xhigh); agent phase2-reviewer
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `DashWallet/Sources/Models/DashConnect/PlatformDashConnectDataSource.swift`:
- [SUGGESTION] DashWallet/Sources/Models/DashConnect/PlatformDashConnectDataSource.swift:1452-1456: Avoid deriving every rotation candidate synchronously on the main actor
  `derivableEncryptionKeys` derives rotation 0 and then every contiguous rotation until the first gap, capped only by the number of identity keys for that purpose. This call is wrapped in one `MainActor.run` block, and each iteration performs a synchronous SDK/FFI derivation. The platform permits up to 15,000 identity public keys, so an identity with a long contiguous rotation history can make approval perform thousands of derivations while blocking the main actor, causing the App Connect approval UI to appear hung. Provide a bulk/range derivation API in SwiftDashSDK, or another SDK-supported off-main execution path that preserves the required private-key lifetime and wiping guarantees.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants