ecdsa review
Our Python 3.12 sandbox loaded ecdsa 0.19.2 in 0.11 seconds, but pip-audit still found one known vulnerability. This is a pure-Python toolkit for ECDSA signatures, Ed25519 and Ed448 signatures, ECDH, curve arithmetic, and PEM, DER, raw-point, or OpenSSH serialization. Its readable internals suit protocol tests and teaching. Version 0.19.2 fixes CVE-2026-33936, a truncated-DER bug that could make high-level loaders throw unexpected exceptions. Upstream explicitly warns that private operations have no side-channel defense, which rules out production secrets.
Our ecdsa 0.19.2 install took 0.3 seconds and 1 MB, yet pip-audit found 1 known vulnerability and upstream rejects production private-key use. Keep it for curve experiments and interoperability fixtures where readable pure Python is the reason for choosing it.
We installed it
| Install | ✓ · 0.3s | 2 packages on disk · 1 MB |
| Import | ✓ | import ecdsa in 0.11s · pure Python · requires Python >=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.* |
| Known vulns | 1 | (pip-audit) |
Answers from our run
Does ecdsa install cleanly?
Yes. In a fresh container with an empty cache, pip install ecdsa finished in 0.3s, leaving 2 packages and 1 MB on disk. pip-audit reported 1 known vulnerability.
What does ecdsa need to run?
Python >=2.6, !=3.0., !=3.1., !=3.2., !=3.3., !=3.4., !=3.5., and nothing compiled: it is pure Python. In our run import ecdsa succeeded in 0.11s.
ecdsa or cryptography: which should you use?
cryptography: Use it for production ECDSA, certificate work, and OpenSSL-backed key handling. Our ecdsa 0.19.2 install took 0.3 seconds and 1 MB, yet pip-audit found 1 known vulnerability and upstream rejects production private-key use.
When should you not use ecdsa?
The private key protects accounts, funds, identities, or confidential data; upstream says one observed private operation may expose the key through side channels
Use it if
- You need to step through elliptic-curve arithmetic in Python for teaching, debugging, or a reproducible experiment
- Your interoperability tests must produce uncommon curves, point encodings, or deliberately malformed DER that a high-level crypto API hides
- A compiler-free test environment needs ECDSA, EdDSA, or ECDH and the material has no production value
- You need NIST, SEC, Brainpool, secp256k1, Ed25519, and Ed448 operations behind one Python package
- The private key protects accounts, funds, identities, or confidential data; upstream says one observed private operation may expose the key through side channels
- You need safe choices without reviewing every call; SigningKey.generate() starts with NIST192p and sign() uses SHA-1 unless you pass different parameters
- Your input includes attacker-controlled PEM or DER on a version below 0.19.2; the current release fixes truncated buffers reaching high-level loaders as unexpected exceptions
- The job includes certificates, TLS, JWT, or JOSE policy; this package supplies curve primitives and encodings rather than those application protocols
- You require typed-package metadata or constant-time native arithmetic; our install had no py.typed marker, and optional gmpy2 changes speed rather than the security model
Setup reality
We installed ecdsa 0.19.2 on Python 3.12 in 0.3 seconds. The sandbox ended with 2 packages taking 1 MB, and import ecdsa worked in 0.11 seconds. Its metadata lists 3 direct dependency entries, including optional arithmetic extras. The code is pure Python, carries an MIT license, and has no py.typed marker. pip-audit reported 1 known vulnerability, so the successful install is not a clean security bill.
There is no account or configuration file. Curve, hash, and representation live in your code. SigningKey.generate() defaults to NIST192p, and sign() defaults to SHA-1. Raw private or public bytes omit the curve identifier, while PEM and DER retain it. External tools commonly expect DER signatures; ecdsa's default signature encoding is the fixed-width r value followed by s, so OpenSSL interop needs sigencode_der and sigdecode_der.
Version 0.19.2 detects truncated data inside three DER helpers used by loaders such as SigningKey.from_der(). Treat 0.19.1 and earlier as unsuitable for supplied key material. Verification has another control-flow surprise: verify() returns True when the signature matches and raises BadSignatureError when it does not. Parsing failures use exceptions such as UnexpectedDER or MalformedPointError, so distinguish bad signatures from bad encodings at the input boundary.
gmpy or gmpy2 is discovered when the module starts and can accelerate the arithmetic. Either extra adds a compiled component to an otherwise pure-Python install. Neither changes the upstream warning about timing, shared hosts, power observation, or RF leakage during private operations. For secrets exposed to an attacker, move the operation to cryptography or another maintained native implementation instead of treating the optional backend as a hardening switch.
Patterns
Generate a P-256 key with explicit defaults generate-p256-key
import hashlib
from ecdsa import NIST256p, SigningKey
signing_key = SigningKey.generate(
curve=NIST256p,
hashfunc=hashlib.sha256,
)
verifying_key = signing_key.verifying_keyThe no-argument choices are NIST192p and SHA-1. Set the curve and hash beside the key creation.
Create a deterministic ECDSA signature sign-rfc6979
import hashlib
signature = signing_key.sign_deterministic(
b"invoice:481",
hashfunc=hashlib.sha256,
)sign_deterministic() derives the ECDSA nonce through RFC 6979. Plain sign() obtains a fresh random nonce.
Turn signature failure into a boolean verify-and-reject
from ecdsa import BadSignatureError
try:
verifying_key.verify(signature, b"invoice:481")
except BadSignatureError:
accepted = False
else:
accepted = TrueA mismatch raises BadSignatureError. verify() returns True only for a matching signature.
Write private and public PEM files save-pem-keys
from pathlib import Path
Path("private.pem").write_bytes(signing_key.to_pem())
Path("public.pem").write_bytes(verifying_key.to_pem())to_pem() produces bytes. PEM records the curve identifier for the matching loader.
Load a key pair from PEM load-pem-keys
from pathlib import Path
from ecdsa import SigningKey, VerifyingKey
signing_key = SigningKey.from_pem(Path("private.pem").read_bytes())
verifying_key = VerifyingKey.from_pem(Path("public.pem").read_bytes())Malformed PEM can raise UnexpectedDER or MalformedPointError. Handle parsing separately from signature rejection.
Use DER signatures with external tools exchange-der-signatures
import hashlib
from ecdsa.util import sigdecode_der, sigencode_der
der_signature = signing_key.sign_deterministic(
payload,
hashfunc=hashlib.sha256,
sigencode=sigencode_der,
)
verifying_key.verify(
der_signature,
payload,
hashfunc=hashlib.sha256,
sigdecode=sigdecode_der,
)The built-in encoding concatenates fixed-width r and s values. OpenSSL-style consumers generally expect DER.
Round-trip raw private bytes reload-raw-private-key
from ecdsa import NIST256p, SigningKey
raw = signing_key.to_string()
restored = SigningKey.from_string(
raw,
curve=NIST256p,
hashfunc=hashlib.sha256,
)Raw key bytes contain no curve name. Loading them with the wrong curve fails or describes different key material.
Serialize a compressed public point compress-public-point
from ecdsa import NIST256p, VerifyingKey
compressed = verifying_key.to_string(encoding="compressed")
restored = VerifyingKey.from_string(
compressed,
curve=NIST256p,
)from_string() recognizes compressed point syntax, while the caller still supplies the curve.
Compute an ECDH shared value derive-ecdh-value
from ecdsa import ECDH, NIST256p
exchange = ECDH(curve=NIST256p)
exchange.generate_private_key()
public_pem = exchange.get_public_key().to_pem()
exchange.load_received_public_key_pem(peer_public_pem)
shared = exchange.generate_sharedsecret_bytes()generate_sharedsecret_bytes() returns raw ECDH output. Derive an encryption key from it with the protocol's KDF.
Sign bytes with Ed25519 sign-ed25519-message
from ecdsa import Ed25519, SigningKey
signing_key = SigningKey.generate(curve=Ed25519)
verifying_key = signing_key.verifying_key
signature = signing_key.sign(b"release-manifest")
assert verifying_key.verify(signature, b"release-manifest")Ed25519 performs its own hashing and produces deterministic signatures. hashfunc and sign_deterministic() do not apply.
Export an Ed25519 SSH public key export-ed25519-ssh
from ecdsa import Ed25519, SigningKey
public_key = SigningKey.generate(curve=Ed25519).verifying_key
print(public_key.to_ssh().decode("ascii"))to_ssh() is limited to Ed25519. NIST, SEC, and Brainpool keys use the other serializers.
Prepare one verifier for repeated use precompute-batch-verification
verifying_key.precompute()
for message, signature in signed_rows:
verifying_key.verify(signature, message)precompute() spends memory and startup work on one public key. Reserve it for repeated verification with that same key.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| cryptography | PyPI | Use it for production ECDSA, certificate work, and OpenSSL-backed key handling. |
| PyNaCl | PyPI | Use it when libsodium's Ed25519 signatures and Curve25519 boxes cover the protocol. |
| coincurve | PyPI | Use it for secp256k1 software that specifically wants bindings to libsecp256k1. |
More security guides
cryptography · pyjwt · jose · requests-oauthlib · oauthlib · dompurify · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

