Skip to content
Learning Platform
Advanced
30 minutes
ECDSA Цифровая подпись Nonce secp256k1 Верификация

Prerequisites:

  • 09-elliptic-curves
  • 10-secp256k1-ed25519

ECDSA: цифровая подпись с визуализацией

Зачем это нужно в блокчейне

Каждая транзакция Bitcoin и Ethereum подписана алгоритмом ECDSA. Когда вы отправляете BTC, ваш кошелек вычисляет подпись (r, s) используя приватный ключ. Любой может проверить подпись с помощью публичного ключа, но никто не может подделать её.

Цифровая подпись — это математическое доказательство того, что именно вы авторизовали транзакцию. Без ECDSA блокчейн невозможен: не было бы способа доказать, что именно владелец средств инициировал перевод.

В Bitcoin каждая транзакция содержит поле scriptSig с ECDSA-подписью. В Ethereum подпись (v, r, s) включена в каждую транзакцию и используется для восстановления адреса отправителя через ecrecover.

🧭Нужно вспомнить модулярную арифметику и обратные элементы? MATH-01: Числа и нотация

Что такое цифровая подпись

Цифровая подпись обеспечивает три свойства:

СвойствоОписаниеАналогия
АутентификацияПодтверждение личности подписантаКак подпись на документе
ЦелостностьГарантия, что сообщение не измененоКак печать на конверте
НеотказуемостьПодписант не может отрицать подписьКак нотариальное заверение

В отличие от рукописной подписи, цифровая подпись математически привязана к конкретному сообщению. Нельзя “перенести” подпись с одной транзакции на другую.

ECDSA: подпись по шагам

Пройдем алгоритм подписи ECDSA пошагово. Нажимайте “Далее” чтобы увидеть каждый этап вычисления:

ECDSA: подпись по шагам
0
1
2
3
4
5
Шаг 0: Входные данные
Для подписи нужен приватный ключ d, хеш сообщения h и параметры кривой (G, n). G -- генератор группы, n -- порядок группы.
d = приватный ключ, h = SHA-256(сообщение)
Параметрыd = 7a1c...e3f2 h = a4b9...7d21
d, h
Входы
k
Nonce
R=kG
Точка
r=R.x
r
(r, s)
Подпись

Формулы ECDSA подписи

Дано:

  • d — приватный ключ (секретное число)
  • h = SHA-256(сообщение) — хеш сообщения
  • G — генератор кривой, n — порядок группы

Алгоритм:

  1. Выбираем случайное k из [1, n-1]
  2. Вычисляем R = k * G (скалярное умножение на кривой)
  3. Берем r = R.x mod n (x-координата точки R)
  4. Вычисляем s = k^(-1) * (h + r * d) mod n
  5. Подпись: (r, s)

Верификация подписи

Любой, зная публичный ключ Q и сообщение, может проверить подпись:

ECDSA: верификация подписи
Входные данные верификации
Подпись (r, s)r = 3f7a...c892 s = 8e4b...2df9
Хеш сообщения ha4b9...7d21
Публичный ключ QQ = d * G
Вычисления
1
s_inv = s-1 mod n
Модулярный обратный к s
2a
u1 = h * s_inv mod n
u1 = f2c1...88a3
2b
u2 = r * s_inv mod n
u2 = 4d9e...b612
3
P = u1 * G + u2 * Q
Две операции скалярного умножения + сложение точек
Проверка: P.x mod n == r ?
Если равны -- подпись валидна. Если нет -- подпись отклонена.
Почему это работает: P = u1*G + u2*Q = u1*G + u2*d*G = (u1 + u2*d)*G. Подставляя u1 = h/s и u2 = r/s: P = (h + r*d)/s * G = (h + r*d) / (k-1(h+r*d)) * G = k*G = R. Значит P.x = R.x = r.

Алгоритм верификации

Дано: подпись (r, s), хеш h, публичный ключ Q = d * G

  1. Вычисляем s_inv = s^(-1) mod n
  2. Вычисляем u1 = h * s_inv mod n
  3. Вычисляем u2 = r * s_inv mod n
  4. Вычисляем точку P = u1 * G + u2 * Q
  5. Проверяем: P.x mod n == r ?

Если равны — подпись валидна!

Код на Python: пошаговая подпись ECDSA

from ecdsa import SECP256k1, SigningKey
from ecdsa.numbertheory import inverse_mod
import hashlib, os

# Параметры кривой secp256k1
curve = SECP256k1
G = curve.generator
n = curve.order

# 1. Генерация ключей
d = int.from_bytes(os.urandom(32), 'big') % (n - 1) + 1  # приватный ключ
Q = d * G  # публичный ключ

# 2. Хеш сообщения
message = b"Transfer 1 BTC to Alice"
h = int(hashlib.sha256(message).hexdigest(), 16)

# 3. Подпись (пошагово)
k = int.from_bytes(os.urandom(32), 'big') % (n - 1) + 1  # случайный nonce
R = k * G                        # точка на кривой
r = R.x() % n                    # x-координата mod n
s = (inverse_mod(k, n) * (h + r * d)) % n  # формула ECDSA

print(f"Подпись: r = {hex(r)}")
print(f"         s = {hex(s)}")

# 4. Верификация
s_inv = inverse_mod(s, n)
u1 = (h * s_inv) % n
u2 = (r * s_inv) % n
P = u1 * G + u2 * Q
assert P.x() % n == r, "Подпись невалидна!"
print("Подпись верифицирована!")

Подпись через библиотеку ecdsa

from ecdsa import SigningKey, SECP256k1

