В сегод­няшней статье я хочу помучить тебя самым ненавис­тным для некото­рых сту­ден­тов пред­метом — выс­шей матема­тикой. А имен­но рас­ска­зать о рас­простра­нен­ном, но мало­извес­тном алго­рит­ме крип­топод­писи EdDSA + SHA-512.

warning

Статья име­ет озна­коми­тель­ный харак­тер и пред­назна­чена для иссле­дова­телей безопас­ности и спе­циалис­тов по раз­работ­ке защиты прог­рам­мно­го обес­печения. Автор и редак­ция не несут ответс­твен­ности за любой вред, при­чинен­ный с при­мене­нием изло­жен­ной информа­ции. Наруше­ние автор­ских прав и патен­тно­го законо­датель­ства, рас­простра­нение кон­тра­фак­тно­го ПО и наруше­ние работы сис­тем прес­леду­ются по закону.

Каж­дый ува­жающий себя хакер прос­то обя­зан знать вра­га в лицо и раз­бирать­ся в основных крип­тоал­горит­мах, популяр­ных и не очень. Так, крип­топод­пись EdDSA, нес­мотря на доволь­но широкое исполь­зование (I2P, OpenBSD, OpenSSH и так далее), в отли­чие от сво­ей эллипти­чес­кой сес­тры EcDSA силь­но обде­лена реали­заци­ями в стан­дар­тных крип­тобиб­лиоте­ках. Ее мож­но най­ти раз­ве что в узкоспе­цифи­чес­ких про­ектах типа NaCl, SUPERCOP, python-ed25519 и про­чей экзо­тике.

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

Предмет исследования

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

Ка­жет­ся, что все в ней прек­расно: в понят­ном тек­сто­вом виде хра­нит­ся и вер­сия, и ID обо­рудо­вания, а так­же вре­мя начала и кон­ца сро­ка дей­ствия в мил­лисекун­дах. Мож­но сме­ло пра­вить и поль­зовать­ся! Но нет, в кон­це мы обна­ружи­ваем поле с нед­вусмыс­ленным име­нем signature, где в шес­тнад­цатерич­ном виде закоди­рован некий 64-бай­товый (512-бит­ный) блок дан­ных.

F8 BF BA E3 73 E5 AC 75 3F 38 AC DA F2 0C 42 B1 80 0C 18 A0 BF 92 8F 39 5C B1 A1 8E 2E E7 47 62
F1 3A E7 94 2A 5F 61 6D 97 5F 76 5B FD 82 B6 58 A0 D7 4C 07 3B 7E 61 A1 B3 85 75 62 93 94 90 06

Дей­стви­тель­но ли это сиг­натура или под­пись тек­сто­вых дан­ных лицен­зии, про­верить эле­мен­тарно: замена любого бай­та в тек­сто­вой час­ти или в самой сиг­натуре дела­ет лицен­зию невалид­ной, и прог­рамма немед­ленно кида­ется пов­торно ска­чивать ее с уда­лен­ного сер­вера. Пер­вый 512-бит­ный хеш, который при­ходит в голову, — это SHA-512, его наличие под­твержда­ет и прог­рамма Krypto Analyzer, если скор­мить ей наш исполня­емый модуль.

То, что это не слу­чай­ный набор бай­тов, а реаль­ная ини­циали­зация под­сче­та хеша SHA-512, нам под­твержда­ет и IDA, в которой мы откры­ваем код при­ложе­ния по любез­но пре­дос­тавлен­ному нам ана­лиза­тором адре­су.

Од­нако счастье ока­зыва­ется не так близ­ко, как кажет­ся: пос­читан­ная кон­троль­ная сум­ма SHA-512 совер­шенно не похожа на зна­чение signature, а зна­чит, для под­писи не годит­ся. Под­пись счи­тает­ся каким‑то иным спо­собом.

Поп­робу­ем коп­нуть с дру­гой сто­роны: най­ти про­вер­ку под­писи в отладчи­ке. Заг­ружа­ем нашу прог­рамму в x64dbg и ищем про­вер­ку под­писи. Прог­рамма написа­на на чис­том C++, ничем не защище­на и сво­бод­на от анти­отладчи­ков, поэто­му иско­мое мес­то находит­ся эле­мен­тарно, в псев­докоде IDA оно выг­лядит так.

