Сообщений 1 Оценка 475 Оценить |
Схема Эль-Гамаля Эллиптические кривые OpenSSL Реализация ГОСТ Р 34.10 - 2001 |
Недавно мне случайно понадобилось разобраться в том, как работает ECDSA (Elliptic Curve Digital Signature Algorithm; цифровая подпись на эллиптических кривых) вообще и его реализация в OpenSSL в частности. В процессе написалась эта статья.
ПРИМЕЧАНИЕ Было бы, конечно, здорово, если бы её написал настоящий специалист в этой области, но увы, что есть, то есть. Правда, специалисты читали и вроде не плевались. |
Были использованы следующие источники (помимо википедии):
Текст, насколько это было в моих силах, им соответствует; мне сложно судить о диапазоне существующих вариаций. Если где-то есть сильно отличающиеся стандарты и реализации, нужно смотреть отдельно.
ПРИМЕЧАНИЕ Наш ГОСТ Р 34.10 - 2001 достаточно близок, об отличиях я напишу. Пользуясь случаем, посылаю луч распознавания текста милым людям, выкладывающим pdf с ГОСТом в виде набора картинок. Особенно это актуально для приложения Б, содержащего контрольный пример с большим количеством 256-битных чисел. К счастью, вот здесь есть несколько неуклюжая, но всё же текстовая версия. Огромное спасибо сайту http://www.bestpravo.ru. |
Итак, речь пойдёт о том, как работает цифровая подпись во варианте ECDSA. Я надеюсь, про цифровые подписи вообще, открытые-закрытые ключи и т.п. вы уже где-то слышали и довольно точно представляете себе что это такое и зачем нужно.
Сначала нужно поговорить про схему Эль-Гамаля, так как с кривыми будет то же самое, только сложнее. Почему-то мне раза три в разных местах рассказывали про RSA, а про Эль-Гамаля ни разу, и боюсь, что не только мне.
И то и другое – системы шифрования с открытыми ключами. RSA основана на сложности задачи разложения числа на множители, а схема Эль-Гамаля – на задаче дискретного логарифма. Оказывается, зная (p, g, y), очень сложно найти такой x, что
ПРИМЕЧАНИЕ Если вы помните алгебру, то g – это образующая мультипликативной группы вычетов по модулю p. Как искать такое g, написано, например, тут. |
Открытый ключ – это тройка (p, g, y), секретный ключ –
Постановка задачи: преобразовать сообщение так, чтобы его мог прочитать только тот, знает секретный ключ.
Шифрующий вычисляет:
a = gk mod p b = yk m mod p |
Пара (a, b) – это зашифрованное сообщение. Расшифровка:
m = a(p - 1 - d) b mod p |
почему это работает:
a(p - 1 - d) b mod p = /* подставляем значения a и b */ = gk(p - 1 - d) yk m mod p = /* подставляем значение y */ = gk(p - 1 - d) gdk m mod p = /* степени можно сложить */ = gk(p - 1 - d) + kd m mod p = /* и упростить */ = gk(p - 1)m mod p = /* по упоминавшейся малой теореме Ферма */ = m mod p = m |
Постановка задачи: подтвердить, что сообщение отправлено владельцем секретного ключа.
Подписывающий вычисляет:
r = gk mod p s – число, такое что: dr + ks = m mod (p-1) |
ПРИМЕЧАНИЕ Искать s, конечно, расширенным алгоритмом Евклида. Взаимная простота k и p-1 нужна для гарантии существования такого s. |
пара (r, s) – это подпись сообщения. Проверка подписи:
gm mod p == yrrs mod p |
Почему это работает:
yrrs mod p = /* подставляем значения y и r*/ = gdrgks mod p = gdr + ks mod p = /* по условию выбора s, i -- какое-то целое */ = gm + i(p-1) mod p = /* опять малая теорема Ферма */ = gm mod p |
Это всё мило, но несколько прямолинейно. Добавим немного алгебры.
Для начала нужно отовсюду убрать "mod p" и сказать вместо этого, что дело происходит в конечном поле. От этого вообще ничего не изменится.
Потом можно заметить, что нам тут не нужно поле, достаточно циклической группы порядка q и любой биекции из неё в {1, 2, … q}. (биекция понадобится, чтобы преобразовать сообщение m [число] в некоторый элемент группы и обратно, и ещё в паре мест). Назовём биекцию буквой f и попробуем кратко воспроизвести всё написанное выше.
Шифруем:
a = gk b = yk f-1(m) |
Расшифровываем:
a(q - d) b = gk(q - d) yk f-1(m) = gk(q - d) gkd f-1(m) = f-1(m) |
Итого, m = f(a(q - d) b), как ожидалось.
Подпись:
r = gk s – число, такое что: df(r) + ks = m mod q |
Проверяем:
yf(r)rs = gdf(r)gks = gdf(r)+ks = gm + iq = gm |
Работает. Теперь можно двигаться дальше.
Я толком не знаю, что такое "'эллиптическая кривая" с точки зрения алгебры, но в криптографии применяется всего два частных случая. Для начала займемся тем, что попроще. Второй частный случай отличается не принципиально, но там формулы сложнее и сущностей больше (про него будет пара слов ниже).
Множество E(F) = {(x,y) из F2 | y2 = x3 + ax + b} + {O} – эллиптическая кривая. Т.е. множество пар, компоненты которых удовлетворяют приведённому уравнению над F, и ещё один элемент – O. Элементы кривой называются точками, O – "точка на бесконечности".
Поскольку пары должны удовлетворять уравнению, понятно, что:
На точках E(F) вводится операция сложения:
Итак, мы получили абелеву группу. Аналогом возведения в степень в аддитивной записи будет соответствующее количество сложений, оно же – умножение на целое число (для более полной аналогии с Эль-Гамалем проще было бы описывать в мультипликативной записи, но я не решился идти против традиции).
Параметры (p, a, b, G, n, h) вместе задают конкретную эллиптическую кривую с точки зрения криптографии.
Несложно заметить, что у нас получилось не совсем то, что нужно.
Во-первых, G – не образующая.
Во-вторых, у нас нет биекции из E(F) в {1, 2, … |E(F)|}.
Но для подписи хватит и того, что есть. Итак:
Вычисляем:
R = kG = (xR, yR) r = xR s: dr + ks = m mod n |
пара (R, s) -- подпись. Проверяем:
rQ + sR = rdG + skG = (rd + ks)G = mG |
Ура, товарищи! Вот она какая, криптография на эллиптических кривых.
ПРИМЕЧАНИЕ Стандартная схема подписи чуть-чуть отличается, но эта ничуть не хуже и ближе к Эль-Гамалю. Правильный алгоритм описан ниже, а пока отличия от стандартной схемы для понимания не существенны. |
Описанные выше кривые, задающиеся шестёркой (p, a, b, G, n, h), называются кривыми над простым полем, они же GFp-кривые.
Во втором случае кривая задается семёркой (m, f(x), a, b, G, n, h). Здесь поле задают два первых параметра, уравнение – третий и четвёртый (хотя это немного другое уравнение и немного другие a и b), а три последних параметра точно те же, что и в первом случае. И точки тоже просто пары элементов поля. Это кривые над полем характеристики 2m, они же GF2m-кривые.
У GF2m-кривых отличаются правила сложения точек, но в результате всё равно имеем абелеву группу и ту же самую идею.
ПРИМЕЧАНИЕ В том, что касается практики: низкоуровневый интерфейс позволяет манипулировать отдельными параметрами кривых, но с точки зрения высокоуровневого они (теоретически) ничем не отличаются. |
Текст чуть меньше чем наполовину скопирован из заголовочных файлов OpenSSL, но важно же расположить их в нужной последовательности и верно поставить акценты! :)
К счастью, нам не нужно выбирать параметры кривой, искать точку G, определять n и h. Это давно сделано за нас людьми, которые понимают в этом гораздо больше (кстати, параноику на заметку). Есть стандарты с рекомендациями и вариантами, из них можно выбрать, а в конкретных системах нужно использовать конкретные кривые, которые полюбились авторам.
Желающие могут, конечно, придумать свою кривую (и для этого тоже есть развернутые рекомендации), но проще вызывать кривые как демонов, по имени.
Примеры имён можно увидеть в файле include/openssl/obj_mac.h:
ПРИМЕЧАНИЕ Для файлов, присутствующих в стандартной установке, даётся стандартный путь, для файлов из исходников – путь в исходниках. Ссылки даю на последнюю (на данный момент) версию из CVS: |
#define SN_secp256k1 "secp256k1" #define NID_secp256k1 714 #define OBJ_secp256k1 OBJ_secg_ellipticCurve,10L #define SN_secp384r1 "secp384r1" #define NID_secp384r1 715 #define OBJ_secp384r1 OBJ_secg_ellipticCurve,34L #define SN_secp521r1 "secp521r1" #define NID_secp521r1 716 #define OBJ_secp521r1 OBJ_secg_ellipticCurve,35L #define SN_sect113r1 "sect113r1" #define NID_sect113r1 717 #define OBJ_sect113r1 OBJ_secg_ellipticCurve,4L |
Кривые с названиями такого вида взяты из документа SEC 2: Recommended Elliptic Curve Domain Parameters.
Число в середине означает длину порядка группы в битах, это же оценка сложности вскрытия. Подразумевается, что злоумышленнику понадобится около 2t/2 операций (есть более точные оценки, но приблизительно так).
secp... – GFp-кривая
sect... – GF2m-кривая (возможно, "t" от слова two)
Суффикс маркирует разное происхождение рекомендуемых параметров кривой: "k" в честь Neal Koblitz (их как-то по-умному считают), "r" от слова random (один раз запустили генератор, записали на бумажку и теперь рекомендуют использовать). И есть ещё какие-то типы кривых для X9.62, у них другие имена, но этого я уже совсем не знаю. Соответствующие именам параметры можно посмотреть в файле openssl/crypto/ec/ec_curve.c.
ПРИМЕЧАНИЕ ГОСТ Р 34.10-2001 описывает только GFp-кривые с 256-битным порядком группы. Конкретные параметры кривой там не приводятся, только ограничения на них. Насколько я понял, кривая secp256k1 не подойдёт (так как у неё a == 0, это нарушает ограничение на инвариант кривой), а secp256r1 использовать можно. |
Кривой соответствует тип данных EC_GROUP*, точке EC_POINT* (это "непрозрачные" указатели, если не залезать в исходники, нам не известно, на что они указывают; в этот раз в исходники не полезем). Вот несколько функций из файла include/openssl/ec.h (там ещё много, и документированы они только там, я постарался выбрать наиболее полезные).
Создание/освобождение кривой:
/** Creates a EC_GROUP object with a curve specified by a NID * \param nid NID of the OID of the curve name * \return newly created EC_GROUP object with specified curve or NULL * if an error occurred */ EC_GROUP *EC_GROUP_new_by_curve_name(int nid); /** Frees a EC_GROUP object * \param group EC_GROUP object to be freed. */ void EC_GROUP_free(EC_GROUP *group); |
Создать новую точку, освободить, вычисление выражения nG + m1P1 + m2P2 + ..:
/** Creates a new EC_POINT object for the specified EC_GROUP * \param group EC_GROUP the underlying EC_GROUP object * \return newly created EC_POINT object or NULL if an error occurred */ EC_POINT *EC_POINT_new(const EC_GROUP *group); /** Frees a EC_POINT object * \param point EC_POINT object to be freed */ void EC_POINT_free(EC_POINT *point); /** Computes r = generator * n sum_{i=0}^num p[i] * m[i] * \param group underlying EC_GROUP object * \param r EC_POINT object for the result * \param n BIGNUM with the multiplier for the group generator (optional) * \param num number futher summands * \param p array of size num of EC_POINT objects * \param m array of size num of BIGNUM objects * \param ctx BN_CTX object (optional) * \return 1 on success and 0 if an error occured */ int EC_POINTs_mul(const EC_GROUP *group, EC_POINT *r, const BIGNUM *n, size_t num, const EC_POINT *p[], const BIGNUM *m[], BN_CTX *ctx); |
Последнюю функцию можно вызывать так:
EC_POINTs_mul(group, r, n, 0, 0, 0, 0); |
Получается просто nG, как раз то, что нужно для получения открытого ключа из секретного. Точку r нужно инициализировать с помощью EC_POINT_new до вызова EC_POINTs_mul.
Более высокий уровень абстракции – ключ. Открытый и секретный ключи на уровне интерфейса не отличают, есть всего один EC_KEY*, в котором можно установить отдельно открытую и секретную части. А можно сгенерировать новый полноценный.
Из того же файла include/openssl/ec.h (я убрал стандартные комментарии, т.к. там всё тривиально, и добавил свои на всякий случай и потому что люблю зелёный цвет):
// Создаёт ключ, ассоциированный с заданной группой. Открытая и секретная части установлены в 0 EC_KEY *EC_KEY_new_by_curve_name(int nid); // Освобождает ключ void EC_KEY_free(EC_KEY *key); // Возвращает кривую const EC_GROUP *EC_KEY_get0_group(const EC_KEY *key); // Возвращает секретный ключ const BIGNUM *EC_KEY_get0_private_key(const EC_KEY *key); // Устанавливает секретный ключ. Тупо копирует, не следит за тем, чтобы открытый ему соответствовал int EC_KEY_set_private_key(EC_KEY *key, const BIGNUM *prv); // Возвращает открытый ключ const EC_POINT *EC_KEY_get0_public_key(const EC_KEY *key); // Устанавливает открытый ключ, не проверяет его соответствие секретному int EC_KEY_set_public_key(EC_KEY *key, const EC_POINT *pub); // Генерирует новый случайный ключ. // В комментарии было сказано, что генерирует секретный и "optional" соответствующий открытый, // но судя по коду, открытый тоже устанавливается всегда int EC_KEY_generate_key(EC_KEY *key); // Проверяет, что параметры ключа похожи на правду// Если секретный ключ не 0, проверяет, что открытый ему соответствует int EC_KEY_check_key(const EC_KEY *key); |
Можно порадоваться, что я не наврал: открытый ключ – это действительно точка, а секретный – это действительно число. Вот она, польза теории.
Точка – это пара элементов поля (можно считать пара чисел), удовлетворяющих уравнению кривой. Если подставить в уравнение конкретный x, останется квадратное уравнение относительно y (для GFp-кривых просто y2 = c) и, если уметь решать такие уравнения над конечным полем, получаем всего два подходящих y. Значит, чтобы идентифицировать точку, достаточно указать x и как-то обозначить, какой из двух y выбрать. Такое представление точки называется компактным (compressed). Развёрнутое представление – это просто x и y. В бинарном виде первый байт указывает вид представления и, если оно компактное, то ещё и указывает, который y выбрать.
ПРИМЕЧАНИЕ Есть ещё третий вариант, гибридный он сочетает в себе недостатки обоих: первый байт как у компактного, но передаются и x, и у. Не знаю, кому это понадобилось. |
ПРЕДУПРЕЖДЕНИЕ На моей домашней убунту 10.4 попытки использовать компактные представления для точек GF2m-кривых заканчиваются с ошибкой "called a function that was disabled at compile-time", версия openssl 0.9.8, старенькая, но именно её предлагает стандартный репозиторий. Для GFp-кривых всё работает нормально, остальные функции для GF2m тоже работают нормально, то есть их поддержка не отключена полностью. На убунте 11.10 с openssl 1.0.0e работает всё, что я проверял. |
Функции, всё ещё из файла include/openssl/ec.h:
/** Enum for the point conversion form as defined in X9.62 (ECDSA) * for the encoding of a elliptic curve point (x,y) */ typedef enum { /** the point is encoded as z||x, where the octet z specifies * which solution of the quadratic equation y is */ POINT_CONVERSION_COMPRESSED = 2, /** the point is encoded as z||x||y, where z is the octet 0x02 */ POINT_CONVERSION_UNCOMPRESSED = 4, /** the point is encoded as z||x||y, where the octet z specifies * which solution of the quadratic equation y is */ POINT_CONVERSION_HYBRID = 6 } point_conversion_form_t; /** Encodes a EC_POINT object to a octet string * \param group underlying EC_GROUP object * \param p EC_POINT object * \param form point conversion form * \param buf memory buffer for the result. If NULL the function returns * required buffer size. * \param len length of the memory buffer * \param ctx BN_CTX object (optional) * \return the length of the encoded octet string or 0 if an error occurred */ size_t EC_POINT_point2oct(const EC_GROUP *group, const EC_POINT *p, point_conversion_form_t form, unsigned char *buf, size_t len, BN_CTX *ctx); /** Decodes a EC_POINT from a octet string * \param group underlying EC_GROUP object * \param p EC_POINT object * \param buf memory buffer with the encoded ec point * \param len length of the encoded ec point * \param ctx BN_CTX object (optional) * \return 1 on success and 0 if an error occured */ int EC_POINT_oct2point(const EC_GROUP *group, EC_POINT *p, const unsigned char *buf, size_t len, BN_CTX *ctx); /* other interfaces to point2oct/oct2point: */ BIGNUM *EC_POINT_point2bn(const EC_GROUP *, const EC_POINT *, point_conversion_form_t form, BIGNUM *, BN_CTX *); EC_POINT *EC_POINT_bn2point(const EC_GROUP *, const BIGNUM *, EC_POINT *, BN_CTX *); char *EC_POINT_point2hex(const EC_GROUP *, const EC_POINT *, point_conversion_form_t form, BN_CTX *); EC_POINT *EC_POINT_hex2point(const EC_GROUP *, const char *, EC_POINT *, BN_CTX *); |
Кроме того, зная тип кривой, точки можно разными способами разобрать/собрать по координатам функциями типа EC_POINT_set_affine_coordinates_GFp и т.п. Координаты же это просто BIGNUM, для них есть много вариантов.
Для ключей идеологически правильно (но не обязательно) использовать высокоуровневые функции (файл всё тот же):
/** Decodes a private key from a memory buffer. * \param key a pointer to a EC_KEY object which should be used (or NULL) * \param in pointer to memory with the DER encoded private key * \param len length of the DER encoded private key * \return the decoded private key or NULL if an error occurred. */ EC_KEY *d2i_ECPrivateKey(EC_KEY **key, const unsigned char **in, long len); /** Encodes a private key object and stores the result in a buffer. * \param key the EC_KEY object to encode * \param out the buffer for the result (if NULL the function returns number * of bytes needed). * \return 1 on success and 0 if an error occurred. */ int i2d_ECPrivateKey(EC_KEY *key, unsigned char **out); /** Decodes a ec public key from a octet string. * \param key a pointer to a EC_KEY object which should be used * \param in memory buffer with the encoded public key * \param len length of the encoded public key * \return EC_KEY object with decoded public key or NULL if an error * occurred. */ EC_KEY *o2i_ECPublicKey(EC_KEY **key, const unsigned char **in, long len); /** Encodes a ec public key in an octet string. * \param key the EC_KEY object with the public key * \param out the buffer for the result (if NULL the function returns number * of bytes needed). * \return 1 on success and 0 if an error occurred */ int i2o_ECPublicKey(EC_KEY *key, unsigned char **out); |
Но просто так их использовать не получится. За ними стоят следующие нетривиальные мысли:
А вот обещанные флаги:
/* some values for the encoding_flag */ #define EC_PKEY_NO_PARAMETERS 0x001 #define EC_PKEY_NO_PUBKEY 0x002 // Установка-получение этих флагов unsigned EC_KEY_get_enc_flags(const EC_KEY *key); void EC_KEY_set_enc_flags(EC_KEY *eckey, unsigned int flags); // Установка-получение формата точки для i2o_ECPublicKey / o2i_ECPublicKey point_conversion_form_t EC_KEY_get_conv_form(const EC_KEY *key); void EC_KEY_set_conv_form(EC_KEY *eckey, point_conversion_form_t cform); |
По умолчанию они сброшены, соответственно, по умолчанию все сохраняется.
Внезапно, ещё немного математики :) В примечании в разделе "Теория" я писал, что, на самом деле, подпись вычисляется не совсем так. Пора узнать, как это происходит. Напомню обозначения:
R = kG = (xR, yR) r = xR Эль-Гамаль: s: dr + ks = m mod n ECDSA: s: ks - dr = m mod n <=> ks = dr + m mod n <=> s = k-1(dr + m) mod n |
То есть изменился знак в условии для s, после чего равносильными преобразованиями оно приводится к виду из SEC1. Как это влияет на проверку:
Эль-Гамаль: rQ + sR = rdG + skG = (rd + ks)G = mG ECDSA: sR - rQ = skG - rdG = (ks - rd)G = mG |
Вычитать точки не сложнее чем складывать.
Но это ещё не всё.
Эль-Гамаль: r = xR, пара (R, s) -- подпись ECDSA: r = xR mod n, пара (r, s) – подпись |
Чтобы такую подпись проверить, нужно восстановить точку R по r. Есть два варианта: простой и умный.
Простой: мы вообще-то умеем восстанавливать точку по одной координате и одному биту от второй. В данном случае мы не знаем ничего про вторую координату (даже одного бита), и мы взяли xR mod n. Но, поскольку в требованиях к кривой указано, что кофактор h не больше 4, а количество точек кривой не сильно отличается от количества точек поля (есть такая теорема), вопрос решается перебором не более чем восьми вариантов. Если хоть один из них подойдёт, значит это она.
Умный (из SEC1):
u1 = ms-1 mod n u2 = rs-1 mod n u1G + u2Q = ms-1G + rs-1Q = s-1(mG + rdG) = = s-1(m + rd)G = /* подставим выражение для s */ = (k-1(dr + m))-1(m + rd)G = k(dr + m)-1(m + rd)G = kG = R |
И в этом случае проверка заключается в том, что x-координата полученной точки R совпадает с переданным r по модулю n. Для вычисления обратного по модулю предназначена функция BN_mod_inverse.
Несложно реализовать это самостоятельно и сравнить с результатами ECDSA_do_sign, до которого мы, наконец, добрались (очевидно, что совпадения не будет, так как в создании подписи участвует случайное число k, но можно проверить их подпись вручную или наоборот).
ПРИМЕЧАНИЕ ГОСТ Р 34.10-2001 описывает другой способ вычисления s и, соответственно, другой алгоритм проверки. Вычисление: s = rd + km mod n Проверка: z1 = sm-1 mod n z2 = -rm-1 mod n z1G + z2Q = sm-1G - rm-1Q = (rd + km)m-1G - rm-1dG = (rdm-1 + k)G - rm-1dG = kG = R Собственно проверка, как и в «умном» способе из SEC1, заключается в сравнении первой координаты полученной точки с r. Стандартный OpenSSL не содержит готовых функций, реализующих этот алгоритм, но его несложно написать самостоятельно на базе низкоуровневых функций. А можно даже не писать: вроде как вот здесь предлагают модифицированный OpenSSL с поддержкой ГОСТа, но я не смотрел их код и даже не скачивал. Зато написал свой, см. последний раздел статьи. |
Наконец, использование всего этого и другой заголовочный файл: include/openssl/ecdsa.h (DSA -- digital signature algorithm). Тут есть два подхода. Во-первых, можно использовать ECDSA_SIG* (который, кстати, вполне прозрачный), это выглядит примерно так:
typedef struct ECDSA_SIG_st { BIGNUM *r; BIGNUM *s; } ECDSA_SIG; /** Allocates and initialize a ECDSA_SIG structure * \return pointer to a ECDSA_SIG structure or NULL if an error occurred */ ECDSA_SIG *ECDSA_SIG_new(void); /** frees a ECDSA_SIG structure * \param sig pointer to the ECDSA_SIG structure */ void ECDSA_SIG_free(ECDSA_SIG *sig); /** DER encode content of ECDSA_SIG object (note: this function modifies *pp * (*pp += length of the DER encoded signature)). * \param sig pointer to the ECDSA_SIG object * \param pp pointer to a unsigned char pointer for the output or NULL * \return the length of the DER encoded ECDSA_SIG object or 0 */ int i2d_ECDSA_SIG(const ECDSA_SIG *sig, unsigned char **pp); /** Decodes a DER encoded ECDSA signature (note: this function changes *pp * (*pp += len)). * \param sig pointer to ECDSA_SIG pointer (may be NULL) * \param pp memory buffer with the DER encoded signature * \param len length of the buffer * \return pointer to the decoded ECDSA_SIG structure (or NULL) */ ECDSA_SIG *d2i_ECDSA_SIG(ECDSA_SIG **sig, const unsigned char **pp, long len); /** Computes the ECDSA signature of the given hash value using * the supplied private key and returns the created signature. * \param dgst pointer to the hash value * \param dgst_len length of the hash value * \param eckey EC_KEY object containing a private EC key * \return pointer to a ECDSA_SIG structure or NULL if an error occurred */ ECDSA_SIG *ECDSA_do_sign(const unsigned char *dgst,int dgst_len,EC_KEY *eckey); /** Verifies that the supplied signature is a valid ECDSA * signature of the supplied hash value using the supplied public key. * \param dgst pointer to the hash value * \param dgst_len length of the hash value * \param sig ECDSA_SIG structure * \param eckey EC_KEY object containing a public EC key * \return 1 if the signature is valid, 0 if the signature is invalid * and -1 on error */ int ECDSA_do_verify(const unsigned char *dgst, int dgst_len, const ECDSA_SIG *sig, EC_KEY* eckey); |
Или можно сразу получать бинарную строку с закодированной в DER подписью.
/** Returns the maximum length of the DER encoded signature * \param eckey EC_KEY object * \return numbers of bytes required for the DER encoded signature */ int ECDSA_size(const EC_KEY *eckey); /** Computes ECDSA signature of a given hash value using the supplied * private key (note: sig must point to ECDSA_size(eckey) bytes of memory). * \param type this parameter is ignored * \param dgst pointer to the hash value to sign * \param dgstlen length of the hash value * \param sig memory for the DER encoded created signature * \param siglen pointer to the length of the returned signature * \param eckey EC_KEY object containing a private EC key * \return 1 on success and 0 otherwise */ int ECDSA_sign(int type, const unsigned char *dgst, int dgstlen, unsigned char *sig, unsigned int *siglen, EC_KEY *eckey); /** Verifies that the given signature is valid ECDSA signature * of the supplied hash value using the specified public key. * \param type this parameter is ignored * \param dgst pointer to the hash value * \param dgstlen length of the hash value * \param sig pointer to the DER encoded signature * \param siglen length of the DER encoded signature * \param eckey EC_KEY object containing a public EC key * \return 1 if the signature is valid, 0 if the signature is invalid * and -1 on error */ int ECDSA_verify(int type, const unsigned char *dgst, int dgstlen, const unsigned char *sig, int siglen, EC_KEY *eckey); |
Результаты одинаковые: подпись, полученную от ECDSA_sign можно прочитать d2i_ECDSA_SIG, и наоборот, i2d_ECDSA_SIG даёт подпись, подходящую для проверки ECDSA_verify.
Немного особенной эллиптической магии. При некотором везении, имея только сообщение, подпись и параметры кривой, можно подобрать по этим данным публичный ключ, для которого всё будет сходиться. И даже несколько таких ключей! Это, конечно, так себе проверка :) В этом случае собственно проверка переносится на следующий этап: полученный таким образом ключ каким-то образом сверяется с настоящим. Например, если известен хеш настоящего ключа, этого оказывается достаточно, так как можно сравнить хеши и быть полностью уверенным в результате.
Здесь уже даже SEC1 предлагает перебор по возможным R. После попадания в похожую на правду точку R предлагается следующее:
r-1(sR - mG) = /* подставляем определение s и "правильной" R */ = r-1(k-1(dr + m)kG - mG) = /* k сокращается, G за скобки */ = r-1(dr + m - m)G = r-1drG = dG = Q |
То есть при правильных исходных данных получатся правильные результаты. Но при любых исходных данных получатся какие-то результаты, т.е. какая-то точка Q. При этом, если точка R похожа на правду, то -R тоже похожа на правду (как минимум для GFp-кривых это очевидно), и её тоже можно подставить и получить в результате ключ Q'. А значит алгоритм, завершающийся после первого же правдоподобного результата, без проверки, будет ошибаться как минимум в половине случаев.
Где здесь нужно везение: число r-1 mod n должно существовать, для этого r должно быть взаимно просто с n. Этого можно добиться, выбирая случайные k, пока не попадётся нужное r.
ПРИМЕЧАНИЕ Почему-то в SEC1 об этом моменте нет ни слова, странно. |
Нужно же включить в статью хоть немного своего кода. Код прямолинеен и не проверяет ошибки, но вроде бы освобождает ресурсы и более-менее работает. В любом случае он предназначен для того, чтобы показать, как можно реализовать алгоритм, а не как надо писать программы. Полная версия с тестами идёт в качестве приложения к статье, а здесь только две основные функции.
Создание подписи:
// Создание подписи в соответствии с ГОСТ Р 34.10-2001 с фиксированным k ECDSA_SIG* mygost_sign_bn_fixedk(const BIGNUM* m, const BIGNUM* k, const EC_KEY* key, BN_CTX* gctx) { LocalBnCtx ctx(gctx); // локальный контекст, не обращайте внимания BIGNUM* n = ctx.get(); // при использовании контекста, BIGNUM* r = ctx.get(); // необходимо получить из него все BIGNUM* tmp = ctx.get(); // переменные до передачи контекста BIGNUM* s = ctx.get(); // вызываемым функциям. Это ужасно неудобно. // получаем группу и приватный ключconst EC_GROUP* group = EC_KEY_get0_group(key); const BIGNUM* priv = EC_KEY_get0_private_key(key); // получаем порядок группы EC_GROUP_get_order(group, n, ctx); /////// получение r////// вычисляем R EC_POINT* R = EC_POINT_new(group); EC_POINT_mul(group, R, k, 0, 0, ctx); // получаем её х-координату EC_POINT_get_affine_coordinates_GFp(group, R, r, 0, ctx); EC_POINT_free(R); // и берём по модулю n BN_mod(r, r, n, ctx); ////// получение s////// tmp = r * d mod n BN_mod_mul(tmp, priv, r, n, ctx); // s = (m*k + r*d) mod n BN_mod_mul(s, m, k, n, ctx); BN_mod_add(s, s, tmp, n, ctx); /////// собственно подпись///// ECDSA_SIG* sig = ECDSA_SIG_new(); BN_copy(sig->r, r); BN_copy(sig->s, s); return sig; } |
И проверка:
// Проверка подписи в соответствии с ГОСТ Р 34.10-2001 int mygost_verify_bn(const BIGNUM* m, const ECDSA_SIG* sig, const EC_KEY* key, BN_CTX* gctx) { LocalBnCtx ctx(gctx); BIGNUM* n = ctx.get(); // при использовании контекста, BIGNUM* m_inv = ctx.get(); // необходимо получить из него все BIGNUM* z1 = ctx.get(); // переменные до передачи контекста BIGNUM* z2 = ctx.get(); // вызываемым функциям. Это ужасно неудобно BIGNUM* r = ctx.get(); // группа и публичный ключconst EC_GROUP* group = EC_KEY_get0_group(key); const EC_POINT* pub = EC_KEY_get0_public_key(key); // порядок группы EC_GROUP_get_order(group, n, ctx); // получаем m_inv = m^-1 mod n BN_mod_inverse(m_inv, m, n, ctx); // z1 = s * m^-1 BN_mod_mul(z1, sig->s, m_inv, n, ctx); // z2 = r * m^-1 BN_mod_mul(z2, sig->r, m_inv, n, ctx); BN_mod_sub(z2, BN_new(), z2, n, ctx); // R = z1*G + z2*Q EC_POINT* R = EC_POINT_new(group); EC_POINT_mul(group, R, z1, pub, z2, ctx); // получаем r EC_POINT_get_affine_coordinates_GFp(group, R, r, 0, ctx); EC_POINT_free(R); BN_nnmod(r, r, n, ctx); // проверка результатаreturn (BN_cmp(r, sig->r) == 0); } |
Вот практически и весь ГОСТ.
Сообщений 1 Оценка 475 Оценить |