# Генерация ключей
sk = SigningKey.generate(curve=SECP256k1)
vk = sk.get_verifying_key()

# Подпись
message = b"Transfer 1 BTC to Alice"
signature = sk.sign(message)

# Верификация
assert vk.verify(signature, message)
print("Подпись верифицирована!")

КРИТИЧЕСКИ ВАЖНО: Безопасность nonce

Nonce k — самое уязвимое место ECDSA. Если атакующий узнает k или если k повторяется — приватный ключ мгновенно раскрывается.

Атака повторного использования nonce
ВНИМАНИЕ: Использование одного nonce k дважды раскрывает приватный ключ!
0
1
2
3
4
Шаг 0: Две подписи с одним k
Отправитель подписал два разных сообщения m1 и m2, но использовал одинаковый nonce k. Хеши сообщений: h1 и h2.
sig1 = (r, s1) для h1 sig2 = (r, s2) для h2
Обе подписи имеют одинаковое r, потому что r = (kG).x, а k одинаковый!
Подпись 1 (сообщение m1)
r = 3f7a...c892
s1 = 2a1f...c903
Подпись 2 (сообщение m2)
r = 3f7a...c892
s2 = 7c4e...df18
Защита: Используйте RFC 6979 (детерминированный nonce) или EdDSA (встроенный детерминированный nonce). Никогда не генерируйте k вручную.

Атака повторного использования nonce

Если две подписи (r, s1) и (r, s2) используют один и тот же k:

s1 = k^(-1) * (h1 + r*d) mod n
s2 = k^(-1) * (h2 + r*d) mod n

Вычитаем:
s1 - s2 = k^(-1) * (h1 - h2) mod n

Находим k:
k = (h1 - h2) * (s1 - s2)^(-1) mod n

Находим приватный ключ:
d = (s * k - h) * r^(-1) mod n

Это не теоретическая угроза! В 2010 году хакеры fail0verflow извлекли приватный ключ Sony из прошивки PlayStation 3. Sony использовала фиксированное значение k = 4 (константа!) для подписи всех обновлений. Одинаковое r в двух подписях позволило восстановить приватный ключ и запустить любой код на PS3.

ПРАВИЛО: Никогда не используйте фиксированный или предсказуемый nonce k. Даже “хороший” генератор случайных чисел на слабом оборудовании может привести к утечке ключей.

RFC 6979: детерминированный nonce

Решение проблемы случайного nonce — RFC 6979 (2013). Вместо генерации случайного k, вычисляем его детерминированно:

k = HMAC-SHA256(приватный_ключ, хеш_сообщения)

Преимущества:

  • Детерминированность: один и тот же ключ + сообщение всегда дают одинаковый k
  • Уникальность: разные сообщения дают разные k (свойство HMAC)
  • Не нужен генератор случайных чисел: работает на устройствах без хорошего /dev/urandom
  • Проверяемость: можно верифицировать, что подпись создана правильно
from ecdsa import SigningKey, SECP256k1

sk = SigningKey.generate(curve=SECP256k1)

# Детерминированная подпись (RFC 6979) — РЕКОМЕНДУЕМЫЙ подход
sig1 = sk.sign_deterministic(b"Transfer 1 BTC")
sig2 = sk.sign_deterministic(b"Transfer 1 BTC")

assert sig1 == sig2  # Одинаковый ввод -> одинаковая подпись
print("Детерминированные подписи совпадают!")

Математический уровень: почему верификация работает

Покажем алгебраически, что верификация корректна.

Дано: подпись (r, s), где s = k^(-1)(h + r*d) mod n

Верификация: P = u1G + u2Q, где u1 = hs^(-1), u2 = rs^(-1)

Доказательство:

P = u1*G + u2*Q
  = u1*G + u2*(d*G)         (подставляем Q = d*G)
  = (u1 + u2*d)*G           (линейность скалярного умножения)

Подставляем u1 и u2:
  = (h*s^(-1) + r*s^(-1)*d)*G
  = s^(-1)*(h + r*d)*G      (выносим s^(-1))

Подставляем s = k^(-1)*(h + r*d):
  = (k^(-1)*(h + r*d))^(-1) * (h + r*d) * G
  = k * (h + r*d)^(-1) * (h + r*d) * G
  = k * G
  = R

Значит P = R, и P.x mod n = R.x mod n = r. QED.

ECDSA в Ethereum: ecrecover

В Ethereum ECDSA имеет особенность — параметр v (recovery id):

Подпись Ethereum: (v, r, s)
v = 27 или 28 (какая из двух возможных точек R)

Функция ecrecover(hash, v, r, s) восстанавливает публичный ключ из подписи. Это используется в smart-контрактах для проверки подписей (EIP-191, EIP-712).

Практика

Откройте Jupyter notebook 07-ecdsa.ipynb для практики:

  • Ручная реализация ECDSA подписи и верификации
  • Демонстрация атаки повторного использования nonce
  • Детерминированная подпись (RFC 6979)
  • Восстановление публичного ключа из подписи

Итоги

КонцепцияФормулаЗначение
Подписьs = k^(-1)(h + rd) mod nСвязывает приватный ключ с сообщением
ВерификацияP = u1G + u2Q, P.x == r?Проверка без знания d
Noncek — случайный и уникальныйПовтор k раскрывает d!
RFC 6979k = HMAC(d, h)Детерминированный безопасный nonce

Следующий урок: EdDSA и подписи Шнорра — более современные и безопасные альтернативы ECDSA.

Finished the lesson?

Mark it as complete to track your progress