Будь мы чуть более ленивы­ми, то закоро­тили бы эту фун­кцию на return 0 (да, при успешной верифи­кации она воз­вра­щает имен­но 0) и успо­коились бы, но тог­да смыс­ла писать эту статью не было бы никако­го. Поп­робу­ем все‑таки разоб­рать­ся, как устро­ена сиг­натура и как она верифи­циру­ется.

Оче­вид­но, что финаль­ную про­вер­ку выпол­няет фун­кция sub_14000517D, которая слож­ным и неп­рямым спо­собом срав­нива­ет на тож­дес­твен­ное равенс­тво две 32-бай­товые пос­ледова­тель­нос­ти. Пер­вую из них IDA-шный деком­пилятор условно обзы­вает a1 + 8. Она пред­став­ляет собой пер­вые 32 бай­та сиг­натуры, то есть в нашем слу­чае выг­лядит так:

F8 BF BA E3 73 E5 AC 75 3F 38 AC DA F2 0C 42 B1 80 0C 18 A0 BF 92 8F 39 5C B1 A1 8E 2E E7 47 62

Вто­рая пос­ледова­тель­ность вычис­ляет­ся фун­кци­ей sub_1401155E0 и условно называ­ется a1 + 104. Оста­вим пока без вни­мания тре­вож­ный зво­ночек «почему про­веря­ется толь­ко пер­вая полови­на сиг­натуры, зачем тог­да вто­рая?» и тупо поп­робу­ем перепод­писать лицен­зию зна­чени­ем a1 + 104. То есть меня­ем в тек­сто­вой час­ти лицен­зии любой байт, ста­вим точ­ку оста­нова на про­вер­ку, берем новое зна­чение a1 + 104 и под­став­ляем его вмес­то пер­вых 32 байт сиг­натуры.

План кажет­ся надеж­ным, как швей­цар­ские часы, но он не работа­ет: пос­ле замены пер­вой полови­ны сиг­натуры зна­чение a1 + 104 сно­ва меня­ется. Это озна­чает, что пер­вая полови­на сиг­натуры вхо­дит в обе час­ти это­го крип­тоурав­нения. Вто­рая полови­на сиг­натуры тоже находит­ся доволь­но быс­тро — она под условным име­нем a1 + 40 явля­ется аргу­мен­том фун­кций sub_1401162C0 и sub_140114D30 в цепоч­ке весь­ма голово­лом­ного рас­чета про­вероч­ного зна­чения a1 + 104.

Поп­робу­ем все‑таки опоз­нать крип­тоал­горитм. Потыкав­шись по фун­кци­ям, ничего харак­терно­го не обна­ружи­ваем, лишь какие‑то дол­гие, лома­ющие мозг вычис­ления, напоми­нающие опе­рации с BigInteger, коими они, оче­вид­но, и явля­ются. Самой пер­спек­тивной в этом пла­не пред­став­ляет­ся фун­кция sub_1401161B0.

По­мимо того что она визу­аль­но кра­сива и инту­итив­но понят­на, эта фун­кция содер­жит мас­су вол­шебных цифр. Собс­твен­но, ее смысл зак­люча­ется в том, что она воз­вра­щает стар­шие 32 бита сум­мы вто­рой полови­ны сиг­натуры a1 + 40 и неко­его магичес­кого 256-бит­ного чис­ла:

EFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB2106215D086329A7ED9CE5A30A2C13

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

1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3ed

Ес­ли перевес­ти млад­шую часть это­го зна­чения в десятич­ный вид, мы получим

27742317777372353535851937790883648493

А это доволь­но извес­тная кон­стан­та! При­чем извес­тная имен­но в таком сочета­нии — в виде сум­мы с двой­кой в 252-й сте­пени (2^252 + эта кон­стан­та). Это так называ­емый порядок (order) базовой точ­ки кри­вой Эдвар­дса, исполь­зующей­ся крип­тоал­горит­мом EdDSA.

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

www

Же­лающие могут самос­тоятель­но вка­тить­ся в эту увле­катель­ную тему на Ви­ки, чуть менее под­робно, но более доход­чиво тема опи­сана на xmr.ru и в цик­ле ста­тей на Хаб­ре.

