Пошли на крайние химеры
Как в блокчейне скрестить голос за оппозицию с вбросом за власть? Результаты эксперимента программиста Петра Жижина

На дальних подступах и непосредственно передвыборами авторы «Новой газеты» предупреждали о потенциальных уязвимостях дистанционного электронного голосования (ДЭГ). В день выборов на электронном избиркоме происходили странные события, о чем нашим читателям рассказали его свидетели — Илья Сухоруков, Анна Лобонок и Николай Колосов. Мы попытались получить внятные объяснения у ЦИК, департамента информационных технологий (ДИТ) Москвы и отвечающего за электронное голосование в Москве Артема Костырко, но от личного разговора Артем Костырко и Элла Памфилова уклонились, а в письменной коммуникации мы не получили внятных ответов.
Статистический анализ физика Максима Гонгальского показал, что, скорее всего, основным механизмом фальсификаций, обеспечившим победу провластных кандидатов, было «скручивание» голосов оппозиции через переголосование, и оценил количество «скрученного» в 250 000 голосов. Отчет технической группы ДЭГ такую версию не опроверг.
Программист Петр Жижин в своей статье на портале для программистов, которую он популярно изложил нашим читателям в интервью Юлии Латыниной, установил существование «секретного блокчейна». Это исключает проверяемость результатов выборов по публичному блокчейну, опубликованному ДИТ Москвы. Петр Жижин тогда не смог дать ответа: как в системе могли осуществляться вбросы? А теперь такой ответ у него есть.
Если очень коротко:
сначала в публичный блокчейн могли вписываться голоса за провластных кандидатов от «не людей». А после завершения голосования в «приватный блокчейн» могли вписываться «химерические» строчки,
в которых поле, соответствующее избирателю, было бы взято от живых людей из других строк приватного блокчейна, а поле, соответствующее голосованию за какого-либо кандидата — взято от вброшенного «не человеком» бюллетеня за власть из публичного блокчейна.
Кроме того, было выяснено, что опубликованный ДИТ Москвы код не соответствовал тому, который запускался на выборах: для выборов увеличили время, в течение которого доступно переголосование. Оказалось также, что во время работы программы осуществлялась запись не предусмотренной законом и порядком проведения ДЭГ простой таблицы с данными избирателей, сведения о которой отсутствуют в документации на ДЭГ.
Наконец, помимо всех перечисленных дыр, Петр экспериментально обнаружил целое «окно», через которое при наличии ключа организатора выборов в блокчейн можно внести любую белиберду, и блокчейн не поперхнется.
Мы первыми публикуем результаты дальнейшего исследования Петром и его коллегами программного кода ДЭГ.
— Петр, после публикации твоей статьи на «Хабре» и твоего интервью в «Новой газете» ты продолжил исследовать, как устроено электронное голосование, и обнаружил кое-что интересное, о чем ты не рассказывал в предыдущем интервью. Что тебе удалось найти?
— За то время, которое прошло с публикации моей статьи на «Хабре» и интервью «Новой», я и еще много других людей в чате программистов, которые занимаются анализом дистанционного голосования, изучали исходный код. Нам стали понятны ответы на те вопросы, которых не было три недели назад: как у нас устроена система подсчета голосов, как у нас устроено переголосование и как можно технически подделать выборы в этой системе, если захотеть.
— Мы так и не сумели добиться от Артема Костырко внятного ответа на вопрос о том, что же такое собой представляет тайный «второй блокчейн», в который записывается переголосование. Можешь ли ты описать, что тебе удалось понять про этот второй блокчейн?
Да. После того как я опубликовал статью на «Хабре», со мной поделились документами из технической группы по электронному голосованию и частью документации ДИТ Москвы, мы посмотрели на содержимое тайного блокчейна для тестового голосования и поняли, что устроена эта система следующим образом. Когда избиратель голосует, он в публичный блокчейн пишет сам голос, а в закрытый блокчейн пишет специальное такое число, которое одно и то же для всех его голосов для первого, второго и всех последующих переголосований.
— Называющееся group_id?
— Да, в базу пишется group_id, то есть идентификатор группы (бюллетеней от одного избирателя), и пишется реальное время голосования — когда сервер приема бюллетеней получил голос. И пишется вот тот самый хэш голоса, который можно было сохранить, если вы пользовались инструкцией от «Голоса»* (власти назвали организацию иноагентом). То есть у нас хэш голосования, реальное время голоса и ID группы бюллетеней от этого избирателя. После голосования все бюллетени из одной и той же группы группируются, и из них выбирается последний по времени голос.
— Уточним для наших читателей, что хэш голосования — это параметр, который уникален для данного голосования. Он связывает записи в публичном блокчейне и в приватном блокчейне, правильно?
— Да. Хэш позволяет нам для данного голоса в публичном блокчейне сказать по данным закрытого блокчейна, когда реально был получен этот голос и к какой группе переголосований он относится. То есть позволяет связать эти два блокчейна друг с другом.
Пример транзакций с тестового голосования
В публичный БЧ пишется транзакция с хэшем b8823acb709b0ac45a6ded89a16d7383b38addb7f2e938d899c4d363adeec0c1 и следующим содержимым:
{"voting_id":"4113e78a5ad798e91b1e387f43d9ad0908934d08ca2b107cfdceb9ca546b6db3","district_id":1,
"encrypted_choice:{"encrypted_message":"aa4401f1a105f9e251e54f62376b49d722bd4b4994986c6c63","nonce":
"8fae4904a3229f82c74fd02cded9fef337ec7a7245211b4a",
"public_key":"37f8cbf25b70316b2b4746957a1dbd785d72b358939eecbb391653fcdbd34722"}}
В приватный БЧ пишется транзакция со следующим содержимым:
{"voting_id":"4113e78a5ad798e91b1e387f43d9ad0908934d08ca2b107cfdceb9ca546b6db3","store_tx_hash":
"b8823acb709b0ac45a6ded89a16d7383b38addb7f2e938d899c4d363adeec0c1","encrypted_group_id":
"ACjHkJ0nwvIEeUGfgfv52TkGHxGYiK7DbOhossEMHJ/U2yRqIBEXaLhBXqBVOE+hXEM+
XSsSxrLbTciltN7073jLXOVwzx7lvsVvWwqwUV2QPTZQZzGJem/0Oa8GTCcOFygGhHyzFV
jMOc4bWrOpTsIlRu3r6RiUz0hbpp/t9HWnX4+ACgabNmYtFKz5FmS9LsAQtx8qvZxZdDxSIx/XIzFFQ4IAURchTRyoyUk+
l8fIEegIMSaATRhJgePdob/PmT9G+MtBpFmybsoqmbIFQerWxTK1jN5PXiqojhfxOo2ENSDUr9dG+c4vqer2GoI3","ts":"1627659588000"}
Можно установить взаимно однозначное соответствие через store_tx_hash в содержимом транзакции БЧ2 и хэшем транзакции с голосом в БЧ1.
— Записывается ли в публичный блокчейн идентификатор group_id, то есть идентификатор, общий для всех голосований данного избирателя?
— Нет, не записывается.
— А записывается ли в публичный блокчейн что-то, что позволяло бы нам выяснить, что этот избиратель легитимен?
— Нет, не записывается.
— Хорошо. Итак, у нас есть второй блокчейн, в который записан group_id, подвергшийся шифрованию, так?
— Да. group_id в открытом виде во время голосования не пишется в закрытый блокчейн, к этому group_id добавляется случайное число и добавляется еще текущее время. Помимо того времени, которое дополнительно пишется в открытом виде. И вот эта тройка чисел шифруется, чтобы для одного и того же избирателя этот зашифрованный текст был бы всякий раз разный. Это делается, чтобы невозможно было понять до завершения голосования, какие голоса принадлежат одному и тому же человеку. Шифруют group_id неким отдельным ключом — не тем, который делится на 7 частей.
— Кому принадлежит ключ зашифрования и расшифрования для group_ id?
— Он просто, по всей видимости, на сервере ДЭГ записан в каком-то текстовом файле. Непонятно, кто на него может смотреть.
Согласно исходному коду, ключ шифровки лежит в переменной окружения PHP под именем CRYPT_SECRET.
Ссылка на исходный код.
Значение этой переменной указывается в конфигурационном файле. Примеров того, как могла быть сделана конфигурация, у нас нет.
— А сервер у кого стоит?
— У нас голосование проводит ДИТ Москвы, поэтому, видимо, в ДИТе.
— Еще раз давай проговорим: и ключ зашифрования group_id, и ключ расшифрования group_id хранится у одного и того же юридического лица, правильно?
— Все так, да. Используется один и тот же ключ, который находится в руках одной и той же организации — ДИТ Москвы.
— Ты сказал, что в публичный блокчейн не пишется никаких параметров, которые позволяли бы определить, что этот голос принадлежит какому-то конкретному избирателю и что этот избиратель легитимен, верно?
— Да, все так.
— Как в таком случае программное обеспечение, которое осуществляет запись в публичный блокчейн, проверяет, что пришедшие из внешнего мира сообщения принадлежат не какому-то прохиндею, который нас хочет обмануть, а легитимному избирателю?
— Перед блокчейном есть отдельный сервер, который осуществляет авторизацию пользователя, то есть получает имя пользователя, пароль, авторизацию по SMS проводит какую-то — и это защита от того, что у нас никакой прохиндей не запишет туда лишний голос. Однако если у вас есть доступ напрямую к этому блокчейну, чтобы туда можно было напрямую в обход этого сервера писать данные, вы можете проигнорировать все эти требования, чтобы ваш голос приходил от настоящего пользователя с настоящим SMS.
— То есть снаружи мы в нее не можем забраться, потому что у нас стоит фильтр в виде Mos.ru. Но предположим, что теперь я злоумышленник, который атакует эту систему изнутри, который имитирует приход на нее сообщения как бы от внешнего пользователя. Что мне нужно для взлома? Кто мне позволит или не позволит записать это сообщение в блокчейн и по какому принципу позволит или не позволит?
— Для того чтобы вы могли кинуть бюллетень в эту урну электронного голосования, вы подписываете своей цифровой подписью бюллетень, и организатор голосования должен разрешить кинуть бюллетень в урну, подписанный вашей цифровой подписью. И если ты внешний человек, который даже имеет прямой доступ к блокчейну, но не имеет доступа к ключам, которые позволяют писать в блокчейн транзакции от имени организаторов выборов, то ты не можешь кинуть бюллетень в эту урну. Но если ты организатор выборов, ты можешь это делать. Этим же ключом, например, подписываются сообщения от организаторов выборов — сообщения о том, что создано голосование, что зарегистрированы какие-то избиратели, этим ключом подписывается выдача бюллетеней, и завершение голосования тоже с этим же самым ключом. Если вы имеете доступ к этому ключу, то можете все это делать.
— Давай еще раз подчеркнем для наших читателей, что этот ключ не тождественен тому ключу, с помощью которого расшифровываются голоса, который делится на семь частей, верно?
— Да, верно.
— И что это не ключ для шифрования group_id?
— Верно.
— Итак, это уже третий ключ, о котором мы говорим. И когда тебе это удалось выяснить?
— Я прочитал исходный код, это, в принципе, было понятно уже сразу. Но захотелось провести эксперимент. Я взял тот исходный код, который был опубликован ДИТ Москвы, и запустил вот этот публичный блокчейн, к которому мы имели доступ и могли смотреть на observer.mos.ru. Я осуществил следующую схему: создал какое-то тестовое голосование, которое мне хотелось провести, создал голосование с двумя кандидатами, Васей и Петей. Дальше я зарегистрировал на это голосование ноль избирателей, выдал ноль бюллетеней и после этого 100 бюллетеней вкинул в урну. Еще раз подчеркну, у нас ноль избирателей зарегистрировано, ноль бюллетеней выдано, но в урне оказалось 100 голосов. И самое удивительное: поскольку эта система не может учитывать переголосования, в ней есть такая дырка, которая позволяет подвести произвольный результат, и через эту дырку я подвожу результат: я вбросил 100 голосов за Васю, а результат подвел — 146 миллионов голосов за Петю и ноль голосов за Васю, за которого я вбрасывал.
Исходный код программы, которая это делает (с инструкцией по запуску).
— Поясни, в чем заключается дырка? Тем, что ты вбросил бюллетени, ты уже поиздевался над этим блокчейном. Ты мог бы дальше просто, как честный вор, просуммировать то, что ты вбросил. Но ты где-то проковырял еще дыру, в чем эта дыра заключалась?
— Заключается эта дыра в том, что вот те 100 голосов с точки зрения публичного блокчейна — это все настоящие голоса, но с точки зрения того, как в целом система устроена, какие-то из этих 100 голосов могут быть настоящие, какие-то из этих 100 голосов могут быть переголосованными. Поэтому проделана в этом публичном блокчейне еще одна отдельная дырка, которую предполагалось использовать следующим образом: у нас голоса вот эти публичного блокчейна сопоставляются с теми группами избирателей, которые записаны в закрытом, секретном блокчейне, и закрытый блокчейн подводит итоги, и дальше эти итоги копируют из закрытого блокчейна в публичный. А то слишком палевно, видимо, будет, если у нас голосование закончится, а итогов там не будет.
— Подожди, у тебя в закрытом блокчейне все равно не было 146 миллионов голосов, ведь так?
— Да, да.
— То есть правильно ли я понимаю, что после того, как закрытый блокчейн что-то посчитал, имеется стадия, которая осуществляется ручками и живым человеком?
— Ручками, да, или это может быть какая-то программа, запускаемая вручную или автоматически. Но, судя по тому, что происходило, — подсчет был остановлен и автоматически итоги не подвелись. То ли что-то сломалось, то ли еще что. В моем эксперименте я руками записываю желаемый результат.
— Ручками внес те данные, которые как бы были получены из второго, закрытого блокчейна.
— Да.
— И при этом ты всем сказал, что поскольку отношения у нас джентльменские, то мы тебе должны верить, правильно?
— Примерно так, да.
— И больше никакого способа проверить, чт*о* в закрытом блокчейне, у нас нет, так?**
— 146 миллионов голосов было бы совсем палевно, да. А как проверить? У нас джентльменские отношения. Закрытый блокчейн посчитал.
— И этот опыт ты когда провел, сравнительно недавно, да?
— Да, я это сделал сравнительно недавно.
— Ты не думаешь вообще, что руководитель Общественного штаба по наблюдению за выборами Алексей Венедиктов тебе должен премию вручить за обнаружение дыры?
— Я в этой системе предполагаю, что я имею абсолютный к ней доступ.Я организатор выборов в этой системе. Но если я внешний человек, если пользоваться логикой Венедиктова, логикой ДИТ Москвы, то извне взломать ничего не получится. Если ты инсайдер, если у тебя есть доступ…
— На обычных выборах я ни разу не сталкивался с тем, чтобы вброс организовывал бы кто-то помимо избиркома. Потому что если вброс осуществит аутсайдер, то он только все напортит. Ему необходимо содействие избирательной комиссии, чтобы подделать списки избирателей.
Так вот, правильно ли я в итоге понимаю, что для того, чтобы осуществить вброс и/или прямое поправление результатов ручками (а этом два разных способа), тебе всего лишь навсего нужно иметь доступ к ключу организаторов голосования?
— Да. И доступ к ноде блокчейна прямой, чтобы напрямую, в обход всех серверов туда писать.
— Объясни, почему при таком вбросе не возникнет проблемы с Госуслугами? Правильно ли я понимаю, что если злоумышленник таким образом будет вносить записи, он вносит запись, в которой нет никаких сведений об избирателе, правильно?
— Да. Это сделано, чтобы защитить как бы тайну голосования.
— Поэтому публичный блокчейн не может отследить, что запись появилась не с моs.ru, а с потолка?
— В блокчейне нет никакой записи, чтобы проверить подлинность избирателя. Предполагается, что она проверяется вне блокчейна. Он публичный, и если что-то такое туда писать, то можно нарушить тайну голосования.
— А скажи, пожалуйста, в хэш транзакции голосования (который, напомним, объединяет и публичный, и непубличный блокчейн), в него никак не пишется никаким скрытым образом voter_id, sudir_id, group_id?
— Нет, ничего не пишется.
— А откуда хэш берется, поясни его происхождение?
— Хэш формируется из содержимого голоса непосредственно. И еще там есть подпись избирателя, но подпись здесь такая немного странная — она прямо вот на каждом новом бюллетене генерируется новая, как если бы вы каждый раз подписывали все время по-разному.
— Правильно ли я понимаю, что нет никакой возможности по подписи избирателя проверить его легитимность?
— В публичном блокчейне — нет.
— Этот хэш каким-то образом инкорпорирует в себя хэши предыдущих транзакций или он строго связан только с этой транзакцией?
— Строго связан только с этой транзакцией, строго только с этим голосом.
— Правильно ли я понимаю, что собственно блокчейновость имеет место, но обепечивается не хэшем голосования, а другими хэшами?
То есть эта база «прошита» дополнительными хэшами насквозь, как книга избирателей прошита ниткой. Благодаря чему из блокчейна невозможно ничего «выкинуть». Но это — другие хэши, не тот хэш, который связывает публичный и непубличный блокчейн.
— Да. Но в принципе ничего необычного в этом нет.
— Итак, мы поняли, что, имея ключ организатора голосования, можно вписать в публичный блокчейн любую информацию о том, что было проголосовано за кого-то, при этом не указывать, кем проголосовано. И тем самым мы обходим необходимость логиниться на Госуслугах, создавать фейковых избирателей, отслеживать, кто из избирателей не голосует, или же взламывать их аккаунты, верно?
— Да. И если приводить бумажный аналог, это можно сравнить с тем, как если бы я пришел на избирательный участок, предварительно напечатав бюллетени на принтере, и вбросил бы эту самостоятельно напечатанную пачку.
— То есть бюллетени, которые не учтены ни при получении их избиркомом, ни при выдаче избирателям, — просто мы из типографии принесли.
— Да.
— В таком случае мы в обычной жизни попали бы на то, что у нас не сошлось бы количество бюллетеней в урне и количество голосующих избирателей: было бы в урне больше бюллетеней, чем избирателей по книгам, и тогда бы урну пришлось аннулировать.
Та же самая проверка должна быть встроена и в систему ДЭГ, с которой мы имеем дело, потому что она записывает факт выдачи бюллетеней. Если предположить, что злоумышленник, атакующий систему, атаковал ее так, как ты предлагаешь, то получается какая-то глупость: у него будет голосов больше, чем выдано бюллетеней. Как ты это объясняешь, то есть как ты с этим справишься в качестве злоумышленника?
— Во-первых, я хочу сказать, что, по публичным данным, именно это и произошло, на что движение «Голос» обратило внимание сразу же после подведения итогов.
Если мы зайдем прямо сейчас на observer.mos.ru, то увидим, что голосов в системе больше, чем выдано бюллетеней.
Так что у нас действительно так и произошло. Объяснение здесь, откуда тут взялись бюллетени, лежит в алгоритме переголосований, потому что какие-то из этих бюллетеней были первые, какие-то вторые, какие-то третьи, какие-то последние. И нам нужно выкинуть все эти бюллетени, которые были переголосованы потом каким-то другим бюллетенем. В моей предлагаемой схеме взлома мы накидали туда голосов, но потом, для того чтобы подвести «хорошие» итоги, нам нужно какие-то выкинуть. Для этого есть удобный механизм в виде второго блокчейна, который позволяет нам сказать, что вброшенные бюллетени относятся к некоему идентификатору группы бюллетеней.
— Напомню нашим читателям, что идентификатор группы бюллетеней одинаковый у всех бюллетеней, которые поступили от данного избирателя.
— Да. И мы можем по ходу голосования писать в публичный блокчейн голоса, которые за провластного кандидата, а в закрытый блокчейн ничего не писать до поры до времени. Когда у нас закончилось голосование, опубликован ключ расшифрования, мы расшифровываем все голоса и смотрим, какие из них за оппозицию. Про вброшенные голоса мы говорим: вот у нас есть голоса, мы запомнили их хэши. После голосования мы внесем эти хэши и в тайный блокчейн. При этом запись мы сделаем в виде химеры: мы возьмем хэш вброшенного бюллетеня (за власть) и group_id бюллетеня за оппозицию. Таким образом, мы «перетрем» ненастоящим бюллетенем, который мы кинули в систему, настоящий бюллетень реального избирателя.

— Давай еще раз проговорим: когда мы вбрасывали, мы вбрасывали их как бы безымянными. Поэтому не записывали их во второй блокчейн.
Как же нам удалось их потом вставить вот в этот второй блокчейн? Мы же много раз повторили, что блокчейн устроен как прошитый и запечатанный документ, из которого нельзя выдрать или вставить лист, потому что каждая следующая транзакция несет в себе следы предыдущей. Почему же с потайным блокчейном это не работает?
— Во-первых, из-за того, что этот блокчейн сам по себе приватный, никто его никогда не видел. Если мы его перепишем заново с нуля, все равно никто ничего не заметит, вы не видели, как он составлялся, поэтому вы не увидите, что мы его подменили.
— В отличие от публичного блокчейна, который хотя бы раз в полчаса выгружался на observer.
— Да. Во-вторых, можно, например, выключить подсчет голосов между тем моментом, когда мы опубликовали ключи и все (или часть) расшифровали, и моментом, когда надо подводить итоги. Если у нас, например, все не расшифровалось, но ключ уже опубликован, мы останавливаем подсчет голосов, останавливаем все эти блокчейны, расшифровываем голоса и смотрим, какие нам из них не нравятся, и дописываем их в этот закрытый блокчейн так, чтобы выкинуть те голоса, которые нас не устраивают.
— Итак, повторим, как мы сделали химеру: этруски взяли голову змеи и приделали к хвосту льва, а мы взяли хэши от голосований, которые были за оппозицию, нашли в закрытом блокчейне, какой group_id соответствует данному хэшу, потом взяли этот group_id, вставили в новую строчку закрытого блокчейна и хэш заменили на хэш от вброшенного голоса от ноунейма, правильно?
Пример изготовления химеры
Пример транзакции из приватного блокчейна с тестового голосования с записанным зашифрованным group_id:
{"voting_id":"6a2bd471332d3dc0c3d25bb170b4ba18a8ffdd1b09cb6a6a36304d7272da96f9","store_tx_hash":
"1791e2b22570a54a44a662a42c4f6e92074177f7ac7d2732f5a3f322b9fac132","encrypted_group_id":
"DPVz0ezcVoq3jCuepPPShfEqSGeOrDrs3H9P1za1uydq6MimiKrEJGi8xQ2zqr95Ws+
usKa/XLxp2WEShbbcNzpjAhI3ZjcXS0nW5HfjrylRDgTeNp1Rdpgg/tcJobCoJhX+
dTSBwEZRjHVhaVemTASKqDMxGCNHrGW1EzbjExp7AVhk4DwbTwZ+
s/chdm+
Mt10W6bzylfAb5xlq/a8xgRCtPTbNbI9Ce3PoOU2RHF7yWkl1SEIlm2lFSfISaoOTptm5QwwzjUb/
GJnqbM9Elm62gD+d2iubw0ZAtIcZ3x+ISPBU0yB4qkDTT/okAwev","ts":"1627535034000"}
Чтобы переголосовать настоящий голос, мы смотрим в БЧ-2 и видим неприятную транзакцию:
{"voting_id":"ID","store_tx_hash":"<хэш за «нехорошего» кандидата>","encrypted_group_id":"<зашифрованный ID группы реального избирателя>","ts":"<20:00 18 сентября>"}
У нас есть ненастоящий, вброшенный в 13.00 19 сентября голос за «хорошего» кандидата. Чтобы переголосовать настоящий голос нашим вброшенным, мы пишем следующую транзакцию в БЧ-2:
{"voting_id":"ID","store_tx_hash":"<хэш фейкового голоса за 'хорошего' кандидата>","encrypted_group_id":"<зашифрованный ID группы реального избирателя>","ts":"<13:00 19 сентября>"}
Несмотря на то, что в зашифрованный ID группы пишется реальное время голоса, БЧ-2 при подсчете берет время из открытого источника — поля ts, где время не зашифрованное. Поэтому можно подставить тот же самый шифротекст, не расшифровывая encrypted_group_id.
После этого продолжаем подсчет в штатном режиме, система уже не знает, что мы подменили реальный голос фейковым.
Все так, да. И добавили в запись время, что этот голос пришел позже, чем тот голос, который мы выкинули.
— И дальше мы можем честно посчитать приватный блокчейн, после того как мы в него вкинули голоса, и приватный блокчейн нам скажет, что люди голосовали за оппозицию, а меньше чем через день раздумали, верно?
— Да. Такая вот система получилась дырявая.
— Какие еще интересные дырки или странности в этой системе удалось тебе увидеть?
— Мы нашли, что помимо блокчейна-1 (публичного) и блокчейна-2 (тайного) в коде системы ДЭГ предусмотрено ведение некоей «обычной» базы данных. Когда сообщение с голосом приходит в систему, помимо того, чтобы записать его в блокчейн-1 и (часть данных) в блокчейн-2, он еще попадает в отдельную таблицу под названием p_ballot — считай, это просто табличка в Excel. Зачем это делается, из исходного кода абсолютно непонятно. Туда просто пишутся данные, никак не используются.
Таблица p_ballot
Ссылка на код.
Функция persistBallot вызывается сразу же после получения бюллетеня вот тут.
Более того, в этой таблице все бюллетени пронумерованы по порядку их добавления в систему. При добавлении в таблицу бюллетеню автоматически назначается порядковый номер (поле ID): 1, 2, 3, 4 и т.д.
Это видно исходя из того, как эта таблица создается при запуске голосования.
Нумерация бюллетеней прямо запрещена законом.
— Это была странность номер один?
— А теперь странность номер два. Тот сервис, который шифрует идентификаторы группы избирателей, чтобы они зашифрованными попали в блокчейн-2, имеет внутри себя логи. Логи — это специальный текстовый файл, который создается по ходу работы программы, для того чтобы программист понимал, что с ней происходит, что она делает, или мог понимать, где какие ошибки могут возникнуть. Так вот, в нашем случае в этот текстовый файл пишется содержимое следующего рода. На каждый запрос к этому серверу что-то зашифровать он пишет сначала сообщение: зашифровываю вот такой-то текст. Дальше пишет опять в этот же лог: зашифровал этот текст следующим образом. Таким образом, если у вас неправильно настроено логирование в этом сервисе, у вас все пишется в этот лог, а по умолчанию именно так у меня и произошло, то все шифрование теряет абсолютно смысл, потому что вы читаете этот текстовый файл и видите нешифрованный и шифрованный текст одновременно.
Пример лога сервиса шифрования
[2021-10-09 13:38:49] production.INFO: Получение запроса на шифрование сообщения {"action":"crypt_request","request":{"data":"test"}}
[2021-10-09 13:38:49] production.INFO: Ответ с телом зашифрованного сообщения: {"action":"crypt_response","response":{"result":"J18pQE9c0K/xzeNS3gtJUi/AnjNIhUJKAuPrSZs1S5YYAr9E5FqQnPNtxZL93EE+OX7lQY+pf8JDHTL2nn98VA=="}}
[2021-10-09 13:38:50] production.INFO: Request served: /api/encryption/crypt?data=test {"action":"request_served","method":"GET","uri":"/api/encryption/crypt?data=test","duration":308.49,"version":8,"requestIp":"127.0.0.1","code":null}
— Напоминает анекдот про Штирлица и парашютные стропы.
— Примерно так, да. Странность номер три заключается в том, что идентификатор группы избирателей создается на основе идентификатора в mos.ru. Таким образом, если у нас есть доступ к сайту mos.ru и доступ к всему списку избирателей, мы можем из содержимого закрытого блокчейна или этой странной «таблицы в экселе» понять, кто как голосовал, каждый из двух миллионов человек.
Как это может нарушить анонимность
За создание group_id избирателя (также известный в системе в том числе под названием mdmCypher) отвечает вот этот участок кода.
Код делает следующее:
- Считает $ssoIdHmac = hash_hmac('stribog512', $sudirId, $hmacSecret)
hmacSectet — соль для хэширования из переменной окружения (аналогично ключу шифрования для groupId)
sudirId — ID избирателя в системе СУДИР (система управления доступом к информационным ресурсам г. Москвы), считайте ID избирателя в mos.ru.
- Обращается к сервису componentX для получения по ssoIdHmac соответствующую группу бюллетеней. Получает зашифрованный groupId избирателя.
Шифруется он в методе receiveGid.
Сам groupId получается из «подсистемы «Реестр участников голосования», или еще он называется MDM (Master Data Management). Исходного кода этой части у нас уже нет.
Делается это следующим запросом.
Опять обращу внимание, что в логи пишется ssoIdHmac и groupId избирателя.
Чтобы нарушить тайну голосования надо сделать следующее:
sudirId сопоставить с персональными данными избирателя.
Для sudirId всех 2 млн человек посчитать ssoIdHmac.
Запросом в MDM получить groupId всех ssoIdHmac.
Посмотреть в содержимое «закрытого блокчейна», либо в неправильно настроенные логи, либо в таблицу p_ballot
Там найти хэши со всеми голосами данных groupId в публичном блокчейне.
Посмотреть в публичном блокчейне, за кого был отдан голос. С шага один у нас есть персональные данные этого избирателя.
— Дмитрий Нестеров многократно указывал в своих выступлениях на то, что электронное голосование чисто теоретически не может совместить две вещи: проверку легитимности и анонимность. Либо одно, либо другое. Правильно ли я понимаю, что ровно это мы и видим сейчас с двумя блокчейнами? Публичный блокчейн не может проверить легитимность, но при этом соблюдает вашу анонимность, а приватный блокчейн нарушает вашу анонимность (потому что в создании group_id используется идентификатор избирателя с mos.ru), но при этом как бы знает о том, легитимный вы избиратель или нет.
— Примерно так, да.
— Были ли еще сюрпризы в исходном коде?
— Наконец, мы выяснили, что исходный код, который был опубликован, отличается от того, который реально использовался. Сначала заметили, что отличается та часть кода, которая исполняется в браузере пользователя. Дальше мы обнаружили, что в опубликованном коде переголосовывать можно только в текущие сутки, а не через 24 часа после первого голоса (как было на реальных выборах). Я не думаю, что исходный код очень сильно изменялся, но все равно некрасиво со стороны ДИТ.
Подмена кода
Файл election.js из реального голосования можно найти по ссылке. Можете сами убедиться, что он отличается от файла в репозитории.
Наконец, в исходном коде действительно записана логика, что переголосовывать можно только в течение текущих суток.
— И наконец, возвращаясь к тому ключу, с помощью которого в блокчейн можно писать все что угодно, в обход mos.ru или Госуслуг. Есть ли у тебя понимание, исходя из анализа исходного кода и твоих экспериментов, в каких формах мог существовать этот ключ?
— Я напомню, что сейчас мы говорим о ключе подписи организаторов выборов. Это электронно-цифровая подпись, которой организатор выборов подписывает свои сообщения, что это именно он создает голосование, именно он регистрирует избирателей, именно он выдает бюллетень, именно он подводит итоги. Когда я понял значимость этого ключа, у меня сразу возник вопрос: а кто имел доступ к этому ключу и как он формировался?
И для меня было большим расстройством увидеть, что в тестовом голосовании по вопросу общественного транспорта, по вопросу прививок, обязательной вакцинации от коронавируса использовался тот же самый ключ, который использовался на выборах в Госдуму. Это значит, что между 30 июля и 17–19 сентября непонятно какие люди, непонятно на каком основании могли смотреть на этот ключ, могли не смотреть на этот ключ, и это очень расстраивает — то, что этот ключ не пересоздавался для каждого голосования заново, меня очень пугает. Это навевает мысли о плохих практиках программирования в ДИТ Москвы. Во-первых, когда секретные ключи просто переиспользуются, это уже нарушение правил информационной безопасности. Во-вторых, я опасаюсь, что этот ключ мог быть просто записан в текстовый файл, и к нему непонятно какие программисты могли иметь доступ, в непонятно какое время (у нас удалены были конфигурационные файлы из исходного кода, и я не могу это подтвердить или опровергнуть).
Благодарность Петра Жижина за помощь в анализе исходного кода: чату программистов, которые анализировали ДЭГ, и, в частности, Борису Тавадову, и программисту под псевдонимом SinX.