Принцип работы эллиптической криптографии
Задается неявное уравнение плоской кривой специального вида, называемого эллиптической кривой. Задается простое число, образующее поле вычетов по модулю. Координаты точек кривой принадлежат этому полю, т.е. являются целыми неотрицательными числами, не превышающее модуль. Над двумя точками кривой задается операция сложения таким образом, чтобы новая точка также принадлежала кривой. Если точку сложить саму с собой то получится умножение на 2, если прибавить еще раз — то на 3, и так далее умножение на произвольное число n. Также вместе с кривой задается базовая точка, принадлежащая кривой, операции над которой и используются в криптографии.
Для криптографии выбирается произвольное большое число n, являющееся секретным ключем, на него умножается базовая точка и новая точка служит публичным ключем, который затем умножается противоположной стороной на другое число с целью согласования ключа или проверки подписи. Стойкость основывается отсутствием на данный момент известного способа определения множителя по точке за разумное время
Ed25519
Берштейн предложил использовать эллиптическую кривую со следующими параметрами.
Уравнение кривой:
, где
Модуль:
q =
отсюда и возникло название кривой.
Базовая точка:
(x, 4/5), где x получается решением уравнения (см. ниже)
Ее координаты в явном виде:
x=15112221349535400772501151409588531511454012693041857206046113283949847762202,
y=46316835694926478169428394003475163141307993866256225615783033603165251855960.
Сложение:
, где
Следует заметить, что деление здесь это не обычное деление с остатком, а умножение на обратный по модулю элемент, вычисляемый согласно малой теоремы Ферма по формуле , т. е. операция возведения в степень по модулю.
В качестве публичного ключа предлагается использовать только координату y, при необходимости решая уравнения кривой относительно х, и вычисляя его по формуле .
Поскольку q представимо в виде 8*k+5, то для вычисления квадратного корня из х следует воспользоваться формулой . Действительно q = = , отсюда квадратный корень .
Реализация
Код полностью располагается по по адресу в файле Signature.cpp. Для работы с большими числами используются функции библиотеки BN из openssl.
Создадим класс Ed25519, реализующий саму кривую, и содержащий параметры кривой, рассчитываемые в его конструкторе. В первую очередь необходимы 3 метода: сложение, умножение на число и вычисление x по заданному y.
class Ed25519
{
//...
EDDSAPoint Sum (const EDDSAPoint& p1, const EDDSAPoint& p2) const
EDDSAPoint Mul (const EDDSAPoint& p, const BIGNUM * e) const
BIGNUM * RecoverX (const BIGNUM * y) const
//...
}
Из-за частного использования операция сложения и трудоемкости операции деления, приведем x и y к общему знаменателю (1+m)*(1-m), тем самым избавившись от одного деления. В результате код для сложения выглядит примерно так:
// m = d*p1.x*p2.x*p1.y*p2.y
BN_mul (xx, p1.x, p2.x, m_Ctx); //p1.x*p2.x
BN_mul (yy, p1.y, p2.y, m_Ctx); //p1.y*p2.y
BIGNUM * m = BN_dup (d);
BN_mul (m, m, xx, m_Ctx);
BN_mul (m, m, yy, m_Ctx);
// x = (p1.x*p2.y + p2.x*p1.y)*inv(1 + m)
// y = (p1.y*p2.y + p1.x*p2.x)*inv(1 - m)
// m1 = 1-m
BN_one (m1);
BN_sub (m1, m1, m);
// m = m+1
BN_add_word (m, 1);
// y = (p1.y*p2.y + p1.x*p2.x)*m
BN_add (y, xx, yy);
BN_mod_mul (y, y, m, q, m_Ctx);
// x = (p1.x*p2.y + p2.x*p1.y)*m1
BN_mul (yy, p1.x, p2.y, m_Ctx);
BN_mul (xx, p2.x, p1.y, m_Ctx);
BN_add (x, xx, yy);
// denominator m = m*m1
BN_mod_mul (m, m, m1, q, m_Ctx);
Inv (m);
BN_mod_mul (x, x, m, q, m_Ctx); // x = x/m
BN_mod_mul (y, y, m, q, m_Ctx); // y = y/m
Также для удвоения (сложения точки самой с собой) реализован отдельный метод Double, поскольку в этом случае p1.x = p2.x и p1.y = p2.y, что позволяет сократить число умножений. Кроме того, вместо BN_mul используется более быстрая BN_sqr.
Умножение релизовано простейшим методом удвоения и прибавления, т.е. идти вдоль числа побитово от старшего к младшему, на каждом шаге удваивать значение результата, а если бит — единица, то прибавлять умножаемую точку.
EDDSAPoint res {zero, one};
if (!BN_is_zero (e))
{
int bitCount = BN_num_bits (e);
for (int i = bitCount - 1; i >= 0; i--)
{
res = Double (res);
if (BN_is_bit_set (e, i)) res = Sum (res, p);
}
}
Вычисление x по y реализовано тривиально.
Сначала подкоренное выражение:
BN_sqr (y2, y, m_Ctx); // y^2
// xx = (y^2 -1)*inv(d*y^2 +1)
BN_mul (xx, d, y2, m_Ctx);
BN_add_word (xx, 1);
Inv (xx);
BN_sub_word (y2, 1);
BN_mul (xx, y2, xx, m_Ctx);
Затем вычисление квадратного корня по формуле
// x = srqt(xx) = xx^(2^252-2)
BN_mod_exp (x, xx, two_252_2, q, m_Ctx);
Подпись и проверка подписи
Данная тема достаточно интересна и обширна, потому этому вопросу будет посвящена отдельная статья. В то же время методы Sign и Verify реализованы и используются практически. Потому, кому интересно, можно глянуть в код. Здесь же будут лишь перечислены некоторые особенности.
Как и другие системы электронной подписи, подпись EdDSA представляет собой пару 32-х байтных чисел (R,S), потому длина подписи — 64 байта.
Числа представлены в Little Endian.
В качестве хэш-функции используется SHA-512.
При подписи случайное число не генерируется, вместо этого используется правая половина SHA-512 хэша от секретного ключа, объединенный с самими подписываемыми данными.
Также используется простое число , использовавшееся при выборе базовой точки, с тем расчетом, чтобы умножение на него базовой точки давало нуль.
Заключение
Очевидно, что самым медленным местом данной реализации является деление в операции сложения. На самом деле, пользуясь современными достижениями теории чисел и спецификой параметров EdDSA, можно исключить его полностью, как это сделано в ref10. Однако это существенно усложнит реализацию и сделает ее менее понятной, потому этим следует заниматься только если возникнет реальная практическая необходимость. В настоящее время проверка подписи EdDSA в I2P довольно редкое событие, по сравнению, скажем, с шифрованием по схеме Эль-Гамаля.
Существует мнение, что реализация собственной криптографии это крайне плохая идея. Однако использование непонятно как работающей реализации представляется немногим лучше. Кроме того, данная статья может быть полезна тому, кому интересно докопаться до сути, а работающий практический код поможет в этом.
Комментарии (16)
grich
29.10.2015 11:51Однако использование непонятно как работающей реализации представляется немногим лучше
Вполне понятных реализаций довольно много. Например, tweetNaCl, написанная тем же Берштейном (для нее есть порты и на другие языки), если так трудно читать стандартный NaCl или libsodium код.
Проблема Вашей реализации в том, например, что она не учитывает side channel атаки. «Прозрачные» реализации полезны только для понимания работы (хотя и обычных формул для этого достаточно), на практике же их использовать нельзя — об этом стоит писать, а не о «don't implement your own crypto»orignal
29.10.2015 14:09+1>Например, tweetNaCl, написанная тем же Берштейном
Я в процессе работы смотрел и ее. Да, она более понятна чем ref10, но там тоже много неясностей. Например, почему для извлечения корня там используется 2^252-3, а не 2^252-2? Что такое D2? Зачем нужно возводить d^2+1 в седьмую степень? Конечно, на все эти вопросы существуют ответы, но опять же это требует дополнительного изучения вопроса.grich
29.10.2015 15:14Ну, D2 — это, очевидно, удвоенное значение D.
Корень нужно извлекать не из скаляра (в таком случае действительно можно было возвести в степерь (p+3)/8), а из дроби u/v — потому что именно в таком виде хранятся координаты точки для ускорения вычислений. Отсюда, скорее всего, растут ноги (p-5)/8 = (p+3)/8-1 и седьмая степень. Все объяснения есть в статьях о Ed255 и Curve255
Разбираться в любом случае нужно, если цель — «для практического использования в I2P». Ваш код тоже, может, кому-то непонятен будет: у всех свой порог вхождения. Я не настоящий сварщик, но с TweetNaCl, думаю, разобраться можно за разумное времяorignal
29.10.2015 16:07Лично я надеюсь со временем разобраться с ref10 и использовать именно его. Просто надо идти от простого к сложного, и от работающего кода к работающему лучше.
gxcreator
29.10.2015 14:21Можете прокомментировать причины форка от основной ветки на гитхабе?
orignal
29.10.2015 14:33Посмотрите историю изменений в любом файле на гитхабе — меня там больше нет. Некие люди воспользовались моими временным отсутствием там, что выглядит крайне некрасиво с их стороны. Когда я вернулся, то решил продолжать работу над проектом в собственном форке, заодно перейдя с crypto++ на openssl.
gxcreator
29.10.2015 14:49Хм, а каким образом воспользовались? Не вижу там особо деструктивных действий
orignal
29.10.2015 14:53Во всех файлах проекта отсутствует история изменений. Как будто я два года этот проект не делал и вообще «вас тут не стояло».
Одному человеку были даны все права доступа к проекту совершенно для иных целей.
pravic
30.10.2015 23:54А где там форк? Нигде не нашёл упоминания.
orignal
31.10.2015 00:48Я сделал форк от релиза 0.10.0 с гитхаба на bitbucket. Здесь
bitbucket.org/orignal/i2pd
Scratch
Пару моментов:
1) Bernstein
2) Немного оффтоп. Он же написал скрипт, который позволяет довольно наглядно сравнить разные стандарты цифровой подписи. Видно, как на самом деле они похожи