На­конец, для сов­сем прод­винутых отлични­ков дос­тупна ан­гло­языч­ная литера­тура по ис­поль­зованию кри­вых Эдвар­дса в крип­тогра­фии.

Для всех тех, кому читать всю эту теорию лень и неин­терес­но, поп­робую объ­яснить суть задачи на паль­цах, что­бы хотя бы базовые прин­ципы даль­нейше­го обсужде­ния ста­ли ясны.

Нач­ну с самого глав­ного и весь­ма прис­кор­бно­го фак­та: эллипти­чес­кая крип­тогра­фия асим­метрич­на. Те, кто уже стал­кивались, нап­ример, с RSA, зна­ют, что это такое: для каж­дой под­писи есть два клю­ча. Один (зак­рытый, сек­ретный) — у раз­работ­чика, с помощью это­го клю­ча он и фор­миру­ет под­пись. И откры­тый (пуб­личный) — у поль­зовате­ля, который про­веря­ет этим клю­чом валид­ность под­писи. Сде­лано это для того, что­бы ты никак не смог под­делать под­пись на изме­нен­ной лицен­зии. Как это ни грус­тно зву­чит, в нашем при­мере мы тоже не смо­жем это­го сде­лать при всем желании.

Од­нако мы можем соз­дать собс­твен­ную под­пись, сге­нери­ровав пару клю­чей и под­менив пуб­личный ключ в при­ложе­нии сво­им. Не буду объ­яснять, чем патч пуб­лично­го клю­ча луч­ше гораз­до более прос­того пат­ча верифи­кации сиг­натуры, ведь наше иссле­дова­ние прес­леду­ет не вре­донос­ную, а чис­то поз­наватель­ную цель.

Итак, поп­робу­ем разоб­рать­ся в алго­рит­ме про­вер­ки под­писи, сге­нери­ровать свою пару клю­чей, най­ти в нашем при­ложе­нии пуб­личный ключ и, поменяв его, перепод­писать лицен­зию новой под­писью.

Теория

Об­ратить­ся к теории нам все‑таки при­дет­ся. Для начала опре­делим­ся, что же такое эллипти­чес­кая кри­вая. Тем, кто не про­читал науч­ные опре­деле­ния по при­веден­ным выше ссыл­кам или про­читал, но ничего не понял, даю полез­ный совет: забудь как минимум кар­тинки, которые ты там видел. На самом деле это никакая не эллипти­чес­кая и никакая не кри­вая.

На прак­тике это чудовищ­но огромное, но конеч­ное дис­крет­ное мно­жес­тво точек, при­чем мно­жес­тво упо­рядо­чен­ное. Каж­дая точ­ка име­ет два целочис­ленных парамет­ра — коор­динаты X и Y, фак­тичес­ки псев­дослу­чай­ные, вычис­ляемые по очень слож­ным фор­мулам. Ты спро­сишь: каким обра­зом мно­жес­тво упо­рядо­чено?

На нем опре­дели­ли опе­рации сло­жения точ­ки со ска­ляр­ной кон­стан­той и дру­гой (или той же самой) точ­кой. При­чем через опе­рацию сло­жения с самой собой опре­деле­на и опе­рация умно­жения. На этом мно­жес­тве так­же опре­деле­на базовая точ­ка (которую называ­ют B или, как боль­ше любят жен­щины, точ­ка G). Пос­коль­ку рас­смат­рива­емое нами мно­жес­тво, называ­емое эллипти­чес­кой кри­вой, зам­кну­то и огра­ничен­но, то через опре­делен­ное чис­ло шагов скла­дыва­ния с самой собой точ­ка G перехо­дит в себя же. То есть G = G * L.

Вот это самое вол­шебное чис­ло L и называ­ется поряд­ком базовой точ­ки кри­вой Эдвар­дса, про­вер­ку на пре­выше­ние которо­го мы наш­ли в фун­кции sub_1401161B0. Все вычис­ления в алго­рит­ме дела­ются по модулю L, соот­ветс­твен­но, ска­ляр­ная кон­стан­та, пре­выша­ющая L, заведо­мо оши­боч­на, имен­но поэто­му и выпол­няет­ся про­вер­ка.

