Сбой энтропии COLDCARD: как тихий откат ГПСЧ стоил пользователям 38 миллионов долларов

Ключевые выводы
- Пользователям, сгенерировавшим и использовавшим seed-фразу на COLDCARD Mk3: немедленно сгенерируйте новую seed-фразу на другом кошельке и перенесите средства на новый кошелёк.
- Пользователям, сгенерировавшим и использовавшим seed-фразу на COLDCARD Mk4 или Mk5: если версия прошивки, на которой создавалась исходная seed-фраза, была ниже 5.6.0, немедленно сгенерируйте новую seed-фразу на другом кошельке и перенесите средства на новый кошелёк.
- Пользователям, сгенерировавшим и использовавшим seed-фразу на COLDCARD Q: если версия прошивки, на которой создавалась исходная seed-фраза, была ниже 1.5.0Q, немедленно сгенерируйте новую seed-фразу на другом кошельке и перенесите средства на новый кошелёк.
- Пользователи аппаратных кошельков OneKey этой ошибкой не затронуты и могут спокойно продолжать ими пользоваться.
В криптографии «случайность» — это строгая метрика. Её можно измерить, и она должна выдерживать проверку реальной вычислительной мощностью.
За seed-фразой из 12 слов стоит 128-битное случайное число, закодированное в понятные человеку слова по спецификации BIP-39. Насколько непредсказуемо это число — напрямую определяет, устоит ли кошелёк перед перебором. 128 бит означают 2¹²⁸ вариантов. Соберите вместе всю вычислительную мощность Земли — и вы всё равно не успеете перебрать их до конца существования Вселенной.
Поэтому в теории аппаратные кошельки должны подходить к этому со всей строгостью. Должен быть выделенный аппаратный генератор истинно случайных чисел (TRNG), который черпает случайность из физического мира — из шума схем, — в паре с защищённым элементом (SE), сертифицированным по EAL, и зрелым алгоритмом, который в конце всё это тщательно перемешивает.
30 июля 2026 года около 594 BTC (примерно 38 млн долларов) менее чем за полчаса были выметены почти из 500 кошельков COLDCARD. На следующий день производитель Coinkite подтвердил первопричину в материале «Technical Deep Dive into the Entropy Issue» [1]: начиная с изменения прошивки в марте 2021 года ошибка конфигурации сборки тихо заставляла код откатываться на встроенный в MicroPython программный генератор псевдослучайных чисел (ГПСЧ).
Последствия различаются по моделям. По предварительной оценке Coinkite, у Mk3 осталось лишь около 40 бит эффективного пространства перебора, а у Mk4, Mk5 и Q — около 72 при проектной цели в 128. Сорок бит — это примерно 1,1 триллиона вариантов, что всё ещё звучит астрономически, но для атакующего, которому по карману аренда GPU-кластера, задача превратилась из «физически невозможной» в инженерный проект, реализуемый по матожиданию. При переходе со 128 до 40 пространство перебора сократилось в 2⁸⁸ раз — в 300 септиллионов раз.
Эта кража на 38 млн долларов превратила матожидание в свершившийся факт.
Примечание: средства пользователей OneKey в безопасности. Ни одна строка кода OneKey не касается этой проблемы, и ни в одной из них не используются задействованные реализации или зависимости. Полное заявление: https://x.com/OneKeyHQ/status/2083106588022509974?s=20
TL;DR
- Пользователям, сгенерировавшим и использовавшим seed-фразу на COLDCARD Mk3: немедленно сгенерируйте новую seed-фразу на другом кошельке и перенесите средства на новый кошелёк.
- Пользователям, сгенерировавшим и использовавшим seed-фразу на COLDCARD Mk4 или Mk5: если версия прошивки, на которой создавалась исходная seed-фраза, была ниже 5.6.0, немедленно сгенерируйте новую seed-фразу на другом кошельке и перенесите средства на новый кошелёк.
- Пользователям, сгенерировавшим и использовавшим seed-фразу на COLDCARD Q: если версия прошивки, на которой создавалась исходная seed-фраза, была ниже 1.5.0Q, немедленно сгенерируйте новую seed-фразу на другом кошельке и перенесите средства на новый кошелёк.
- Пользователи аппаратных кошельков OneKey этой ошибкой не затронуты и могут уверенно продолжать ими пользоваться.
А теперь — хардкорная техническая часть 👇
Первопричина: тихий программный откат
От аппаратного интерфейса ГСЧ к libNgU
Миграция криптографического стека в 2021 году сменила генерацию seed в кошельке с ckcc.rng_bytes() на ngu.random.bytes() [6]:
ngu.random.bytes() → libNgU rng_get() → тот символ rng_get, с которым это в итоге линкуется
Риск не в имени функции и не в длине вывода. Он в том, какой объектный файл на самом деле предоставляет тот финальный rng_get().
#ifndef проверяет только факт определения, а не значение
Конфигурация целевой платы определяла MICROPY_HW_ENABLE_RNG как 0, то есть путь аппаратного ГСЧ в MicroPython не был включён.
Но на тот момент libNgU использовал [8]:
#ifndef MICROPY_HW_ENABLE_RNG
#error ...
#endif
#ifndef проверяет лишь, существует ли макрос. Макрос, определённый как 0, всё равно считается «определённым», поэтому #error, который должен был заблокировать неверную сборку, так и не сработал.
Одноимённый символ пропустил неправильную реализацию в финальную прошивку
Когда аппаратный ГСЧ не включён, MicroPython всё равно предоставляет rng_get() под тем же именем, но это программный ГПСЧ-откат Yasmarang [9].
Так сложилась вся цепочка отказа:
макрос существует, но его значение равно 0 → защитная проверка сборки в libNgU не срабатывает → прошивка всё равно успешно компилируется → одноимённый rng_get разрешается в откат MicroPython → генерация seed так и не получает аппаратный ввод ГСЧ, который должна была получить
По отдельности каждое звено этой цепочки — всего лишь мелкий недосмотр. Но сцепленные вместе, они превращают контроль системы сборки в фикцию: наличие безопасной реализации в исходниках или в бинарнике не означает, что критичный для безопасности вызов действительно до неё доходит.
Смешивание источников энтропии не компенсирует её нехватку
Начальное состояние отката формируется из части UID, SysTick, времени RTC и субсекундного состояния, после чего Yasmarang продвигает его детерминированно. libNgU также подмешивает к нему второй поток Yasmarang, стартующий из фиксированного начального состояния.
Два детерминированных потока, которые можно восстановить или перебрать, не приобретают новой физической непредсказуемости оттого, что их сложили по XOR или прогнали через SHA-256. Хеширование может кондиционировать вход, но не способно расширить конечное множество кандидатов в подлинное пространство неизвестности на 2^256.
Что на самом деле означают 40 бит и 72 бита
Оценки эффективного пространства перебора, которыми оперирует Coinkite, — это не измеренные и не сертифицированные значения энтропии. Оба числа описывают размер пространства состояний-кандидатов, которое придётся перебрать атакующему. Это не измерение минимальной энтропии (min-entropy), они не переводятся в среднее число попыток и уж точно не дают конкретного времени или стоимости атаки.
Реальная стоимость атаки зависит от цепочки предварительных условий: знает ли атакующий UID устройства или может ли его угадать; способен ли он сузить диапазон RTC, SysTick и времени загрузки; сколько вызовов ГСЧ произошло до генерации seed; есть ли у него под рукой xpub, адрес или открытый ключ для офлайн-проверки кандидатов; а также каковы затраты на деривацию BIP-39/BIP-32 и параллельное оборудование.
Поэтому точная формулировка такова: при нынешних предварительных предположениях Coinkite об атаке эффективное пространство перебора Mk3 оценивается примерно в 40 бит, а Mk4/Q/Mk5 — примерно в 72. Это не то же самое, что «у каждого кошелька Mk3 ровно 40 бит энтропии», и не «доказано, что у новых моделей 72-битная криптографическая стойкость», и уж тем более не «любой обычный компьютер способен восстановить каждый кошелёк за фиксированное время».
Почему затронуты также Mk4, Mk5 и Q
Два защищённых элемента на платах Mk5 и Q (источник: coldcard.com)
Два защищённых элемента на платах Mk5 и Q (источник: coldcard.com)
Mk4, Mk5 и Q действительно берут случайный материал из двух защищённых элементов, но реализация до исправления не подавала весь этот вывод в криптографический DRBG.
Открытые исходники показывают, что после хеширования материала защищённого элемента берутся лишь четыре байта и передаются в ngu.random.reseed(), заменяя всего одно 32-битное слово состояния Yasmarang.
Поэтому, когда остальная часть состояния отката и трассировка вызовов зафиксированы:
число выходных потоков, различимых по reseed защищённого элемента ≤ 2³²
Это не противоречит официальным «около 72 бит». Модель на 72 бита также учитывает неопределённость в таких состояниях, как UID, таймеры, RTC и история вызовов. Свободная совокупная верхняя граница Block Engineering оказывается ниже 2^73.3, и они прямо заявляют, что это не сертификация 73-битной криптографической стойкости [10].
Иными словами: Mk3 — наихудший случай, потому что защищённого reseed там нет вовсе. Ввод от защищённого элемента на Mk4/Q/Mk5 снижает риск, но четырёхбайтовый reseed не восстанавливает 128-битную стойкость, к которой стремился проект.
Официальное уведомление и фактическая область воздействия
Текущее уведомление Coinkite начинается с Mk3 4.0.1. Но неизменяемые исходники v4.0.0 уже содержат соответствующий путь генерации энтропии [7], и между 4.0.0 и 4.0.1 нет никакого исправления ГСЧ.
Block Engineering идёт дальше, включая в свой анализ исходников также Mk2/Mk3 v4.0.0–v4.1.9 [10].
Поэтому нужно разделять два уровня:
- Официальная область Coinkite: Mk3 4.0.1+;
- Техническая область, подтверждаемая исходным кодом: Mk2/Mk3 v4.0.0–v4.1.9.
Нельзя утверждать, что Coinkite официально подтвердил Mk2 или v4.0.0. Но нельзя и делать вывод, что они безопасны, только потому что уведомление их не называет.
Как работает исправление
Экстренное исправление в Mk4/Mk5 5.6.0 и Q 1.5.0Q не ограничилось изменением одного условного выражения. Оно добавило контроль на уровне сборки [3][4][5]:
- Исключить объект отката ГСЧ из MicroPython;
- Убедиться, что объект уровня платы предоставляет глобальный
rng_get(); - Использовать
nmдля проверки символов в объектных файлах; - Полностью проваливать сборку, если откат по-прежнему экспортирует символ или если объект уровня платы не предоставляет
rng_get()должным образом.
Что делать пользователям
Mk4, Mk5 и Q
- Сначала обновитесь до 5.6.0 или новее (Mk4/Mk5) либо до 1.5.0Q или новее (Q).
- Сгенерируйте совершенно новую seed-фразу на исправленной прошивке.
- Запишите резервную копию и выполните проверку восстановления.
- Сверьте отпечаток кошелька и адрес получения на экране устройства.
- Отправьте небольшую тестовую транзакцию, прежде чем переносить остальные средства.
- Сохраняйте старую резервную копию, пока каждый перевод не подтверждён, но прекратите принимать средства на старый кошелёк.
Mk3
На данный момент для Mk3 нет выпущенной исправляющей прошивки, а 4.1.9 исправлением не является. Пользователям не стоит ждать прошивку, которая может выйти позже, потому что никакая новая прошивка не способна добавить энтропии уже существующей seed-фразе.
Следуйте текущим рекомендациям Coinkite по миграции: сгенерируйте новую seed-фразу по доверенному пути, проверьте резервную копию, адреса и небольшую транзакцию, а затем перенесите свои активы.
Два разных порога для игральных костей
Для костей есть два числа: 50 и 99. Они служат двум разным целям и не взаимозаменяемы.
50+ бросков, добавленных при исходной генерации seed. Если при первичной генерации seed было подмешано не менее 50 честных, независимых, приватных бросков шестигранной кости, которые никогда не записывались, не сохранялись и не разглашались, Coinkite заявляет, что не считает такую seed-фразу подверженной риску только из-за этой проблемы с ГСЧ [11].
50 × log₂(6) ≈ 129,25 бит
Это примерно 128-битный порог именно для данного инцидента, а не абсолютное доказательство безопасности всего вашего операционного процесса.
Процедура Dice Roll Import на чистом Mk3. Coinkite также предлагает иную процедуру: на чистом Mk3 с прошивкой 4.1.9 выберите Import Existing > Dice Rolls и введите не менее 99 честных бросков. Этот специальный путь напрямую потребляет последовательность бросков и не использует затронутый генератор устройства.
99 × log₂(6) ≈ 255,91 бит
99 — это порог в официальной процедуре; отсчитывая назад от буквального стандарта «не менее 256 бит», вам понадобилось бы 100.
Passphrase — это отдельный барьер, а не исправление
Стойкая, уникальная, случайная и секретная passphrase BIP-39 деривирует другой кошелёк и может добавить независимый фактор перебора; PIN устройства управляет лишь локальным доступом и не участвует в деривации корневого ключа BIP-39 [12].
Но passphrase не может восполнить энтропию, которой у мнемоники никогда не было. Короткие passphrase, известные цитаты, предсказуемые шаблоны и повторно использованные пароли всё ещё поддаются угадыванию. Опечатка тоже породит кошелёк, который выглядит корректным, но полностью отличается, поэтому нужно сверять отпечаток и хранить надёжную резервную копию, восстановление из которой вы проверили.
Заключение
Ключевая проблема, которую вскрыл этот инцидент, — в том, что фактическая достижимость критичной для безопасности реализации никогда не обеспечивалась системой сборки. Фраза «кто-то ошибся в макросе» описывает это далеко не полностью.
Как минимум аппаратные кошельки и другие системы генерации ключей должны:
- Проверять, с каким объектом и символом в итоге линкуется API безопасности, а не только сверять сигнатуру функции;
- Заставлять проверки возможностей верифицировать и существование, и значение макроса;
- Делать критичные для безопасности откаты «отказоустойчиво закрытыми» (fail closed), никогда не подставляя тихо обычный ГПСЧ;
- Заставлять CI проверять состав объектов, карты компоновки (link map), происхождение символов и сквозной поток данных;
- Доказывать тестами, что реальная аппаратная энтропия доходит до финальной seed, а не только проверять длину вывода, ненулевость или отсутствие повторов;
- Писать уведомления, чётко разделяющие версию, на которой сгенерирована seed, текущую прошивку, исправленную версию и то, что делать со старыми seed.
Именно открытый исходный код COLDCARD позволил сторонним исследователям восстановить эту цепочку вызовов, и Coinkite опубликовала как формальный технический отчёт, так и исправленный релиз.
Заявления о том, что бренд «абсолютно безопасен», не стоят ничего. Стоит другое — превратить вопрос «откуда берётся случайность, с чем она в итоге линкуется, куда уходит при сбое и как это проверить в поставляемом продукте» в системное свойство, которое можно перепроверять снова и снова.
References
[1] Coinkite: Technical Deep Dive into the Entropy Issue [2] Coinkite: Mk3 Security Advisory [3] Coldcard 5.6.0 / 1.5.0Q fix commit [4] Coldcard 5.6.0 release commit [5] Coldcard 1.5.0Q release commit [6] 2021 libNgU seed generation migration commit [7] Official v4.0.0 source snapshot [8] libNgU random.c [9] MicroPython commit introducing the fallback [10] Block Engineering technical analysis [11] Coldcard dice roll math [12] Coldcard passphrase documentation [13] OneKey trezorcrypto.random source branch [14] OneKey seed generation logic [15] OneKey standard Makefile [16] OneKey SCons config






