Skip to content
ecdsa.com

Self-signed certificate · Python

How to create a self-signed EC P-256 certificate with SANs in Python

The cryptography package (pyca) builds certificates with x509.CertificateBuilder: subject and issuer get the same name, the validity comes from timezone-aware datetimes, and add_extension adds the SANs, the basic constraints and the key usages before sign() signs with the P-256 key and SHA-256. The run decodes cert.pem with OpenSSL and verifies it for localhost and 127.0.0.1.

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

cert.py — P-256 key and self-signed server certificate
import datetime
import ipaddress
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID
key = ec.generate_private_key(ec.SECP256R1())
name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "localhost")])
now = datetime.datetime.now(datetime.timezone.utc) # timezone-aware: naive datetimes are taken as UTC
cert = (
x509.CertificateBuilder()
.subject_name(name)
.issuer_name(name) # self-signed: issuer = subject
.public_key(key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(now)
.not_valid_after(now + datetime.timedelta(days=365))
.add_extension(x509.SubjectAlternativeName([
x509.DNSName("localhost"),
x509.IPAddress(ipaddress.ip_address("127.0.0.1")),
]), critical=False)
.add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True)
.add_extension(x509.KeyUsage(
digital_signature=True, content_commitment=False, key_encipherment=False, data_encipherment=False,
key_agreement=False, key_cert_sign=False, crl_sign=False, encipher_only=False, decipher_only=False,
), critical=True)
.add_extension(x509.ExtendedKeyUsage([ExtendedKeyUsageOID.SERVER_AUTH]), critical=False)
.sign(key, hashes.SHA256())
)
with open("key.pem", "wb") as f:
f.write(key.private_bytes(serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption()))
with open("cert.pem", "wb") as f:
f.write(cert.public_bytes(serialization.Encoding.PEM))

How it works

  1. ec.generate_private_key(ec.SECP256R1()) creates the key; SECP256R1 is P-256.
  2. subject_name and issuer_name take the same x509.Name, and sign(key, hashes.SHA256()) signs with the certificate's own key — together that makes it self-signed.
  3. x509.random_serial_number() returns a random positive serial of at most 159 bits, inside the 20 bytes RFC 5280 allows.
  4. x509.DNSName and x509.IPAddress(ipaddress.ip_address(…)) are the two SAN types; critical=True marks basicConstraints and keyUsage as critical.
  5. private_bytes(…, PrivateFormat.PKCS8, NoEncryption()) writes key.pem as unencrypted PKCS#8, public_bytes(Encoding.PEM) writes cert.pem.

Gotchas

  • Use timezone-aware datetimes: cryptography reads a naive datetime as UTC, so datetime.now() on a machine at UTC+2 produced a certificate that starts two hours in the future and is rejected as not yet valid until then.
  • x509.KeyUsage takes all nine flags; leaving some out fails with TypeError: KeyUsage.__init__() missing 8 required positional arguments: 'content_commitment', ….
  • The not_valid_before and not_valid_after properties are deprecated: reading them warns CryptographyDeprecationWarning: Properties that return a naïve datetime object have been deprecated. Please switch to not_valid_before_utc.
  • Python clients reject the certificate until they trust it: urlopen fails with [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate, while context=ssl.create_default_context(cafile="cert.pem") connects by both localhost and 127.0.0.1.

Related recipes