Skip to content
ecdsa.com

Generate a CSR · Python

How to generate a CSR for an EC P-256 key with SANs in Python

The cryptography package (pyca) generates a CSR with x509.CertificateSigningRequestBuilder: set the subject, add a SubjectAlternativeName extension with the host names, and sign with a P-256 key and SHA-256. The run writes key.pem and csr.pem and decodes the CSR with OpenSSL to confirm the subject, the SANs and the signature.

Tested with cryptography 50.0.2 (Python 3.14.8) and OpenSSL 3.5.7 (openssl req -verify and -text cross-check).

csr.py — P-256 key and CSR with two SANs
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.x509.oid import NameOID
key = ec.generate_private_key(ec.SECP256R1()) # P-256
csr = (
x509.CertificateSigningRequestBuilder()
.subject_name(x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Example Corp"),
x509.NameAttribute(NameOID.COMMON_NAME, "api.example.com"),
]))
# The SAN extension is what browsers and CAs match host names against
.add_extension(
x509.SubjectAlternativeName([x509.DNSName("api.example.com"), x509.DNSName("www.example.com")]),
critical=False,
)
.sign(key, hashes.SHA256()) # ecdsa-with-SHA256, signed with the request's own key
)
with open("key.pem", "wb") as f:
f.write(key.private_bytes(
serialization.Encoding.PEM,
serialization.PrivateFormat.PKCS8,
serialization.NoEncryption(), # use BestAvailableEncryption(b"…") to protect it
))
with open("csr.pem", "wb") as f:
f.write(csr.public_bytes(serialization.Encoding.PEM))
print("signature valid:", csr.is_signature_valid)

How it works

  1. ec.generate_private_key(ec.SECP256R1()) creates the P-256 key (SECP256R1 is the SEC name of the same curve).
  2. subject_name(x509.Name([...])) sets the subject; attributes appear in the CSR in the order given.
  3. add_extension(x509.SubjectAlternativeName([...]), critical=False) adds the SANs to the extensionRequest attribute.
  4. sign(key, hashes.SHA256()) produces the ecdsa-with-SHA256 self-signature that proves the requester holds the key; is_signature_valid checks it.

Gotchas

  • The CN is not a substitute for SANs: clients match host names against the SAN extension, so api.example.com appears in both places here.
  • NoEncryption() writes the private key in clear text. For a key that will sit on disk, use BestAvailableEncryption(b"passphrase") and keep the passphrase elsewhere.
  • sign(key, None) is only valid for Ed25519 and Ed448 keys; with an EC key it raises TypeError: Algorithm must be a registered hash algorithm, not None.
  • critical=False is the norm for SANs whenever the subject is non-empty; RFC 5280 requires the extension to be critical only when the subject is empty.

Related recipes