from cryptography.hazmat.primitives.asymmetric import ecfrom cryptography.hazmat.primitives.serialization import ( Encoding, NoEncryption, PrivateFormat, PublicFormat,) private_key = ec.generate_private_key(ec.SECP256R1()) # P-256 priv_pem = private_key.private_bytes( Encoding.PEM, PrivateFormat.PKCS8, NoEncryption())pub_pem = private_key.public_key().public_bytes( Encoding.PEM, PublicFormat.SubjectPublicKeyInfo) open("key.pem", "wb").write(priv_pem)open("pub.pem", "wb").write(pub_pem)print(pub_pem.decode(), end="") # -----BEGIN PUBLIC KEY-----from cryptography.hazmat.primitives.serialization import BestAvailableEncryption priv_pem = private_key.private_bytes( Encoding.PEM, PrivateFormat.PKCS8, BestAvailableEncryption(b"passphrase"))How it works
ec.SECP256R1()is P-256 / prime256v1. The library names curves by their SEC designation —SECP384R1,SECP521R1, andSECP256K1for the Bitcoin curve.PrivateFormat.PKCS8yieldsBEGIN PRIVATE KEY;PrivateFormat.TraditionalOpenSSLyields the legacyBEGIN EC PRIVATE KEY.- The public half always travels as
PublicFormat.SubjectPublicKeyInfo— theBEGIN PUBLIC KEYcontainer other languages load directly. - For debugging,
private_key.private_numbers().private_valueexposes the raw scalar d as a Python int — never log it outside a throwaway test key.
Gotchas
- SECP256**R**1 and SECP256**K**1 differ by one letter and are entirely different curves. Signatures made on one never verify on the other, and the error says nothing about curves.
NoEncryption()writes the private key in the clear — deliberate for CI fixtures, dangerous for anything durable.BestAvailableEncryptioncosts one line.- If another tool refuses your private key, check the header first: some older stacks want
TraditionalOpenSSL(SEC1), others only PKCS#8. Re-serialize rather than re-generate.