SHiNE-server/Dev_Docs/audit/Solana-audit-3-by-Claude-12июня2026.md
AidarKC 42dcf6970d homeserver: рендейм subserver→homeserver, документ деривации ключей, запрет пустого пароля
Основное (наша работа в этой сессии):
- Переименование «subserver» → «homeserver» по всему проекту: основной ESP32-скетч
  (папка shine_subserver_ui → shine_homeserver_ui, .ino, flash-скрипт, режим burn.sh
  homeserver-ui), скетч lvgl_nav_minimal_test (ключ homeserver.key:<имя>), spec-доки
  reference/*, формат PDA (терминология session_type=100 «Homeserver пользователя»),
  константа SESSION_TYPE_HOMESERVER в JS и Rust (значение 100 не менялось, формат не затронут),
  pending/future доки, AGENTS.md, DAO-док. Сохранены отдельный lvgl_subserver_touch_test и
  историческая пометка о рендейме в DERIVATION.md.
- Новый источник истины по деривации ключей: Dev_Docs/Keys/DERIVATION.md (Argon2id-секрет из
  пароля, формула Ed25519(SHA-256(base64(secret)|suffix)), суффиксы root/bch/dev/homeserver.key,
  Solana-ключ = dev.key). Уточнены роли root (главный/master) и dev (пополняемый кошелёк) в
  Dev_Docs/Keys/README.md.
- UI: убран легаси-путь пустого пароля (derivePasswordSeed и др.), deriveMasterSecretFromPassword
  бросает ошибку на пустом пароле, register-view блокирует пустой пароль; экран пополнения
  переведён на канонический device-адрес из preGeneratedKeyBundle (удалён расходящийся
  deriveWalletFromPassword).

Включены также параллельные правки Solana-аудита №3 (были в рабочем дереве, переплетены в lib.rs):
- shine_users: defense-in-depth «строгий список аккаунтов» (require!(it.next().is_none()))
  в init/update economy config и create/update user PDA, плюс описание в doc/programs/shine_users.md;
- Dev_Docs/audit/Solana-audit-3-by-Claude-12июня2026.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 21:16:12 +04:00

14 KiB
Raw Blame History

Аудит безопасности Solana-программ SHiNE — выпуск 3 (12.06.2026)

Тематический аудит с фокусом на полноту проверок входных аккаунтов (signer / owner / каноничный PDA-адрес / system-program / sysvar инструкций / аккаунт оракула) — отвечает на вопрос «точно ли хватает всех проверок входных аккаунтов». Код перечитан целиком после исправлений аудита №2 (Solana-audit-2-by-Claude-11июня2026.md):

  • shine_login_guard (183 строки) — stateless-классификатор логинов, аккаунтами не пользуется;
  • shine_users (1068 строк) — реестр пользователей, PDA-записи, ed25519-подписи, экономика лимитов;
  • shine_payments (1398 строк) — очереди тикетов, выплаты из вольта, оракул Pyth.

Это ручная (не-Anchor #[derive(Accounts)]) реализация на solana_program, поэтому каждая проверка аккаунта выполняется явно в коде handler-а. Перебраны: подмена аккаунтов/PDA, подмена владельца, bump-seed атаки, отсутствие signer/authority, подмена system-program и sysvar, подмена аккаунта оракула, неинициализированные/ повторно инициализируемые PDA, «лишние» аккаунты.

Итоговый вердикт

Проверок входных аккаунтов достаточно во всех трёх программах. По каждому handler присутствуют все требуемые классы проверок; грубых дыр (подмена PDA на чужой аккаунт, отсутствие owner/signer-проверки, использование пользовательского bump, подмена аккаунта оракула) не найдено. Все Critical/HIGH из аудитов №1 и №2 закрыты и в этом проходе подтверждены в коде. Новых эксплуатируемых пробелов в валидации аккаунтов нет; есть несколько LOW/INFO-замечаний «by design».

Статус прошлых находок (подтверждено в коде на 12.06.2026)

  • 🔴 Critical #1 (economy-config PDA, shine_users) — закрыто: validate_users_economy_config_pda (адрес + owner == program_id) вызывается и в create, и в update перед чтением.
  • 🔴 Critical #2 (singleton-PDA, shine_payments) — закрыто: validate_singleton_state_pda (адрес + owner == id()) во всех инструкциях.
  • 🟠 Medium (валидация Pyth) — закрыто: пин адреса PYTH_SOL_USD_ACCOUNT, owner == pyth_receiver, PriceUpdateV2, feed_id, возраст, доверительный интервал.
  • 🟡 Low (griefing на предсказуемых адресах) — закрыто: create_pda_account создаёт «поверх предзаполненного» в обеих программах.
  • 🔴 HIGH аудита №2 (recipient_wallet == inflow_vault замораживает выплаты) — закрыто: запрет recipient == inflow_vault в buy_ticket_by_purchase_usd (стр. 1026), process_manager_add_ticket (стр. 747), process_change_ticket_recipient (стр. 878) + защита по умолчанию require!(vault.key != recipient.key) в transfer_from_vault (стр. 1278).

Матрица проверок входных аккаунтов

shine_users

Инструкция signer owner PDA адрес/seed PDA system sysvar / подпись прочее
init_users_economy_config owner == system + data_is_empty (анти-reinit) деривация + сверка значения из settings, не из ввода
update_users_economy_config ✓ + signer == DAO_AUTHORITY owner == program_id деривация + сверка lamports_per_limit_step > 0
create_user_pda ✓ + signer == device_key user_pda owner == system + empty; econ_config owner == program_id user_pda, econ_config, inflow_vault, login_guard — все сверены ed25519 (record sig idx 2, last_block idx 1) inflow_vault сверен с PDA shine_payments; login_guard сверен дважды
update_user_pda ✓ + signer == device_key user_pda owner == program_id; econ_config owner == program_id деривация + сверка ed25519 + version == old+1 + prev_hash == hash(old) immutable-поля сверены с прежней записью

shine_payments

Инструкция signer owner / валидация PDA адрес PDA system прочее
init ✓ payer все 4 PDA is_uninitialized деривация + сверка dao_wallet из settings, нет лишних аккаунтов
update_coef_limit ✓ + signer == config.dao_wallet config/coef owner == id() деривация + сверка границы coef/limit/reward; нет лишних аккаунтов
grant_manager_limits ✓ + signer == config.dao_wallet config owner == id(); allowance create/read allowance из manager_wallet state.manager_wallet == args.manager_wallet
buy_ticket / _usd / _sol config/coef/queues owner == id() ticket деривация + сверка + is_uninitialized oracle (key+owner+возраст+confidence), dao_wallet == config.dao_wallet, recipient != inflow_vault, slippage
manager_add_ticket allowance/queues owner == id() allowance из signer; ticket деривация + сверка + uninit allowance.manager_wallet == signer, queue_id ∈ {1,2,3}, recipient != inflow_vault
step_payout все singleton-PDA owner == id() ticket деривация + сверка dao_wallet == config.dao_wallet, inflow == config.inflow_vault, ticket queue/index/!is_paid/recipient, oracle
change_ticket_recipient ✓ + signer == ticket.recipient_wallet queues + ticket owner == id() (через read_state) ticket деривация из своих queue_id/index + сверка !is_paid, запрет менять «следующий к выплате», recipient != inflow_vault

shine_login_guard

Аккаунты не используются (_accounts); программа stateless, средствами не владеет. Защита со стороны вызова реализована в shine_users: сверяется и адрес вызываемой программы (login_guard_program.key == SHINE_LOGIN_GUARD_PROGRAM_ID), и program_id в get_return_data. Подмена/подделка ответа исключены. Отдельных проверок входных аккаунтов внутри программы не требуется.


🟡 LOW / INFO — наблюдения без прямой эксплуатации

L1. Permissionless init в обеих программах

shine_payments::init и shine_users::init_users_economy_config может вызвать кто угодно первым. Практического эксплойта нет: все значения (включая dao_wallet и DAO_AUTHORITY) берутся из констант settings, а не из ввода, повторная инициализация заблокирована проверками is_uninitialized / data_is_empty. Риск низкий; при желании привязать init к ожидаемому деплой-кошельку. Совпадает с моделью «первый init = деплой».

L2. В shine_users нет явной проверки «лишних аккаунтов» — ИСПРАВЛЕНО (12.06.2026)

shine_payments в каждом handler делает require!(account_iter.next().is_none()). В shine_users такой проверки не было — лишние аккаунты в конце списка просто игнорировались (читается строго нужное количество через next_account_info). Это безвредно (на безопасность не влияло), но для симметрии и явности добавлено. Класс: гигиена, не уязвимость.

Закрыто: во все 4 инструкции shine_users (init_users_economy_config, update_users_economy_config, create_user_pda, update_user_pda) после чтения фиксированного набора аккаунтов добавлено require!(it.next().is_none(), ShineUsersError::InvalidInstruction). Документация — doc/programs/shine_users.md §3.4.

L3. Гонка за логином (first-come) в shine_users — known issue

Адрес user_pda детерминирован из логина; после закрытия griefing-подсева остаётся обычное состязание за регистрацию (front-run в мемпуле). On-chain решается только commit-reveal; для текущей модели — приемлемый риск, ранее зафиксирован в аудите №2 (L2). К проверкам аккаунтов не относится.

L4. Экономическая устойчивость вольта (дизайн, не баг)

Деньги за покупку тикетов уходят на dao_wallet, а выплаты step_payout идут из inflow_vault, наполняемого регистрационными комиссиями shine_users (коэффициент по умолчанию START_COEF_PPM = 5x). При недостаточном притоке регистраций вольт истощается и выплаты останавливаются (без потери средств). Это свойство экономической модели «очередь/билеты», а не дефект валидации аккаунтов — отмечено для полноты (ранее L4 в аудите №2). Мониторить баланс вольта vs обязательств.


Проверено и подтверждено как корректное (по входным аккаунтам)

  • Подмена PDA невозможна нигде: всюду пара «деривация find_program_address + сверка полного адреса». Пользовательский bump не принимается, create_program_address с внешним bump не используется — bump-seed атаки исключены.
  • Проверка владельца при каждом чтении PDA: read_state и validate_singleton_state_pda (shine_payments) требуют owner == id(); validate_users_economy_config_pda и проверка user_pda.owner == program_id (shine_users) — перед десериализацией данных.
  • Создаваемые PDA: проверка is_uninitialized / owner == system && data_is_empty исключает повторную инициализацию и перезапись чужого аккаунта.
  • signer / authority: все handler начинают с обязательного is_signer; привилегированные операции дополнительно сверяют ключ с авторитетом (config.dao_wallet, DAO_AUTHORITY, allowance.manager_wallet, ticket.recipient_wallet, device_key).
  • system-program сверяется с system_program::ID там, где идёт создание аккаунта/перевод; sysvar инструкций сверяется с sysvar::instructions::id() перед ed25519-интроспекцией.
  • Аккаунт оракула: пин адреса PYTH_SOL_USD_ACCOUNT + owner == pyth_receiver + feed_id + возраст (120 с) + доверительный интервал (10%).
  • Ed25519 в shine_users: относительные индексы 1/2, num_signatures == 1, все три ix_index == u16::MAX (offset-данные внутри самой ed25519-инструкции), сверка program_id == ed25519_program и pubkey/signature/message по хэшу — указать на чужую инструкцию нельзя.
  • Алиасинг аккаунтов: recipient != inflow_vault запрещён на входе во всех точках задания получателя + vault.key != recipient.key в transfer_from_vault.
  • inflow_vault в shine_users сверяется с PDA, выведенным из SHINE_PAYMENTS_PROGRAM_ID и SHINE_PAYMENTS_INFLOW_VAULT_SEED — комиссия не может уйти на чужой адрес.
  • Реентранси отсутствует: CPI только в System Program и в stateless shine_login_guard (с проверкой возвращённого program_id); обратных вызовов в наши программы нет.

Приоритет действий

  1. LOW выполнено 12.06.2026: добавлено require!(it.next().is_none(), …) во все инструкции shine_users для симметрии с shine_payments (L2).
  2. INFO — зафиксировать в эксплуатационной документации known-issue гонки за логином (L3) и экономику вольта (L4); рассмотреть привязку init к ожидаемому деплой-кошельку (L1).

Критичных и высоких находок по полноте проверок входных аккаунтов в этом проходе нет. Единственная LOW-правка (L2) применена в рамках этого же изменения; код shine_users собирается успешно (cargo build -p shine_users).