Skip to content
ecdsa.com
Python

python-ecdsa

tlsfuzzer's pure-Python ECDSA and EdDSA library: raw r‖s signatures over SHA-1 unless told otherwise, random nonces by default with RFC 6979 one method away, and public keys as bare x‖y.
License
MIT
Checked version
0.19.2 ·
Source
github.com/tlsfuzzer/python-ecdsa
Documentation
ecdsa.readthedocs.io/en/latest/

Compatibility at a glance

ECDSAEdDSA

ECDSA curves
P-224P-256P-384P-521secp256k1brainpoolP256r1brainpoolP384r1brainpoolP512r1
EdDSA
Ed25519Ed448
Public key formats
spki-pemspki-dersec1-uncompressedsec1-compressedraw-xy
Private key formats
pkcs8-pemsec1-pemsec1-derraw-scalar

ecdsa implements ECDSA and EdDSA in pure Python, with no OpenSSL underneath. That makes it easy to install anywhere and handy for test vectors and teaching, and it is still a dependency of older JOSE stacks such as python-jose. Its maintainers state that it is not designed to resist side-channel attacks, so production signing belongs in pyca/cryptography or coincurve.

Three defaults cause almost every interop failure. The hash defaults to SHA-1: a SigningKey created without hashfunc signs SHA-1 digests, which the check confirmed by verifying the default signature with pyca/cryptography — it passed with SHA-1 and failed with SHA-256. The signature is raw r‖s (64 bytes on P-256), not DER. And VerifyingKey.to_string() returns the bare 64-byte x‖y with no 0x04 prefix, which OpenSSL, Go and WebCrypto do not read as a point.

Signing uses a random nonce unless you call sign_deterministic(), which reproduced the RFC 6979 P-256 test vector. s is left as computed — high about half the time — unless you pick one of the *_canonize encoders, and verification accepts either half. Every encoding choice is made per call through sigencode= and sigdecode=, so the signing and verifying sides must name the same pair; a DER signature passed to the default raw decoder raises BadSignatureError: Malformed formatting of signature.

Curve support is the widest of the Python libraries — P-224 to P-521, secp256k1 and Brainpool — plus Ed25519 and Ed448. Keys come in as SPKI PEM or DER, SEC1 points and raw scalars; to_pem() writes SEC1 EC PRIVATE KEY unless you pass format="pkcs8".

The sample fixes all three defaults at once: SHA-256, a deterministic nonce, and a canonical low-S DER signature. To check a signature it produced against another library, the DER ⇄ raw converter and test vectors are the quickest route.

Facts and the checks behind them

