Перейти к содержимому
ecdsa.com
Go

go-ethereum crypto

Пакет crypto из go-ethereum (geth): подпись secp256k1 по 32-байтовому хешу без префикса сообщения, 65-байтовые подписи с v = 0/1 и функция проверки, которая отклоняет high-S, хотя при восстановлении ключа такие подписи принимаются.
Лицензия
LGPL-3.0-or-later
Проверенная версия
1.17.8 ·
Исходный код
github.com/ethereum/go-ethereum
Документация
pkg.go.dev/github.com/ethereum/go-ethereum/crypto

Совместимость вкратце

ECDSAEthereum и Bitcoin

EdDSA
none
Форматы открытых ключей
sec1-uncompressedsec1-compressed

Сервисы на Go, которые работают с подписями Ethereum, обычно импортируют github.com/ethereum/go-ethereum/crypto и accounts. При включённом cgo пакет использует копию libsecp256k1, которая поставляется в составе go-ethereum, а при CGO_ENABLED=0 переключается на реализацию decred на чистом Go. Проверка прогнала обе сборки, и все факты совпали.

crypto.Sign(hash, key) принимает ровно 32 байта (иначе возвращает ошибку hash is required to be exactly 32 bytes), ничего не хеширует и не добавляет префикс. Сообщение для входа сначала нужно пропустить через accounts.TextHash: эта функция добавляет префикс EIP-191 и хеширует результат keccak-256, что проверка подтвердила сравнением с независимой реализацией keccak. Если вместо этого подписать голый keccak сообщения, получится подпись, которая никогда не совпадёт с подписями ethers, viem и кошельков.

Подпись занимает 65 байт, а идентификатор восстановления записан в v как 0 или 1, тогда как кошельки, ethers, viem и web3.py ставят туда 27 или 28. crypto.Ecrecover отклонил v = 27/28 с ошибкой invalid signature recovery id, поэтому на входе вычитайте 27, а на выходе прибавляйте его обратно, как это делает пример. Nonce вычисляются по RFC 6979 (подпись совпала с независимой эталонной реализацией), а s всегда находится в нижней половине диапазона.

Два пути проверки расходятся в отношении high-S. crypto.VerifySignature, который принимает 64 байта без v, отклонил зеркальную подпись (r, n − s), а crypto.Ecrecover и crypto.SigToPub восстановили по ней правильный открытый ключ — так же, как прекомпилированный контракт ecrecover в EVM. Поэтому контракты и сервисы, которые восстанавливают адреса, должны сами отклонять high-S, если используют подпись как уникальный идентификатор.

Открытые ключи передаются как несжатые точки SEC1 длиной 65 байт или сжатые точки длиной 33 байта, а закрытые ключи — как 32-байтовые скаляры. Чтобы понять, какому соглашению следует подпись, вставьте её в проверку подписей Ethereum; повторы nonce в транзакциях одного адреса выявит проверка повторного использования nonce в Ethereum.

Факты и проверки, на которых они основаны