Поп­робую теперь наг­лядно опи­сать про­цесс под­писи лицен­зии и ее про­вер­ки при помощи алго­рит­ма EdDSA. Для прос­тоты и удобс­тва забудем пока про реаль­ные коор­динаты X и Y каж­дой точ­ки и нарису­ем мно­жес­тво E в виде зам­кну­той эллипти­чес­кой кри­вой. Изоб­разим базовую точ­ку G на ней. В этом слу­чае опе­рации сло­жения и умно­жения точ­ки на кри­вой будут пред­ста­вимы прос­то в виде переме­щения точ­ки по кри­вой на опре­делен­ное чис­ло шагов.

Вы­берем в качес­тве зак­рытого клю­ча любое слу­чай­ное чис­ло, мень­шее поряд­ка кри­вой s < L, тог­да откры­тым (пуб­личным) клю­чом будет точ­ка A на кри­вой, рав­ная A = G * s. Суть несим­метрич­ной крип­тогра­фии зак­люча­ется в том, что ско­рость обратной к умно­жению опе­рации прос­то несо­изме­римо ниже, чем само умно­жение. То есть получить при­ват­ный ключ из сек­ретно­го в нашей реали­зации (32-бай­товые или 256-бит­ные опе­ран­ды) мож­но мгно­вен­но, а обратная задача на акту­аль­ном железе может счи­тать­ся целую веч­ность.

Вы­берем теперь некое про­изволь­ное чис­ло r < L (на самом деле не сов­сем про­изволь­ное: в раз­ных реали­заци­ях его для пущей жес­ти ста­вят в зависи­мость от сек­ретно­го клю­ча и даже от хеша под­писыва­емо­го сооб­щения). Теперь соз­дадим соот­ветс­тву­ющую ему точ­ку R = r * G. Далее счи­таем хеш от под­писыва­емых дан­ных (назовем его прос­то HASH). Счи­тать его мож­но как угод­но, хеш‑фун­кция может быть про­изволь­ной, количес­тво битов хеша дол­жно сос­тавлять 512, хотя потом это зна­чение все рав­но нор­миру­ется по модулю L.

Имен­но поэто­му опре­делен­ное рас­простра­нение получи­ла связ­ка EdDSA + SHA-512, которую мы сей­час и раз­бира­ем. Нес­мотря на про­изволь­ность реали­зации, по канону положе­но, что­бы в хеширу­емые дан­ные обя­затель­но вхо­дил откры­тый ключ (дво­ичное пред­став­ление точ­ки A) и дво­ичное пред­став­ление точ­ки R. Для чего это сде­лано, ты, наде­юсь, уже понял, поп­робовав поменять зна­чение R в сиг­натуре (а это и есть пер­вые 32 бай­та сиг­натуры). Вто­рой полови­ной сиг­натуры будет чис­ло S = r + HASH * s. В общем, про­цесс под­писи выг­лядит при­мер­но так, как показа­но на рисун­ке, сиг­натура выделе­на зеленым.

Поп­робу­ем теперь верифи­циро­вать получен­ную под­пись. Берем зна­чение S (вто­рые 32 бай­та сиг­натуры) и умно­жаем на базовую точ­ку G. Получа­ем новую точ­ку G * S, затем счи­таем зна­чение HASH выше­опи­сан­ным обра­зом, умно­жаем его на пуб­личный ключ A и получа­ем еще одну точ­ку A * HASH. А затем прос­то счи­таем раз­ность меж­ду ними. Если верифи­кация про­ходит, дол­жна получить­ся точ­ка R.

В чем сек­рет фокуса? Дело в том, что для при­нятых на E опе­раций сло­жения и умно­жения, нес­мотря на всю слож­ность их реали­зации, точ­но так же выпол­няет­ся рас­пре­дели­тель­ный закон: G * S A * HASH = G * (r + HASH * s) G * s * HASH = G * (r + HASH * s HASH * s) = G * r = R.

Переходим к практике

