Сообщений 1    Оценка 475        Оценить  
Система Orphus

Криптография на эллиптических кривых для чайников

Автор: Холодилов Сергей Александрович
Опубликовано: 08/08/2012
Исправлено: 10.12.2016
Версия текста: 1.1
Схема Эль-Гамаля
Исходные данные
Шифрование
Подпись
Обобщим
Эллиптические кривые
Подпись
Второй тип кривых
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, что: y = gx mod p.

Исходные данные

ПРИМЕЧАНИЕ

Если вы помните алгебру, то g – это образующая мультипликативной группы вычетов по модулю p. Как искать такое g, написано, например, тут.

Открытый ключ – это тройка (p, g, y), секретный ключ – d.

Шифрование

Постановка задачи: преобразовать сообщение так, чтобы его мог прочитать только тот, знает секретный ключ.

Шифрующий вычисляет:

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

Текст чуть меньше чем наполовину скопирован из заголовочных файлов 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);

По умолчанию они сброшены, соответственно, по умолчанию все сохраняется.

Подпись своими руками

Внезапно, ещё немного математики :) В примечании в разделе "Теория" я писал, что, на самом деле, подпись вычисляется не совсем так. Пора узнать, как это происходит. Напомню обозначения:

Для фиксации отличий сравним два варианта: описанный выше как продолжение схемы Эль-Гамаля и принятый стандартом ECDSA. Вычисление подписи;:

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

Нужно же включить в статью хоть немного своего кода. Код прямолинеен и не проверяет ошибки, но вроде бы освобождает ресурсы и более-менее работает. В любом случае он предназначен для того, чтобы показать, как можно реализовать алгоритм, а не как надо писать программы. Полная версия с тестами идёт в качестве приложения к статье, а здесь только две основные функции.

Создание подписи:

      // Создание подписи в соответствии с ГОСТ Р 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        Оценить