Skip to content

fix(audit): rotate program ID after committed keypair leak (MULT-19)#30

Merged
dev-jodee merged 11 commits into
mainfrom
audit/ai-scanner/01-rotate-program-keypair
Apr 28, 2026
Merged

fix(audit): rotate program ID after committed keypair leak (MULT-19)#30
dev-jodee merged 11 commits into
mainfrom
audit/ai-scanner/01-rotate-program-keypair

Conversation

@dev-jodee
Copy link
Copy Markdown
Collaborator

@dev-jodee dev-jodee commented Apr 28, 2026

Audit finding: MULT-19

Committed Canonical Program Keypair Enables Malicious Mainnet Deployment at SDK Default ID

Issue

keys/subscriptions-keypair.json contained the private key for the same address (EPEUTog…rqg) used by declare_id!, the IDL publicKey, 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! + IDL publicKey updated
  • git rm keys/subscriptions-keypair.json and keys/ added to .gitignore
  • Webapp scripts/server use a hardcoded program-ID const instead of reading the keypair file
  • Surfpool now installs the program at the canonical ID via a new runbooks/surfnet-setup runbook using svm::setup_surfnet { deploy_program { program_id, binary_path, idl_path } } — direct account write, no keypair required
  • txtx.yml registers the new runbook; ensure-surfpool passes --runbook surfnet-setup
  • Justfile recipes (program-id, ensure-surfpool, verify-mainnet) derive the program ID from declare_id! via sed
  • deploy-idl-devnet / deploy-idl-mainnet take the keypair via PROGRAM_UPGRADE_AUTHORITY_KEYPAIR env var (intended use: doppler run -- just deploy-idl-...)
  • Mainnet/devnet runbooks/deployment runbook reads the keypair from input.program_keypair_path (off-repo)
  • CI: dropped the dev-build-keypair artifact (no longer needed)
  • prepare-deploy-keys recipe deleted (no consumers after rotation)

Notes

  • Old keypair already public — git history rewrite skipped per the user's call
  • Devnet program at the old ID is abandoned; new ID gets a fresh devnet deployment when ready
  • Mainnet keypair stored off-repo; user handles real deploy
  • Program logic unchanged

Test plan

  • cargo build -p subscriptions
  • cargo test -p subscriptions --lib (210/210 pass)
  • pnpm --filter @subscriptions/client build
  • just program-id returns De1egAFM…vR44
  • Manual: just ensure-surfpool deploys at De1egAFM…vR44 on a clean checkout (no keys/ present)
  • Manual: integration tests pass against surfpool with the new install path

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 28, 2026

Compute Unit Report

Instruction Samples Min CUs Max CUs Avg CUs Est Cost (Low) [SOL] Est Cost (Med) [SOL] Est Cost (High) [SOL]
cancel_subscription 11 1774 2098 1982 0.000005000 0.000005079 0.000005991
close_subscription_authority 7 1866 1901 1871 0.000005000 0.000005074 0.000005935
create_fixed_delegation 36 3566 9595 4889 0.000005001 0.000005195 0.000007444
create_plan 84 3525 11038 4939 0.000005001 0.000005197 0.000007469
create_recurring_delegation 25 3590 8092 4494 0.000005001 0.000005179 0.000007247
delete_plan 8 401 401 401 0.000005000 0.000005016 0.000005200
init_subscription_authority 136 6250 16753 9487 0.000005002 0.000005379 0.000009743
revoke_delegation 19 303 570 405 0.000005000 0.000005016 0.000005202
subscribe 21 6639 15663 8506 0.000005002 0.000005340 0.000009253
transfer_fixed 6 8478 8481 8480 0.000005002 0.000005339 0.000009240
transfer_recurring 17 8566 8651 8594 0.000005002 0.000005343 0.000009297
transfer_subscription 10 8862 8985 8901 0.000005002 0.000005356 0.000009450
update_plan 21 409 488 461 0.000005000 0.000005018 0.000005230

Generated: 2026-04-28

@dev-jodee dev-jodee force-pushed the audit/ai-scanner/01-rotate-program-keypair branch from 3ddc3cb to 6155c84 Compare April 28, 2026 13:09
- 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
@dev-jodee dev-jodee force-pushed the audit/ai-scanner/01-rotate-program-keypair branch from 6155c84 to 40be1d2 Compare April 28, 2026 13:27
@dev-jodee dev-jodee marked this pull request as ready for review April 28, 2026 16:24
- 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.
@dev-jodee dev-jodee merged commit 0ce2484 into main Apr 28, 2026
6 checks passed
@dev-jodee dev-jodee deleted the audit/ai-scanner/01-rotate-program-keypair branch April 28, 2026 19:52
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.

1 participant