本文へスキップ
ecdsa.com
Go

go-ethereum crypto

go-ethereum(geth)の crypto パッケージです。secp256k1 でメッセージのプレフィックスを付けずに 32 バイトのハッシュに署名し、署名は v = 0/1 の 65 バイトです。検証関数は 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

Ethereum の署名を扱う Go のサービスは、通常 github.com/ethereum/go-ethereum/crypto と accounts をインポートします。cgo を有効にすると、このパッケージは go-ethereum に同梱された libsecp256k1 のコピーを使い、CGO_ENABLED=0 では純粋な Go で書かれた decred の実装にフォールバックします。チェックでは両方のビルドを実行し、すべての事実が同じ結果になりました。

crypto.Sign(hash, key) はちょうど 32 バイトを受け取り(それ以外では hash is required to be exactly 32 bytes になります)、ハッシュの計算もプレフィックスの付加も行いません。ログインメッセージは、先に accounts.TextHash に通す必要があります。この関数は EIP-191 のプレフィックスを付けて keccak-256 でハッシュします。チェックでは、独立した keccak 実装と照合してこれを確認しました。代わりにメッセージの keccak にそのまま署名すると、ethers、viem、ウォレットの結果とは決して一致しない署名になります。

署名は 65 バイトで、リカバリ ID は v = 0 または 1 です。一方、ウォレット、ethers、viem、web3.py はここに 27 または 28 を入れます。crypto.Ecrecover は v = 27/28 を invalid signature recovery id で拒否したため、サンプルのように、受け取るときに 27 を引き、送り出すときに 27 を足してください。nonce は RFC 6979 に従い(署名は独立したリファレンス実装と一致しました)、s は常に low-S です。

2 つの検証経路は、high-S の扱いが異なります。v を含まない 64 バイトを受け取る crypto.VerifySignature は鏡像の (r, n − s) 署名を拒否しましたが、crypto.Ecrecover と crypto.SigToPub は、EVM の ecrecover プリコンパイルと同様に、その署名から正しい公開鍵を復元しました。そのため、アドレスを復元するコントラクトやサービスで署名を一意の識別子として扱う場合は、high-S を自ら拒否しなければなりません。

公開鍵は 65 バイトの非圧縮形式または 33 バイトの圧縮形式の SEC1 の点として、秘密鍵は 32 バイトのスカラーとしてやり取りされます。署名がどの規約に従っているかは、Ethereum 署名検証ツールに貼り付けると確認できます。1 つのアドレスのトランザクション間で nonce が繰り返されていれば、Ethereum nonce 再利用チェッカーで見つかります。

事実とその根拠となるチェック

以下の内容はドキュメントからの転記ではありません。2026年10月20日に、チェック content/libraries/verify/go-ethereum-crypto/run.sh がコンテナイメージ golang:1.27.3-trixie 内で go-ethereum crypto 1.17.8 を実行しました。このページの内容はその出力と一致していなければならず、各事実には該当する出力行を示しています。

  • ECDSA 署名のエンコーディング

    rsv-65

    crypto.Sign は r、s、リカバリ ID からなる 65 バイトを返します。

    チェックによる記録
    fact ecdsa-signature-encoding = rsv-65
  • リカバリ ID 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
  • ECDSA の nonce

    rfc6979

    RFC 6979 です。署名は、s を正規化した状態で、独立した RFC 6979 実装(RFC のテストベクトルで確認済み)の署名と一致しました。

    チェックによる記録
    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 は、s を n − s に置き換えた署名に対して false を返しました。

    チェックによる記録
    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 は、v を反転させた鏡像の high-S 署名から署名者の公開鍵を復元しました。

    チェックによる記録
    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 を go-ethereum の 0/1 と通信上の 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

ライブラリ