Skip to content
ecdsa.com
Python

pyca/cryptography

The Python Cryptographic Authority's cryptography package, the standard crypto library for Python: OpenSSL-backed ECDSA with DER signatures and opt-in RFC 6979, EdDSA, ML-KEM and ML-DSA, and no JWK support.
License
Apache-2.0 OR BSD-3-Clause
Checked version
50.0.2 ·
Source
github.com/pyca/cryptography
Documentation
cryptography.io/en/latest/

Compatibility at a glance

ECDSAEdDSAPost-quantum

ECDSA curves
P-224P-256P-384P-521secp256k1brainpoolP256r1brainpoolP384r1brainpoolP512r1
EdDSA
Ed25519Ed448
Public key formats
spki-pemspki-dersec1-uncompressedsec1-compressedopenssh
Private key formats
pkcs8-pempkcs8-derencrypted-pkcs8sec1-pemsec1-derraw-scalaropenssh

cryptography is what most Python code — PyJWT, Authlib, paramiko, certbot, the AWS and Google SDKs — uses for real cryptography. Its hazmat layer exposes keys and signatures directly; the wheels bundle their own OpenSSL, so behaviour follows the package version rather than the operating system (50.0.2 ships OpenSSL 4.0.3).

ECDSA signatures come out as ASN.1 DER, and verify() expects DER. WebCrypto, JWS and most hardware tokens use the 64-byte raw r‖s form, which verify() rejects with the same InvalidSignature as a forged signature. decode_dss_signature and encode_dss_signature convert between DER and the integer pair; the fixed-width padding is yours to add, exactly as the sample shows, or use the DER ⇄ raw converter to check by hand.

The nonce is random unless you ask for RFC 6979: ec.ECDSA(hashes.SHA256(), deterministic_signing=True) reproduced the RFC's P-256 test vector exactly — including its high s, because nothing in the package normalizes s, and verification accepts both halves. Signatures headed for a low-S verifier such as libsecp256k1 or @noble/curves need s replaced by n − s first. sign() and verify() hash the data themselves; pass Prehashed(hashes.SHA256()) when you already hold a digest, or you will hash twice.

Curve coverage is broad — P-224 through P-521, secp256k1 and the Brainpool r1 curves — plus Ed25519, Ed448, ML-KEM-768/1024 and ML-DSA-44/65/87 in 50.0.2. Keys load from PEM and DER (SPKI, PKCS#8, encrypted PKCS#8, SEC1), OpenSSH and bare SEC1 points; there is no JWK reader, so JWKs go through PyJWT, Authlib or jwcrypto, or the JWK converter.

Most errors people search for come from key loading rather than signing: a public key passed to load_pem_private_key, an encrypted key without a password, or a point that is not on the named curve. The signature verifier and the library interop matrix help when a signature from another stack refuses to verify.

Facts and the checks behind them

Nothing below is copied from documentation. The check content/libraries/verify/pyca-cryptography/run.sh ran pyca/cryptography 50.0.2 on OpenSSL 4.0.3 29 Sep 2026 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

    der

    sign() returns ASN.1 DER and verify() expects DER.

    Recorded by the check
    fact ecdsa-signature-encoding = der
  • Low-S when signing

    either

    s is not normalized: high-S values appeared among 64 signatures.

    Recorded by the check
    fact ecdsa-low-s-sign = either
  • 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

    Random by default; deterministic_signing=True reproduced RFC 6979 appendix A.2.5 exactly, including its high s.

    Recorded by the check
    fact ecdsa-nonce = rfc6979-opt-in
    evidence ecdsa-nonce: default repeats: False; deterministic_signing=True reproduces RFC 6979 A.2.5 (r and high s): True
  • What the signing call hashes

    hashes-message-or-digest

    sign() hashes the data; with Prehashed(hashes.SHA256()) it signs a digest you computed, and that signature verified against the original message.

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

    throws

    A raw r‖s signature, a 3-byte blob and a signature over another message all raise InvalidSignature; the exception does not say which.

    Recorded by the check
    fact ecdsa-malformed-signature = throws
    evidence ecdsa-malformed-signature: raw r||s: InvalidSignature
    evidence ecdsa-malformed-signature: 3 bytes: InvalidSignature
    evidence ecdsa-malformed-signature: wrong message: InvalidSignature
  • ECDSA curves

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

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

    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

    ML-KEMML-DSA

    ML-KEM-768 and ML-KEM-1024 encapsulated and decapsulated, and ML-DSA-44/65/87 signed and verified; there is no ML-KEM-512 class and no SLH-DSA.

    Recorded by the check
    fact pqc = ML-KEM, ML-DSA
    evidence pqc: ML-KEM-512 class present: False; SLH-DSA module present: False
  • Public key formats

    spki-pemspki-dersec1-uncompressedsec1-compressedopenssh

    Public keys loaded from SPKI PEM and DER, OpenSSH, and SEC1 points (compressed or uncompressed) via from_encoded_point; a 64-byte x‖y without the prefix was rejected, and there is no JWK support.

    Recorded by the check
    fact public-key-formats = spki-pem, spki-der, sec1-uncompressed, sec1-compressed, openssh
    evidence public-key-formats: no JWK reader or writer in the package
  • Private key formats

    pkcs8-pempkcs8-derencrypted-pkcs8sec1-pemsec1-derraw-scalaropenssh

    Private keys loaded from PKCS#8 PEM and DER, encrypted PKCS#8, SEC1 PEM and DER, OpenSSH, and a raw scalar via derive_private_key.

    Recorded by the check
    fact private-key-formats = pkcs8-pem, pkcs8-der, encrypted-pkcs8, sec1-pem, sec1-der, raw-scalar, openssh

Usage

Sign with P-256, convert the DER signature to raw r‖s and back, and verify.
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature, encode_dss_signature

key = ec.generate_private_key(ec.SECP256R1())
message = b"order #1042 approved"

# DER out of sign(); turn it into the 64-byte r||s that WebCrypto and JOSE expect.
der = key.sign(message, ec.ECDSA(hashes.SHA256()))
r, s = decode_dss_signature(der)
raw = r.to_bytes(32, "big") + s.to_bytes(32, "big")

# A raw signature from WebCrypto goes the other way before verify().
back = encode_dss_signature(int.from_bytes(raw[:32], "big"), int.from_bytes(raw[32:], "big"))
try:
    key.public_key().verify(back, message, ec.ECDSA(hashes.SHA256()))
    print("verified: True, raw bytes:", len(raw))
except InvalidSignature:
    print("verified: False")

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 cryptography==50.0.2
Command
python usage.py
Output must match (regular expression)
verified: True, raw bytes: 64

Libraries