fix(audit): rotate program ID after committed keypair leak (MULT-19)#30
Merged
Conversation
Compute Unit Report
Generated: 2026-04-28 |
3 tasks
3ddc3cb to
6155c84
Compare
- Rotate declare_id! and IDL publicKey to De1egAFM…vR44 (matches README) - Remove committed keys/subscriptions-keypair.json, gitignore keys/ - Switch surfpool to keypair-less local install via svm::setup_surfnet runbook (runbooks/surfnet-setup/) - Justfile recipes derive program ID from declare_id! via sed; IDL deploys take keypair via PROGRAM_UPGRADE_AUTHORITY_KEYPAIR env var - Drop dev-build-keypair CI artifact (no longer used) - Webapp scripts/server use hardcoded program-ID const instead of reading the keypair file
6155c84 to
40be1d2
Compare
- Compile-time assertions for Header::LEN, header field offsets, and FixedDelegation / RecurringDelegation / SubscriptionDelegation LEN - Document release-gate in docs/003: future layout changes must bump CURRENT_VERSION and ship a migrate IX - Annotate CURRENT_VERSION with the v1 lock note CURRENT_VERSION stays at 1.
- PLAN_LEN_V1 + assert in plan.rs - PLAN_DATA_LEN_V1 + assert in create_plan.rs
Skip rollover when candidate_start >= expiry_ts. Pulls within drift window remain valid in the final authorized period; no fresh allowance is granted in terminal periods. Affects validate_recurring_transfer (used by recurring delegations and subscription transfers).
Extract is_effectively_expired helper. Sponsor revocation now waits the same TIME_DRIFT_ALLOWED_SECS past expiry that transfers tolerate, so sponsor cannot close a delegation while the delegatee can still pull.
Extend SubscribeData with expected_mint/amount/period_hours/created_at. Program rejects with PlanTermsMismatch if the live plan disagrees with what the subscriber signed. Stale-signed subscribe transactions can no longer enroll into a recreated plan with different terms. SDK and webapp callers fetch plan data and pass the snapshot.
Plan::check_destination and Plan::can_pull now filter out zero-padded slots before membership tests. A plan with fewer than four configured destinations no longer authorizes a zero-owned receiver, and a plan with fewer than four pullers no longer authorizes a zero-pubkey caller.
…-10) Webapp exit flows now pass the on-chain payer as receiver when it differs from the connected signer, so sponsor-funded delegations and SubscriptionAuthority accounts can actually be closed. Also migrates revokeSubscription and cancelAndRevokeSubscription from buildRevokeDelegation to buildRevokeSubscription with planPda + receiver, fixing subscription revoke for both sponsor and non-sponsor cases.
…(MULT-9) Stale-delegation cleanup no longer appends a close on the current SubscriptionAuthority. Revoking stale delegations is now scoped to the supplied delegation accounts; the SA stays open and current grants remain valid.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Audit finding: MULT-19
Issue
keys/subscriptions-keypair.jsoncontained the private key for the same address (EPEUTog…rqg) used bydeclare_id!, the IDLpublicKey, and the canonical SDK constants. Mainnet-beta had no program at that address. Anyone with the public repo could squat the canonical ID and intercept SDK-shaped wallet-signed instructions.Fix
Rotate the canonical program ID to
De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44(matches existing README) and stop committing the keypair.declare_id!+ IDLpublicKeyupdatedgit rm keys/subscriptions-keypair.jsonandkeys/added to.gitignorerunbooks/surfnet-setuprunbook usingsvm::setup_surfnet { deploy_program { program_id, binary_path, idl_path } }— direct account write, no keypair requiredtxtx.ymlregisters the new runbook;ensure-surfpoolpasses--runbook surfnet-setupprogram-id,ensure-surfpool,verify-mainnet) derive the program ID fromdeclare_id!viaseddeploy-idl-devnet/deploy-idl-mainnettake the keypair viaPROGRAM_UPGRADE_AUTHORITY_KEYPAIRenv var (intended use:doppler run -- just deploy-idl-...)runbooks/deploymentrunbook reads the keypair frominput.program_keypair_path(off-repo)dev-build-keypairartifact (no longer needed)prepare-deploy-keysrecipe deleted (no consumers after rotation)Notes
Test plan
cargo build -p subscriptionscargo test -p subscriptions --lib(210/210 pass)pnpm --filter @subscriptions/client buildjust program-idreturnsDe1egAFM…vR44just ensure-surfpooldeploys atDe1egAFM…vR44on a clean checkout (nokeys/present)