Ничего из приведённого ниже не скопировано из документации. 20 октября 2026 г. проверка content/libraries/verify/go-ethereum-crypto/run.sh запустила go-ethereum crypto 1.17.8 в образе контейнера golang:1.27.3-trixie; содержимое страницы должно совпадать с выводом проверки, а у каждого факта приведены соответствующие строки вывода.

  • Кодировка подписи ECDSA

    rsv-65

    crypto.Sign возвращает 65 байт: r, s и идентификатор восстановления.

    Что записала проверка
    fact ecdsa-signature-encoding = rsv-65
  • Идентификатор восстановления v

    0-1

    Последний байт равен 0 или 1; crypto.Ecrecover отклонил 27/28 с ошибкой invalid signature recovery id.

    Что записала проверка
    fact recovery-v = 0-1
    evidence recovery-v: Ecrecover with v = 27/28: invalid signature recovery id
    evidence recovery-v: signer address recovered from the 0/1 signature matches: true
  • Префикс сообщения

    eip-191

    Хеширование по EIP-191 выполняет accounts.TextHash, и результат совпал с независимо вычисленным keccak-256 от сообщения с префиксом; сам crypto.Sign префикс не добавляет.

    Что записала проверка
    fact message-prefix = eip-191
    evidence message-prefix: accounts.TextHash equals keccak256 of the EIP-191 prefixed message; crypto.Sign itself adds no prefix
  • Что хеширует вызов подписи

    takes-digest

    crypto.Sign принимает 32-байтовый хеш, а на любые другие данные возвращает ошибку hash is required to be exactly 32 bytes.

    Что записала проверка
    fact ecdsa-prehash = takes-digest
    evidence ecdsa-prehash: crypto.Sign on a 31-byte input: hash is required to be exactly 32 bytes (31)
  • Хеш-функция по умолчанию

    none

    crypto.Sign сам ничего не хеширует.

    Что записала проверка
    fact ecdsa-default-hash = none
  • Nonce в ECDSA

    rfc6979

    RFC 6979: подпись совпала с результатом независимой реализации RFC 6979 (сверенной с тестовым вектором из RFC) после нормализации s.

    Что записала проверка
    fact ecdsa-nonce = rfc6979
    evidence ecdsa-nonce: two signatures of the same digest identical: true; equal to the reference RFC 6979 signature with s normalized: true
  • Low-S при подписи

    low-s

    Среди 64 подписей нет ни одного значения high-S.

    Что записала проверка
    fact ecdsa-low-s-sign = low-s
  • High-S при проверке

    rejects-high-s

    crypto.VerifySignature вернул false для подписи, в которой s заменили на n − s.

    Что записала проверка
    fact ecdsa-low-s-verify = rejects-high-s
    evidence ecdsa-low-s-verify: VerifySignature on (r, n - s): false; Ecrecover on (r, n - s, flipped v): recovers the signer true (err <nil>)
  • Восстановление ключа из подписей с high-S

    accepted

    crypto.Ecrecover восстановил открытый ключ подписанта по зеркальной подписи с high-S и инвертированным v.

    Что записала проверка
    fact ecrecover-high-s = accepted
  • Кривые ECDSA

    secp256k1

    Только secp256k1.

    Что записала проверка
    fact ecdsa-curves = secp256k1
  • Форматы открытых ключей

    sec1-uncompressedsec1-compressed

    crypto.UnmarshalPubkey читает 65-байтовую несжатую точку, а crypto.DecompressPubkey — 33-байтовую сжатую.

    Что записала проверка
    fact public-key-formats = sec1-uncompressed, sec1-compressed
    evidence public-key-formats: UnmarshalPubkey(65 bytes): <nil>; DecompressPubkey(33 bytes): <nil>
  • Форматы закрытых ключей

    raw-scalar

    Закрытые ключи — 32-байтовые скаляры (crypto.ToECDSA, crypto.HexToECDSA).

    Что записала проверка
    fact private-key-formats = raw-scalar
  • EdDSA

    none

    В этом пакете нет EdDSA.

    Что записала проверка
    fact eddsa = none
  • Постквантовые алгоритмы

    none

    Постквантовых алгоритмов нет.

    Что записала проверка
    fact pqc = none

Использование

Подпишите сообщение по EIP-191, преобразуйте v между значениями 0/1 из go-ethereum и 27/28 в передаваемых подписях и восстановите подписанта.
package main

import (
	"fmt"

	"github.com/ethereum/go-ethereum/accounts"
	"github.com/ethereum/go-ethereum/crypto"
)

func main() {
	key, _ := crypto.GenerateKey()
	message := []byte("Sign in to example.com\nNonce: 8f2c1a")

	// crypto.Sign takes a 32-byte hash and adds no prefix: hash with TextHash (EIP-191).
	sig, _ := crypto.Sign(accounts.TextHash(message), key)

	// Wallets and ethers/viem send v = 27/28; go-ethereum wants 0/1.
	wire := append([]byte{}, sig...)
	wire[64] += 27

	// Coming back from the wire: undo the offset before recovering.
	in := append([]byte{}, wire...)
	if in[64] >= 27 {
		in[64] -= 27
	}
	pub, err := crypto.SigToPub(accounts.TextHash(message), in)
	fmt.Println("v on the wire:", wire[64], "signer matches:", err == nil && crypto.PubkeyToAddress(*pub) == crypto.PubkeyToAddress(key.PublicKey))
}

Тестовый стенд для фрагментов кода запускает каждый пример без изменений в образе контейнера golang:1.27.3-trixie, и запуск завершается ошибкой, если вывод не совпадает.

Команда
GOFLAGS=-mod=mod go run .
Вывод должен совпадать (регулярное выражение)
v on the wire: 2[78] signer matches: true

Библиотеки