Расследования · Политика

Пошли на крайние химеры

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

Андрей Заякин , сооснователь «Диссернета», редактор data-отдела «Новой»

Фото: РИА Новости

На дальних подступах и непосредственно перед выборами авторы «Новой газеты» предупреждали о потенциальных уязвимостях дистанционного электронного голосования (ДЭГ). В день выборов на электронном избиркоме происходили странные события, о чем нашим читателям рассказали его свидетели — Илья Сухоруков, Анна Лобонок и Николай Колосов. Мы попытались получить внятные объяснения у ЦИК, департамента информационных технологий (ДИТ) Москвы и отвечающего за электронное голосование в Москве Артема Костырко, но от личного разговора Артем Костырко и Элла Памфилова уклонились, а в письменной коммуникации мы не получили внятных ответов. 

Статистический анализ физика Максима Гонгальского показал, что, скорее всего, основным механизмом фальсификаций, обеспечившим победу провластных кандидатов, было «скручивание» голосов оппозиции через переголосование, и оценил количество «скрученного» в 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 бюллетеня за оппозицию. Таким образом, мы «перетрем» ненастоящим бюллетенем, который мы кинули в систему, настоящий бюллетень реального избирателя.

Фото: URA.RU / TASS

— Давай еще раз проговорим: когда мы вбрасывали, мы вбрасывали их как бы безымянными. Поэтому не записывали их во второй блокчейн. 

Как же нам удалось их потом вставить вот в этот второй блокчейн? Мы же много раз повторили, что блокчейн устроен как прошитый и запечатанный документ, из которого нельзя выдрать или вставить лист, потому что каждая следующая транзакция несет в себе следы предыдущей. Почему же с потайным блокчейном это не работает?

— Во-первых, из-за того, что этот блокчейн сам по себе приватный, никто его никогда не видел. Если мы его перепишем заново с нуля, все равно никто ничего не заметит, вы не видели, как он составлялся, поэтому вы не увидите, что мы его подменили.

— В отличие от публичного блокчейна, который хотя бы раз в полчаса выгружался на 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. Зачем это делается, из исходного кода абсолютно непонятно. Туда просто пишутся данные, никак не используются. 

Ссылка на код.

Функция 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) отвечает вот этот участок кода.

Код делает следующее:

1. Считает $ssoIdHmac = hash_hmac('stribog512', $sudirId, $hmacSecret)

hmacSectet — соль для хэширования из переменной окружения (аналогично ключу шифрования для groupId)

sudirId — ID избирателя в системе СУДИР (система управления доступом к информационным ресурсам г. Москвы), считайте ID избирателя в mos.ru.

2. Обращается к сервису componentX для получения по ssoIdHmac соответствующую группу бюллетеней. Получает зашифрованный groupId избирателя.

Шифруется он в методе receiveGid.

Сам groupId получается из «подсистемы «Реестр участников голосования», или еще он называется MDM (Master Data Management). Исходного кода этой части у нас уже нет.

Делается это следующим запросом

Опять обращу внимание, что в логи пишется ssoIdHmac и groupId избирателя.

Чтобы нарушить тайну голосования надо сделать следующее:

1. sudirId сопоставить с персональными данными избирателя.

2. Для sudirId всех 2 млн человек посчитать ssoIdHmac.

3. Запросом в MDM получить groupId всех ssoIdHmac.

4. Посмотреть в содержимое «закрытого блокчейна», либо в неправильно настроенные логи, либо в таблицу p_ballot

5. Там найти хэши со всеми голосами данных groupId в публичном блокчейне.

6. Посмотреть в публичном блокчейне, за кого был отдан голос. С шага один у нас есть персональные данные этого избирателя.

— Дмитрий Нестеров многократно указывал в своих выступлениях на то, что электронное голосование чисто теоретически не может совместить две вещи: проверку легитимности и анонимность. Либо одно, либо другое. Правильно ли я понимаю, что ровно это мы и видим сейчас с двумя блокчейнами? Публичный блокчейн не может проверить легитимность, но при этом соблюдает вашу анонимность, а приватный блокчейн нарушает вашу анонимность (потому что в создании group_id используется идентификатор избирателя с mos.ru), но при этом как бы знает о том, легитимный вы избиратель или нет.

— Примерно так, да.

— Были ли еще сюрпризы в исходном коде?

— Наконец, мы выяснили, что исходный код, который был опубликован, отличается от того, который реально использовался. Сначала заметили, что отличается та часть кода, которая исполняется в браузере пользователя. Дальше мы обнаружили, что в опубликованном коде переголосовывать можно только в текущие сутки, а не через 24 часа после первого голоса (как было на реальных выборах). Я не думаю, что исходный код очень сильно изменялся, но все равно некрасиво со стороны ДИТ.

Файл election.js из реального голосования можно найти по ссылке. Можете сами убедиться, что он отличается от файла в репозитории.

Наконец, в исходном коде действительно записана логика, что переголосовывать можно только в течение текущих суток.

— И наконец, возвращаясь к тому ключу, с помощью которого в блокчейн можно писать все что угодно, в обход mos.ru или Госуслуг. Есть ли у тебя понимание, исходя из анализа исходного кода и твоих экспериментов, в каких формах мог существовать этот ключ?

— Я напомню, что сейчас мы говорим о ключе подписи организаторов  выборов. Это электронно-цифровая подпись, которой организатор выборов подписывает свои сообщения, что это именно он создает голосование, именно он регистрирует избирателей, именно он выдает бюллетень, именно он подводит итоги. Когда я понял значимость этого ключа, у меня сразу возник вопрос: а кто имел доступ к этому ключу и как он формировался? 

И для меня было большим расстройством увидеть, что в тестовом голосовании по вопросу общественного транспорта, по вопросу прививок, обязательной вакцинации от коронавируса использовался тот же самый ключ, который использовался на выборах в Госдуму. Это значит, что между 30 июля и 17–19 сентября непонятно какие люди, непонятно на каком основании могли смотреть на этот ключ, могли не смотреть на этот ключ, и это очень расстраивает — то, что этот ключ не пересоздавался для каждого голосования заново, меня очень пугает. Это навевает мысли о плохих практиках программирования в ДИТ Москвы. Во-первых, когда секретные ключи просто переиспользуются, это уже нарушение правил информационной безопасности. Во-вторых, я опасаюсь, что этот ключ мог быть просто записан в текстовый файл, и к нему непонятно какие программисты могли иметь доступ, в непонятно какое время (у нас удалены были конфигурационные файлы из исходного кода, и я не могу это подтвердить или опровергнуть).

Благодарность Петра Жижина за помощь в анализе исходного кода: чату программистов, которые анализировали ДЭГ, и, в частности, Борису Тавадову, и программисту под псевдонимом SinX.