ecdsa.com

Sign a message · Python

How to sign a message with ECDSA in Python

With the cryptography package, signing a message with ECDSA in Python takes three lines: load the PEM, pick the hash, sign. The output is always ASN.1 DER — converting to the raw r‖s form that WebCrypto and JWTs expect is a separate, explicit step.

Tested with Python 3.14 and cryptography 50.0.

Same recipe in:Node.jsPythonGoOpenSSLWebCrypto
sign.py — sign with SHA-256 (DER output)
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.serialization import load_pem_private_key
 
private_key = load_pem_private_key(open("key.pem", "rb").read(), password=None)
message = open("message.txt", "rb").read()
 
signature = private_key.sign(message, ec.ECDSA(hashes.SHA256())) # ASN.1 DER
open("sig.der", "wb").write(signature)
print(len(signature), "bytes") # 70-72 on P-256, varies per signature
need raw r‖s (WebCrypto, JWS)? split the DER
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
 
r, s = decode_dss_signature(signature)
raw = r.to_bytes(32, "big") + s.to_bytes(32, "big") # 64 bytes on P-256
open("sig.raw", "wb").write(raw)

How it works

  1. load_pem_private_key handles PKCS#8 and SEC1 PEMs; password=None is required (positionally or by name) for unencrypted keys.
  2. sign(message, ec.ECDSA(hashes.SHA256())) hashes internally. For a pre-computed digest, use ec.ECDSA(utils.Prehashed(hashes.SHA256())).
  3. The result is DER — variable length, starting with 0x30. decode_dss_signature exposes the integers when a fixed-width encoding is needed.

Gotchas

  • Two signatures over the same message will differ: pyca cryptography uses a random nonce per signature. Don't write tests that expect byte-identical output.
  • r.to_bytes(32, …) is the P-256 width. On P-384 use 48, on P-521 use 66 — to_bytes(32, …) on a larger curve raises OverflowError, which is your hint, not a library bug.
  • The PyPI package named ecdsa is a different library with raw-by-default output and its own API. Copy-pasting between the two ecosystems produces signatures in the wrong format.

Related recipes