Zum Inhalt springen
ecdsa.com
Go

go-ethereum crypto

Das Paket crypto von go-ethereum (geth): Signieren mit secp256k1 über einen 32-Byte-Hash ohne Nachrichtenpräfix, 65-Byte-Signaturen mit v = 0/1 und eine Prüffunktion, die High-S ablehnt, während die Wiederherstellung es akzeptiert.
Lizenz
LGPL-3.0-or-later
Geprüfte Version
1.17.8 ·
Quellcode
github.com/ethereum/go-ethereum
Dokumentation
pkg.go.dev/github.com/ethereum/go-ethereum/crypto

Kompatibilität auf einen Blick

ECDSAEthereum und Bitcoin

ECDSA-Kurven
secp256k1
EdDSA
none
Formate öffentlicher Schlüssel
sec1-uncompressedsec1-compressed

Go-Dienste, die mit Ethereum-Signaturen arbeiten, importieren meist github.com/ethereum/go-ethereum/crypto und accounts. Mit aktiviertem cgo nutzt das Paket die Kopie von libsecp256k1, die go-ethereum mitliefert; mit CGO_ENABLED=0 weicht es auf die reine Go-Implementierung von decred aus – die Prüfung lief mit beiden Builds, und alle Befunde fielen identisch aus.

crypto.Sign(hash, key) nimmt genau 32 Byte (andernfalls kommt hash is required to be exactly 32 bytes), hasht nicht und stellt kein Präfix voran. Eine Anmeldenachricht muss zuerst accounts.TextHash durchlaufen, das das Präfix nach EIP-191 voranstellt und mit keccak-256 hasht – die Prüfung bestätigte das mit einer unabhängigen keccak-Implementierung. Signierst du stattdessen den bloßen keccak-Hash der Nachricht, entsteht eine Signatur, die nie mit dem übereinstimmt, was ethers, viem und Wallets berechnen.

Die Signatur ist 65 Byte lang und enthält die Recovery-ID als v = 0 oder 1, während Wallets, ethers, viem und web3.py dort 27 oder 28 eintragen. crypto.Ecrecover lehnte v = 27/28 mit invalid signature recovery id ab; ziehe also beim Einlesen 27 ab und addiere 27 bei der Ausgabe, wie es das Beispiel tut. Die Nonces folgen RFC 6979 – die Signatur stimmte mit einer unabhängigen Referenzimplementierung überein –, und s liegt immer in der unteren Hälfte (Low-S).

Bei High-S sind sich die beiden Wege der Signaturprüfung uneinig. crypto.VerifySignature, das 64 Byte ohne v nimmt, lehnte die gespiegelte Signatur (r, n − s) ab, doch crypto.Ecrecover und crypto.SigToPub stellten daraus den richtigen öffentlichen Schlüssel wieder her – so wie es das Precompile ecrecover der EVM tut. Contracts und Dienste, die Adressen wiederherstellen, müssen High-S daher selbst ablehnen, wenn sie eine Signatur als eindeutige Kennung behandeln.

Öffentliche Schlüssel werden als unkomprimierte Punkte mit 65 Byte oder komprimierte Punkte mit 33 Byte im SEC1-Format übertragen, private Schlüssel als 32-Byte-Skalare. Welcher Konvention eine Signatur folgt, siehst du, wenn du sie in den Ethereum-Signatur-Verifizierer einfügst; wiederholte Nonces in Transaktionen einer Adresse zeigt der Nonce-Wiederverwendungs-Checker für Ethereum.

Fakten und die Prüfungen dahinter

