Aller au contenu
ecdsa.com
Python

web3.py (eth-account)

web3.py, de l’Ethereum Foundation, dont la signature est assurée par eth-account : signatures r‖s‖v de 65 octets avec v = 27/28, EIP-191 et RFC 6979, et, comme viem mais contrairement à ethers, récupération du signataire à partir de signatures high-S.
Licence
MIT
Version vérifiée
8.0.0 ·
Code source
github.com/ethereum/web3.py
Documentation
web3py.readthedocs.io/en/stable/

Compatibilité en un coup d’œil

ECDSAEthereum et Bitcoin

Courbes ECDSA
secp256k1
EdDSA
none

web3.py est le client Python des nœuds Ethereum ; la signature des messages et des transactions est déléguée au paquet eth-account (0.14.0 avec web3 8.0.0), qui s’appuie à son tour sur eth-keys pour l’arithmétique de courbe. La plupart du code de signature importe directement eth_account, comme le fait l’exemple.

Account.sign_message(encode_defunct(text=...)) implémente personal_sign : le préfixe EIP-191 avec la longueur en octets écrite en décimal, keccak-256, un nonce RFC 6979 (comparé à un signataire de référence indépendant) et s dans la moitié inférieure, le tout renvoyé sous forme de 65 octets terminés par v = 27 ou 28. encode_defunct est l’élément que l’on oublie : signer des octets bruts avec unsafe_sign_hash omet le préfixe, et c’est précisément ce qui rend une signature inutilisable ailleurs comme preuve de connexion.

Account.recover_message() a récupéré le signataire à partir de la signature miroir high-S comme à partir de v = 0/1 : cette méthode accepte donc plus de formes qu’ethers. Normalisez s vous-même si les signatures sont stockées ou dédoublonnées. La forme compacte EIP-2098 de 64 octets a été rejetée avec Unexpected recoverable signature length: Expected 65, but got 64 bytes ; étendez-la à 65 octets avant la récupération.

Pour les données typées, utilisez encode_typed_data (EIP-712) plutôt que de hacher le JSON vous-même, et vérifiez ce qu’un portefeuille est invité à signer avec l’inspecteur EIP-712. Les portefeuilles de contrat intelligent exigent un appel ERC-1271 au contrat au lieu de recover_message ; les messages de connexion se déboguent le plus facilement dans le vérificateur SIWE.

L’exemple signe un message de connexion et compare l’adresse récupérée : c’est cette comparaison qui devrait remplacer toute vérification octet par octet des signatures.

Les faits et les vérifications qui les fondent

Rien de ce qui suit n’est copié d’une documentation. La vérification content/libraries/verify/web3py/run.sh a exécuté web3.py (eth-account) 8.0.0 dans l’image de conteneur python:3.14.8-slim-trixie le 20 octobre 2026 ; la page doit correspondre à ce que la vérification a affiché, et chaque fait cite les lignes en question.

  • Encodage des signatures ECDSA

    rsv-65

    sign_message() renvoie 65 octets : r, s et v.

    Relevé par la vérification
    fact ecdsa-signature-encoding = rsv-65
  • Identifiant de récupération v

    27-28

    v vaut 27 ou 28.

    Relevé par la vérification
    fact recovery-v = 27-28
  • Préfixe de message

    eip-191

    Le condensat est égal au keccak-256 de \x19Ethereum Signed Message:\n, de la longueur en décimal et du message, calculé de façon indépendante.

    Relevé par la vérification
    fact message-prefix = eip-191
  • Fonction de hachage par défaut

    keccak256

    Les signatures de messages utilisent keccak-256.

    Relevé par la vérification
    fact ecdsa-default-hash = keccak256
  • Ce que l’appel de signature hache

    hashes-message-or-digest

    sign_message() hache avec le préfixe EIP-191 ; unsafe_sign_hash() signe tel quel un condensat de 32 octets et a produit une signature identique pour ce condensat.

    Relevé par la vérification
    fact ecdsa-prehash = hashes-message-or-digest
    evidence ecdsa-prehash: unsafe_sign_hash over the EIP-191 digest equals sign_message: True
  • Nonce ECDSA

    rfc6979

    RFC 6979 : la signature correspondait à celle d’une implémentation de référence indépendante, avec s normalisé.

    Relevé par la vérification
    fact ecdsa-nonce = rfc6979
  • Low-S à la signature

    low-s

    Aucune valeur high-S parmi 64 signatures de messages.

    Relevé par la vérification
    fact ecdsa-low-s-sign = low-s
  • High-S à la vérification

    accepts-high-s

    recover_message() a récupéré le signataire à partir de (r, n − s) avec v inversé.

    Relevé par la vérification
    fact ecdsa-low-s-verify = accepts-high-s
    evidence ecdsa-low-s-verify: recover_message on (r, n - s, flipped v): the signer
  • Formes de signature acceptées

    v-27-28v-0-1

    Les signatures de 65 octets avec v = 27/28 ou v = 0/1 ont permis de récupérer le signataire ; la forme EIP-2098 de 64 octets a levé ValidationError: Unexpected recoverable signature length: Expected 65, but got 64 bytes.

    Relevé par la vérification
    fact signature-input-shapes = v-27-28, v-0-1
    evidence signature-input-shapes: v-27-28: recovers the signer
    evidence signature-input-shapes: v-0-1: recovers the signer
    evidence signature-input-shapes: eip-2098: ValidationError: Unexpected recoverable signature length: Expected 65, but got 64 bytes
  • Courbes ECDSA

    secp256k1

    secp256k1 uniquement.

    Relevé par la vérification
    fact ecdsa-curves = secp256k1
  • EdDSA

    none

    Pas d’EdDSA.

    Relevé par la vérification
    fact eddsa = none
  • Algorithmes post-quantiques

    none

    Aucun algorithme post-quantique.

    Relevé par la vérification
    fact pqc = none

Utilisation

Signer un message de connexion selon la sémantique de personal_sign et récupérer l’adresse du signataire.
from eth_account import Account
from eth_account.messages import encode_defunct

account = Account.create()
message = encode_defunct(text="Sign in to example.com\nNonce: 8f2c1a")

# personal_sign: EIP-191 prefix + keccak-256, RFC 6979, low-S, v = 27 or 28.
signed = account.sign_message(message)

# Recover and compare addresses; checksummed strings compare exactly.
signer = Account.recover_message(message, signature=signed.signature)
print("signature bytes:", len(signed.signature), "v:", signed.v)
print("signer matches:", signer == account.address)

Le banc de test des extraits de code exécute chaque exemple tel quel dans l’image de conteneur python:3.14.8-slim-trixie, et l’exécution échoue si la sortie ne correspond pas.

Mise en place
pip install --quiet --root-user-action=ignore web3==8.0.0 eth-account==0.14.0
Commande
python usage.py
La sortie doit correspondre à cette expression régulière
signature bytes: 65 v: 2[78]\nsigner matches: True

Bibliothèques