На­деюсь, такого прос­того объ­ясне­ния было дос­таточ­но для понима­ния теории, пос­коль­ку далее мы перей­дем к суровой прак­тике, а имен­но к ана­лизу кода. Пос­коль­ку реали­зация алго­рит­ма нас­толь­ко слож­на, что при взгля­де на вос­ста­нов­ленный псев­докод даже самой прос­той фун­кции ста­новит­ся жут­ко, поп­робу­ем упростить себе задачу. Для начала най­дем готовую реали­зацию EdDSA и по ана­логии раз­берем логику и струк­туру алго­рит­ма.

Я счи­таю самым под­ходящим кан­дидатом для срав­нения про­ект Monocypher. Если открыть ис­ходник реали­зации матема­тичес­ких при­мити­вов и вос­ста­нов­ленный IDA псев­докод, то соз­дает­ся впе­чат­ление, что код ком­пилиро­вал­ся пря­мо с них. Единс­твен­ная, но очень досад­ная помеха — в качес­тве хеш‑фун­кции в нем исполь­зует­ся алго­ритм Blake2, тоже доволь­но экзо­тичес­кий. Воз­можно, я ког­да‑нибудь рас­ска­жу и о нем, но нам он не под­ходит, так как у нас исполь­зует­ся SHA-512.

На самом деле это не очень боль­шая проб­лема. Поп­робу­ем на при­мере кон­крет­ной сиг­натуры про­верить работос­пособ­ность реали­зации Monocypher для нашего слу­чая. Вспом­ним най­ден­ную нами ранее фун­кцию верифи­кации сиг­натуры sub_140118940 и запишем ее с уче­том получен­ных зна­ний:

// Верификация сигнатуры, возвращает 0, если успешно, в противном случае 0xFFFFFFFF
// Примерный аналог crypto_eddsa_check
__int64 __fastcall sub_140118940(__int64 a1)
// v5=SHA512(R || Public key || License)
(*(void (__fastcall **)(__int64, char *))(*(_QWORD *)a1 + 24i64))(a1, v5);
// Обрезает хеш v5 по модулю L, аналог crypto_eddsa_reduce
sub_1401170F0((__int64)v5);
// v4 = S, вторая половина сигнатуры
sub_1401162C0(v4, a1 + 40, 8i64);
// v3 = –A, где A публичный ключ a1 + 104 ge_frombytes_neg_vartime
if ( (unsigned int)sub_140114FA0(v3, a1 + 104) ||
// Если S > L, то сигнатура неверная is_above_l
(unsigned int)sub_1401161B0(v4) )
return 0xFFFFFFFFi64;
// v3 = G * S A * HASH
sub_140114D30(v3, (__int64)v5, a1 + 40);
// ge_tobytes преобразование полученного результата из формата расширенных координат в обычный 32-байтовый для последующего сравнения с R
sub_1401155E0(a1 + 104, v3);
// Проверка точек на равенство crypto_verify32
return sub_14000517D(a1 + 8);
}

Как видишь, реали­зация верифи­кации в нашей прог­рамме хоть и ана­логич­ная, но все‑таки нем­ного не такая, как в моносай­феров­ских фун­кци­ях. Давай все же про­верим сов­мести­мость этой реали­зации с нашими хешами. Конеч­но, по‑хороше­му было бы логич­ным прик­рутить к Monocypher хеш‑фун­кцию SHA-512 вмес­то Blake2, но мы нем­ного схал­турим. Откро­ем нашу прог­рамму в x64dbg и пос­тавим точ­ку оста­нова сра­зу пос­ле вызова sub_1401170F0. Получа­ем нор­мализо­ван­ное по L зна­чение HASH:

3A 8A 86 6E 84 C4 EC 81 5E 0E 77 EB 94 1C 8F 87 35 42 C6 36 64 63 56 12 E3 A0 32 A9 79 95 DE 05

Те­перь у нас есть все зна­чения для тес­та: дела­ем из них коротень­кую тес­товую про­цедур­ку и встав­ляем ее в Monocypher.

