ecdsa
ecdsa is elliptic curve cryptography written entirely in Python, with no C extension and no OpenSSL underneath. You create a SigningKey, sign bytes with it, hand out the matching VerifyingKey, and the other side calls verify() which either returns True or raises BadSignatureError. It covers ECDSA signatures on the NIST prime curves (192, 224, 256, 384, 521 bits), secp256k1 as used by Bitcoin, the Brainpool curves, EdDSA on Ed25519 and Ed448, and ECDH shared-secret derivation. Keys and signatures round-trip through raw bytes, DER, and PEM, so they interoperate with OpenSSL. The pure-Python implementation is what makes it easy to install anywhere and also what makes it unsuitable for guarding real secrets.
An excellent teaching and interoperability-testing library that ended up in fourteen million weekly installs mostly as a transitive dependency of JWT and JOSE packages. If you are choosing it deliberately for anything that guards a secret, choose pyca/cryptography instead; the README tells you the same thing.
Use it if
- You need EC crypto somewhere a compiler or OpenSSL is not available: a locked-down build image, PyPy, an embedded target, or a wheel-less platform where pip install cryptography fails
- You are writing interoperability or protocol tests and want to construct malformed or unusual signatures, points, and DER structures on purpose; a hardened library will refuse to help you, this one will
- You are teaching or learning how ECDSA and ECDH actually work and want readable Python you can step through rather than a wrapper over compiled code
- You need a curve that mainstream libraries deprioritize, such as the Brainpool family or the small SEC curves, and the data you are signing has no adversary attached
- You are protecting anything real. The project's own README states in bold that it should not be used in production settings and that it does not protect against side-channel attacks; observing a single signing operation's timing can be enough to reconstruct the private key, and the maintainers say plainly that no pure-Python crypto library can fix this
- You care about speed. The README's own benchmark table has it doing roughly 1,868 P-256 signatures per second against about 45,070 for OpenSSL 1.1.1d on the same machine; installing the optional gmpy2 extra roughly doubles it and still leaves an order of magnitude on the table
- The defaults are the opposite of what you want and nothing warns you: SigningKey.generate() with no arguments gives you a 192-bit NIST192p key, and sign() defaults to SHA-1. Both are fine for the library's stated teaching and testing purpose and wrong for anything else
- You just need JWT, TLS, or X.509 handling. pyca/cryptography does all of it on top of OpenSSL, is what the ecdsa README itself recommends, and ships wheels for every common platform so the install-difficulty argument mostly evaporated years ago
- You want a clean modern dependency tree: this still requires six, the Python 2 compatibility shim, because the package continues to support Python 2.6 and 2.7 alongside Python 3
Setup reality
pip install ecdsa is about as easy as installation gets: pure Python, one dependency (six), no compiler, no headers, works on PyPy. If you want it faster, pip install ecdsa[gmpy2] adds a native arithmetic backend that the library detects at import and uses automatically, but that extra reintroduces the compiled dependency you were probably avoiding, and gmpy2 wheels are not available everywhere. The real setup cost is deciding the parameters the library will not decide for you. Pick the curve explicitly, because the default is NIST192p. Pass hashfunc explicitly, because the default is hashlib.sha1. Choose an encoding, because to_string(), to_der(), and to_pem() produce three incompatible byte formats and from_string() additionally needs you to remember which curve it was. If you plan to talk to OpenSSL, you also need sigencode_der and sigdecode_der from ecdsa.util, since the default signature encoding is a bare concatenation of r and s.
Patterns
Create a key pair, sign, and verifygenerate-sign-verify
from ecdsa import SigningKey
sk = SigningKey.generate() # NIST192p by default
vk = sk.verifying_key
signature = sk.sign(b"message")
assert vk.verify(signature, b"message")verify() returns True or raises BadSignatureError; it never returns False, so a bare call without a try block is a silent no-op if you forget to assert. The no-argument defaults are NIST192p and SHA-1, which you should override for anything beyond a demo.
Choose the curve and hash explicitlypick-curve-and-hash
import hashlib
from ecdsa import SigningKey, NIST256p
sk = SigningKey.generate(curve=NIST256p, hashfunc=hashlib.sha256)
vk = sk.verifying_key
sig = sk.sign(b"message")
assert vk.verify(sig, b"message")Passing hashfunc at generation time makes it the default for that key's sign and verify calls. Mismatched hashes between signer and verifier fail as a bad signature with no hint about the real cause.
Verify without letting an exception escapehandle-bad-signature
from ecdsa import VerifyingKey, BadSignatureError
vk = VerifyingKey.from_pem(open("public.pem").read())
try:
vk.verify(sig, message)
print("good signature")
except BadSignatureError:
print("BAD SIGNATURE")Also catch MalformedPointError and UnexpectedDER when the key or signature bytes come from an untrusted source, because malformed input raises those instead of BadSignatureError.
Store keys in the shortest formraw-string-serialization
from ecdsa import SigningKey, VerifyingKey, NIST384p
sk = SigningKey.generate(curve=NIST384p)
sk_bytes = sk.to_string() # 48 bytes for NIST384p
sk2 = SigningKey.from_string(sk_bytes, curve=NIST384p)
vk_bytes = sk.verifying_key.to_string()
vk2 = VerifyingKey.from_string(vk_bytes, curve=NIST384p)to_string() returns bytes despite the name, a leftover from Python 2. The raw form does not record the curve, so from_string() with the wrong curve either raises MalformedPointError or silently builds a different key.
Read and write OpenSSL key formatspem-and-der
from ecdsa import SigningKey, VerifyingKey, NIST256p
sk = SigningKey.generate(curve=NIST256p)
with open("private.pem", "wb") as f:
f.write(sk.to_pem())
with open("public.pem", "wb") as f:
f.write(sk.verifying_key.to_pem())
sk2 = SigningKey.from_pem(open("private.pem").read())
vk2 = VerifyingKey.from_pem(open("public.pem").read())PEM and DER embed the curve identifier, so from_pem and from_der need no curve argument. from_pem takes text while to_pem returns bytes, which is why the write uses 'wb' and the read does not.
Sign without depending on the entropy sourcedeterministic-signatures
import hashlib
from ecdsa import SigningKey, NIST256p
sk = SigningKey.generate(curve=NIST256p)
sig1 = sk.sign_deterministic(b"message", hashfunc=hashlib.sha256)
sig2 = sk.sign_deterministic(b"message", hashfunc=hashlib.sha256)
assert sig1 == sig2sign_deterministic derives the per-signature k value from the key and message using RFC 6979. Plain sign() uses a random k, and reusing a random k across two signatures leaks the private key outright, so prefer the deterministic call.
Produce signatures OpenSSL can checkopenssl-compatible-der
import hashlib
from ecdsa import SigningKey, VerifyingKey
from ecdsa.util import sigencode_der, sigdecode_der
sk = SigningKey.from_pem(open("sk.pem").read(), hashlib.sha256)
sig = sk.sign_deterministic(data, sigencode=sigencode_der)
vk = VerifyingKey.from_pem(open("vk.pem").read())
assert vk.verify(sig, data, hashlib.sha256, sigdecode=sigdecode_der)The default encoding is r and s concatenated as fixed-width bytes, which OpenSSL will not accept. Use sigencode_string and sigdecode_string instead if you must interoperate with OpenSSL 1.0.0 or older.
Agree on a shared secretecdh-shared-secret
from ecdsa import ECDH, NIST256p
ecdh = ECDH(curve=NIST256p)
ecdh.generate_private_key()
my_public_pem = ecdh.get_public_key().to_pem()
# send my_public_pem, receive theirs
ecdh.load_received_public_key_pem(their_public_pem)
secret = ecdh.generate_sharedsecret_bytes()The output is the raw x coordinate of the shared point, not a key. Run it through HKDF or at minimum a hash before using it as an encryption key, and remember this exchange has no authentication of its own.
Use EdDSA instead of ECDSAed25519-signatures
from ecdsa import SigningKey, Ed25519
sk = SigningKey.generate(curve=Ed25519)
vk = sk.verifying_key
sig = sk.sign(b"message")
assert vk.verify(sig, b"message")Ed25519 and Ed448 hash internally and are deterministic by construction, so hashfunc and sign_deterministic do not apply. For real Ed25519 work, PyNaCl wraps libsodium and is both faster and constant-time.
Encode public keys compressedcompressed-public-keys
from ecdsa import VerifyingKey, NIST256p
vk = VerifyingKey.from_pem(open("public.pem").read())
print(vk.to_string("uncompressed").hex())
print(vk.to_string("compressed").hex())
comp = "022799c0d0ee09772fdd337d4f28dc155581951d07082fb19a38aa396b67e77759"
vk2 = VerifyingKey.from_string(bytearray.fromhex(comp), curve=NIST256p)Compressed points halve the size by storing x plus a parity byte, at the cost of a square root on decode. from_string detects the encoding from the leading byte, so you do not have to declare which one you are passing.
Derive reproducible keys from one secretkey-from-seed
import os
from ecdsa import NIST384p, SigningKey
from ecdsa.util import randrange_from_seed__trytryagain
def make_key(seed):
secexp = randrange_from_seed__trytryagain(seed, NIST384p.order)
return SigningKey.from_secret_exponent(secexp, curve=NIST384p)
master = os.urandom(NIST384p.baselen)
assert make_key(master).to_string() == make_key(master).to_string()
assert make_key(b"2-" + master).to_string() != make_key(master).to_string()Start from at least curve.baselen bytes of real entropy; a passphrase run straight through this helper gives you a key only as strong as the passphrase. from_secret_exponent, from_string, from_der, and from_pem need no entropy source at all.
Speed up repeated verification with one keyprecompute-verification
from ecdsa import SigningKey, NIST384p
sk = SigningKey.generate(curve=NIST384p)
vk = sk.verifying_key
vk.precompute()
for sig, msg in batch:
vk.verify(sig, msg)precompute() builds a lookup table for the public point; the README puts the break-even at roughly 100 verifications with the same key. It costs memory per key, so do not call it on keys you touch once.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| cryptography | PyPI | Anything production: it wraps OpenSSL, is constant-time, ships wheels everywhere, and handles X.509 and key serialization as well as raw signing. |
| pynacl | PyPI | You only need Ed25519 signatures or X25519 key exchange and want libsodium's audited, fast, hard-to-misuse implementation. |
| coincurve | PyPI | You are on secp256k1 for Bitcoin or Ethereum work and want bindings to libsecp256k1, which is orders of magnitude faster and side-channel hardened. |