Nothing below is copied from documentation. The check content/libraries/verify/python-ecdsa/run.sh ran python-ecdsa 0.19.2 in the container image python:3.14.8-slim-trixie on October 20, 2026; the page must match what it printed, and each fact lists those lines.

  • ECDSA signature encoding

    raw

    sign() returns the 64-byte raw r‖s on P-256; sigencode_der produces DER, and verification then needs sigdecode_der.

    Recorded by the check
    fact ecdsa-signature-encoding = raw
    evidence ecdsa-signature-encoding: sigencode_der output parses as DER: True; verifies with sigdecode_der: verified
  • Default hash

    sha1

    Without hashfunc, keys sign SHA-1 digests: the default signature verified in pyca/cryptography with SHA-1 and not with SHA-256.

    Recorded by the check
    fact ecdsa-default-hash = sha1
    evidence ecdsa-default-hash: checked by verifying the default signature in pyca/cryptography with SHA-1 and with SHA-256
  • Low-S when signing

    either

    Plain encoders keep s as computed (high-S appeared among 64 signatures); sigencode_string_canonize and sigencode_der_canonize normalize it.

    Recorded by the check
    fact ecdsa-low-s-sign = either
    evidence ecdsa-low-s-sign: high-S among 64 plain signatures: True; among 64 with sigencode_string_canonize: False
    evidence ecdsa-low-s-sign: sigencode_strings_canonize exists: True
  • High-S when verifying

    accepts-high-s

    A signature with s replaced by n − s verified.

    Recorded by the check
    fact ecdsa-low-s-verify = accepts-high-s
  • ECDSA nonce

    rfc6979-opt-in

    sign() uses a random nonce; sign_deterministic() reproduced RFC 6979 appendix A.2.5.

    Recorded by the check
    fact ecdsa-nonce = rfc6979-opt-in
    evidence ecdsa-nonce: sign() repeats: False; sign_deterministic() reproduces RFC 6979 A.2.5: True
  • What the signing call hashes

    hashes-message-or-digest

    sign() hashes the message; sign_digest() signs a digest you computed, and that signature verified against the message.

    Recorded by the check
    fact ecdsa-prehash = hashes-message-or-digest
  • Malformed ECDSA signature

    throws

    Wrong-length input raises BadSignatureError: Malformed formatting of signature; a valid encoding over the wrong message raises BadSignatureError: Signature verification failed.

    Recorded by the check
    fact ecdsa-malformed-signature = throws
    evidence ecdsa-malformed-signature: DER where raw is expected: BadSignatureError: Malformed formatting of signature
    evidence ecdsa-malformed-signature: 3 bytes: BadSignatureError: Malformed formatting of signature
    evidence ecdsa-malformed-signature: wrong message: BadSignatureError: Signature verification failed
  • ECDSA curves

    P-224P-256P-384P-521secp256k1brainpoolP256r1brainpoolP384r1brainpoolP512r1

    P-224, P-256, P-384, P-521, secp256k1 and the Brainpool r1 curves.

    Recorded by the check
    fact ecdsa-curves = P-224, P-256, P-384, P-521, secp256k1, brainpoolP256r1, brainpoolP384r1, brainpoolP512r1
  • EdDSA

    Ed25519Ed448

    Ed25519 and Ed448.

    Recorded by the check
    fact eddsa = Ed25519, Ed448
  • Post-quantum algorithms

    none

    No post-quantum algorithms.

    Recorded by the check
    fact pqc = none
  • Default public key format

    raw-xy

    VerifyingKey.to_string() returns 64 bytes of x‖y without the 0x04 prefix; pass "uncompressed" or "compressed" for SEC1 encodings.

    Recorded by the check
    fact default-public-key = raw-xy
  • Public key formats

    spki-pemspki-dersec1-uncompressedsec1-compressedraw-xy

    Public keys loaded from SPKI PEM and DER, SEC1 points and the bare x‖y.

    Recorded by the check
    fact public-key-formats = spki-pem, spki-der, sec1-uncompressed, sec1-compressed, raw-xy
  • Private key formats

    pkcs8-pemsec1-pemsec1-derraw-scalar

    Private keys loaded from PKCS#8 PEM, SEC1 PEM and DER, and raw scalars; to_pem() writes -----BEGIN EC PRIVATE KEY----- by default.

    Recorded by the check
    fact private-key-formats = pkcs8-pem, sec1-pem, sec1-der, raw-scalar
    evidence private-key-formats: to_pem() without format writes -----BEGIN EC PRIVATE KEY-----

Usage

Sign with SHA-256, an RFC 6979 nonce and a canonical low-S DER encoding, then verify.
import hashlib

from ecdsa import NIST256p, SigningKey
from ecdsa.util import sigdecode_der, sigencode_der_canonize

key = SigningKey.generate(curve=NIST256p, hashfunc=hashlib.sha256)  # the default would be SHA-1
public = key.get_verifying_key()
message = b"order #1042 approved"

# RFC 6979 nonce, DER encoding, s normalized to the low half: what OpenSSL-style
# verifiers read and what strict low-S verifiers accept.
signature = key.sign_deterministic(message, sigencode=sigencode_der_canonize)
again = key.sign_deterministic(message, sigencode=sigencode_der_canonize)

print("deterministic:", signature == again)
print("verified:", public.verify(signature, message, sigdecode=sigdecode_der))
print(public.to_pem().decode().splitlines()[0])

The snippet harness runs every sample verbatim in the container image python:3.14.8-slim-trixie, and the run fails unless the output matches.

Setup
pip install --quiet --root-user-action=ignore ecdsa==0.19.2
Command
python usage.py
Output must match (regular expression)
deterministic: True\nverified: True\n-----BEGIN PUBLIC KEY-----

Libraries