void test()
{
u8 public_key[32] = { 0x8F,0xEE,0xEB,0xD8,0x76,0x69,0x7A,0xD5,0x37,0xD4,0x47,0xC7,0x3B,0xBC,0x37,0xC8,0x35,0x63,0x1C,0x93,0xA5,0x0C,0x9F,0x2C,0x55,0x2A,0x01,0x22,0x18,0x7C,0x4E,0x5B };
u8 h[32] = { 0x3A,0x8A,0x86,0x6E,0x84,0xC4,0xEC,0x81,0x5E,0x0E,0x77,0xEB,0x94,0x1C,0x8F,0x87,0x35,0x42,0xC6,0x36,0x64,0x63,0x56,0x12,0xE3,0xA0,0x32,0xA9,0x79,0x95,0xDE,0x05};
u8 signature[64] = { 0xF8,0xBF,0xBA,0xE3,0x73,0xE5,0xAC,0x75,0x3F,0x38,0xAC,0xDA,0xF2,0x0C,0x42,0xB1,
0x80,0x0C,0x18,0xA0,0xBF,0x92,0x8F,0x39,0x5C,0xB1,0xA1,0x8E,0x2E,0xE7,0x47,0x62,0xF1,0x3A,0xE7,0x94,0x2A,0x5F,0x61,0x6D,0x97,0x5F,0x76,0x5B,0xFD,0x82,0xB6,0x58,0xA0,0xD7,0x4C,0x07,0x3B,0x7E,0x61,0xA1,0xB3,0x85,0x75,0x62,0x93,0x94,0x90,0x06 };
return crypto_eddsa_check_equation(signature, public_key, h);
}

Тест про­ходит пре­вос­ходно, и это хорошо — ведь мы, по сути, не име­ем ни малей­шего пред­став­ления, как была сге­нери­рова­на под­пись из нашей прог­раммы. Тем не менее под­пись прош­ла верифи­кацию, а зна­чит, Monocypher мож­но зас­тавить работать и для генера­ции сво­ей собс­твен­ной под­писи. Давай про­верим и эту гипоте­зу.

Сна­чала поп­робу­ем сге­нери­ровать новые клю­чи для EdDSA. Для это­го выбира­ем сек­ретный ключ, как про­изволь­ное зна­чение crypto_eddsa_key_pair seed — слу­чай­но выб­ранная соль генера­тора. Что­бы не замора­чивать­ся, сде­лаем его вооб­ще нулевым:

u8 seed[32] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
crypto_eddsa_key_pair(secret_key, public_key, seed);

Име­ем на выходе:

Пуб­личный ключ
19 d3 d9 19 47 5d ee d4 69 6b 5d 13 01 81 51 d1
af 88 b2 bd 3b cf f0 48 b4 50 31 c1 f3 6d 18 58
Сек­ретный ключ
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
19 d3 d9 19 47 5d ee d4 69 6b 5d 13 01 81 51 d1
af 88 b2 bd 3b cf f0 48 b4 50 31 c1 f3 6d 18 58

Как видишь, фор­мат хра­нения сек­ретно­го клю­ча у Monocypher вооб­ще хал­турный — хра­нит­ся толь­ко соль от него и сге­нери­рован­ный пуб­личный ключ. Во вре­мя всех опе­раций с сек­ретным клю­чом он тупо вос­ста­нав­лива­ется из соли, но нам как‑то пофиг: все рав­но сек­ретный ключ даль­ше это­го тес­тового кода не пой­дет. Теперь поп­робу­ем под­писать получен­ным клю­чом новую лицен­зию. Для это­го вос­поль­зуем­ся кодом про­цеду­ры crypto_eddsa_sign:

// secret_key только что сгенерированный секретный ключ
void crypto_eddsa_sign(u8 signature [64], const u8 secret_key[64],
// Совершенно любое сообщение любой длины, по сути, оно требуется нам чисто как соль для r
const u8 *message, size_t message_size)
{
u8 a[64]; // secret scalar and prefix
u8 r[32]; // secret deterministic "random" nonce
u8 h[32]; // publically verifiable hash of the message (not wiped)
u8 R[32]; // first half of the signature (allows overlapping inputs)
crypto_blake2b(a, 64, secret_key, 32);
crypto_eddsa_trim_scalar(a, a);
hash_reduce(r, a + 32, 32, message, message_size, 0, 0);
crypto_eddsa_scalarbase(R, r);
// Вот в этом месте тормозим отладчиком исполнение и смотрим полученные значения r и R
hash_reduce(h, R, 32, secret_key + 32, 32, message, message_size);
COPY(signature, R, 32);
crypto_eddsa_mul_add(signature + 32, h, a, r);
WIPE_BUFFER(a);
WIPE_BUFFER(r);
}