Nichts im Folgenden ist aus einer Dokumentation abgeschrieben. Das Prüfskript content/libraries/verify/go-ethereum-crypto/run.sh hat go-ethereum crypto 1.17.8 am 20. Oktober 2026 im Container-Image golang:1.27.3-trixie ausgeführt; die Seite muss mit seiner Ausgabe übereinstimmen, und jeder Fakt führt die zugehörigen Zeilen auf.

  • Kodierung von ECDSA-Signaturen

    rsv-65

    crypto.Sign gibt 65 Byte zurück: r, s und die Recovery-ID.

    Von der Prüfung aufgezeichnet
    fact ecdsa-signature-encoding = rsv-65
  • Recovery-ID v

    0-1

    Das letzte Byte ist 0 oder 1; crypto.Ecrecover lehnte 27/28 mit invalid signature recovery id ab.

    Von der Prüfung aufgezeichnet
    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
  • Nachrichtenpräfix

    eip-191

    Das Hashing nach EIP-191 übernimmt accounts.TextHash, dessen Ergebnis mit einer unabhängigen keccak-256-Berechnung über die Nachricht mit Präfix übereinstimmte; crypto.Sign selbst stellt kein Präfix voran.

    Von der Prüfung aufgezeichnet
    fact message-prefix = eip-191
    evidence message-prefix: accounts.TextHash equals keccak256 of the EIP-191 prefixed message; crypto.Sign itself adds no prefix
  • Was der Signaturaufruf hasht

    takes-digest

    crypto.Sign nimmt einen 32-Byte-Hash und scheitert bei jeder anderen Eingabe mit hash is required to be exactly 32 bytes.

    Von der Prüfung aufgezeichnet
    fact ecdsa-prehash = takes-digest
    evidence ecdsa-prehash: crypto.Sign on a 31-byte input: hash is required to be exactly 32 bytes (31)
  • Standard-Hash

    none

    Innerhalb von crypto.Sign wird nicht gehasht.

    Von der Prüfung aufgezeichnet
    fact ecdsa-default-hash = none
  • ECDSA-Nonce

    rfc6979

    RFC 6979: Die Signatur stimmte mit der einer unabhängigen Implementierung von RFC 6979 überein (die ihrerseits gegen den Testvektor des RFC geprüft wurde), mit normalisiertem s.

    Von der Prüfung aufgezeichnet
    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 beim Signieren

    low-s

    Kein High-S-Wert unter 64 Signaturen.

    Von der Prüfung aufgezeichnet
    fact ecdsa-low-s-sign = low-s
  • High-S bei der Prüfung

    rejects-high-s

    crypto.VerifySignature gab für die Signatur, in der s durch n − s ersetzt war, false zurück.

    Von der Prüfung aufgezeichnet
    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>)
  • Wiederherstellung aus High-S-Signaturen

    accepted

    crypto.Ecrecover stellte aus der gespiegelten High-S-Signatur mit umgeschaltetem v den öffentlichen Schlüssel des Signierers wieder her.

    Von der Prüfung aufgezeichnet
    fact ecrecover-high-s = accepted
  • ECDSA-Kurven

    secp256k1

    Nur secp256k1.

    Von der Prüfung aufgezeichnet
    fact ecdsa-curves = secp256k1
  • Formate öffentlicher Schlüssel

    sec1-uncompressedsec1-compressed

    crypto.UnmarshalPubkey liest den unkomprimierten Punkt mit 65 Byte, crypto.DecompressPubkey den komprimierten mit 33 Byte.

    Von der Prüfung aufgezeichnet
    fact public-key-formats = sec1-uncompressed, sec1-compressed
    evidence public-key-formats: UnmarshalPubkey(65 bytes): <nil>; DecompressPubkey(33 bytes): <nil>
  • Formate privater Schlüssel

    raw-scalar

    Private Schlüssel sind 32-Byte-Skalare (crypto.ToECDSA, crypto.HexToECDSA).

    Von der Prüfung aufgezeichnet
    fact private-key-formats = raw-scalar
  • EdDSA

    none

    Kein EdDSA in diesem Paket.

    Von der Prüfung aufgezeichnet
    fact eddsa = none
  • Post-Quanten-Algorithmen

    none

    Keine Post-Quanten-Algorithmen.

    Von der Prüfung aufgezeichnet
    fact pqc = none

Verwendung

Signiere eine Nachricht nach EIP-191, rechne v zwischen den Werten 0/1 von go-ethereum und 27/28 im Übertragungsformat um und stelle den Signierer wieder her.
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))
}

Die Snippet-Testumgebung führt jedes Beispiel unverändert im Container-Image golang:1.27.3-trixie aus, und der Lauf schlägt fehl, wenn die Ausgabe nicht übereinstimmt.

Befehl
GOFLAGS=-mod=mod go run .
Ausgabe muss passen (regulärer Ausdruck)
v on the wire: 2[78] signer matches: true

Bibliotheken