mrkeyoor.com_
Thu 06 Aug 10:55 UTC
PyPIUtilsupdated 06 Aug 2026

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.

Verdict

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.

API stability5/5Despite the 0.x version number, SigningKey and VerifyingKey have kept the same shape for over a decade; releases add curves, encodings, and speedups rather than moving methods around, and code from 2015 still runs.
Docs4/5Read the Docs pages plus a long README with runnable snippets for every operation, published benchmark tables, and an unusually direct security section that tells you not to use it. Points off because some examples still carry Python 2 syntax.
Maintenance4/50.19.2 shipped in March 2026 with the repository pushed in June 2026, and only 15 issues are open (20 counting PRs). Testing discipline is high, with published mutation-score and condition-coverage badges, though the work rests on a very small group.
Ecosystem3/5About 14.1 million weekly downloads, but the traffic is largely transitive through JOSE and JWT packages rather than direct adoption, and no plugin ecosystem grew around it. Fewer than 1,000 GitHub stars for that download count tells the story.

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
Skip it if

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 == sig2

sign_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

PackageRegistryPick it when
cryptographyPyPIAnything production: it wraps OpenSSL, is constant-time, ships wheels everywhere, and handles X.509 and key serialization as well as raw signing.
pynaclPyPIYou only need Ed25519 signatures or X25519 key exchange and want libsodium's audited, fast, hard-to-misuse implementation.
coincurvePyPIYou are on secp256k1 for Bitcoin or Ethereum work and want bindings to libsecp256k1, which is orders of magnitude faster and side-channel hardened.