Пос­ле вызова crypto_eddsa_scalarbase мы име­ем сле­дующее:

R
07 e9 25 9e 9c d4 2d 1a 7c 61 69 54 00 71 b6 47
ff b7 b9 c8 ee d7 06 8b 76 bf 14 5b 6e 22 f0 28

...и соот­ветс­тву­ющий ему r:

ec cf 58 70 79 3d f9 ab a3 22 19 8e 8c bb 66 72
38 0d 69 1e f2 2e f8 18 a7 39 fd 22 fb 1a 78 06

Те­перь нам пред­сто­ит сно­ва перес­читать хеш, но уже на осно­ве исправ­ленных R и пуб­лично­го клю­ча. Сно­ва запус­каем x64dbg, меня­ем пер­вую полови­ну сиг­натуры на R, а ста­рый пуб­личный ключ — на све­жес­генери­рован­ный. Затем оста­нав­лива­емся на точ­ке оста­нова вызова sub_1401170F0 и получа­ем новое зна­чение HASH:

5F DF 27 01 E9 8D C6 40 B7 A0 2F BD 2B 66 FF F5 1E A4 9D 19 58 8B EF F3 8E 8E 1D 99 52 00 F8 07

За­ходим обратно в monocypher.c и под­став­ляем зна­чение нового хеша вмес­то hash_reduce:

void crypto_eddsa_sign(u8 signature [64], const u8 secret_key[64],
const u8 *message, size_t message_size)
{
u8 a[64]; // secret scalar and prefix
u8 r[32]; // secret deterministic "random" nonce
u8 h[32] = { 0x5F,0xDF,0x27,0x01,0xE9,0x8D,0xC6,0x40,0xB7,0xA0,0x2F,0xBD,0x2B,0x66,0xFF,0xF5,0x1E,0xA4,0x9D,0x19,0x58,0x8B,0xEF,0xF3,0x8E,0x8E,0x1D,0x99,0x52,0x00,0xF8,0x07 };
u8 R[32]; // first half of the signature (allows overlapping inputs)
crypto_blake2b(a, 64, secret_key, 32);
crypto_eddsa_trim_scalar(a, a);
hash_reduce(r, a + 32, 32, message, message_size, 0, 0);
crypto_eddsa_scalarbase(R, r);
// hash_reduce(h, R, 32, secret_key + 32, 32, message, message_size);
COPY(signature, R, 32);
crypto_eddsa_mul_add(signature + 32, h, a, r);
WIPE_BUFFER(a);
WIPE_BUFFER(r);
}

На выходе получа­ем пол­ное зна­чение сиг­натуры перепод­писан­ной новым клю­чом лицен­зии:

07 e9 25 9e 9c d4 2d 1a 7c 61 69 54 00 71 b6 47
ff b7 b9 c8 ee d7 06 8b 76 bf 14 5b 6e 22 f0 28
a5 9c c9 16 19 78 19 cf 69 c5 65 66 9b e3 c8 3c
03 b0 96 8d 07 a1 e8 80 3d fb e7 3e c6 47 be 02

Выводы

Как видишь, мы научи­лись при помощи прос­того модуля Monocypher, написан­ного на C, не толь­ко под­писывать свои дан­ные сиг­натурой EdDSA, но и перепод­писывать чужие. Конеч­но, я понимаю, что крат­кость статьи не поз­воля­ет охва­тить мно­жес­тво важ­ных и инте­рес­ных момен­тов исполь­зования EdDSA: исто­рию соз­дания, раз­личия и пре­иму­щес­тва по срав­нению с обыч­ным EcDSA, осо­бен­ности генера­ции клю­чей и циф­ровой под­писи, быс­трую реали­зацию матема­тичес­ких при­мити­вов на эллипти­чес­кой кри­вой и мно­гое дру­гое. Если рас­ска­зывать обо всем этом, то матери­ала хва­тит на целую кни­гу, но цели написать кни­гу у меня нет. Я наде­юсь, что пре­дос­тавил дос­таточ­но ссы­лок на матери­алы, с помощью которых любоз­натель­ный и пыт­ливый читатель смо­жет най­ти любую инте­ресу­ющую его информа­цию об этой тех­нологии.