Python cryptography: create a certificate signed by an existing CA, and export

There are two issues that I see here. First, you're creating another self-signed certificate so the certificate you've generated is not signed by the CA, it is itself a CA. To correct this you sign with the private key of your CA (e.g. private_key in your example), but you need to create a new private key associated with the new certificate and embed the public key of that in the cert.

certificate_private_key = <generate an ec or rsa key here>
certificate_public_key = certificate_private_key.public_key()

Then do

builder = builder.public_key(certificate_public_key)

You also have an issue with your output because you're trying to copy and paste things out of a print statement. The output of cert.public_bytes(serialization.Encoding.PEM) will be a valid X509 certificate with delimiters and proper PEM line lengths, so write it directly to a file:

with open("cert.crt", "wb") as f:
    f.write(cert.public_bytes(serialization.Encoding.PEM))

The result can be parsed with openssl x509 -noout -text -in cert.crt

Here is a complete example utilizing cryptography to create a self-signed root CA and sign a certificate using that CA.

import datetime

from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa


root_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048,
    backend=default_backend()
)
subject = issuer = x509.Name([
    x509.NameAttribute(NameOID.COUNTRY_NAME, u"US"),
    x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u"Texas"),
    x509.NameAttribute(NameOID.LOCALITY_NAME, u"Austin"),
    x509.NameAttribute(NameOID.ORGANIZATION_NAME, u"My Company"),
    x509.NameAttribute(NameOID.COMMON_NAME, u"My CA"),
])
root_cert = x509.CertificateBuilder().subject_name(
    subject
).issuer_name(
    issuer
).public_key(
    root_key.public_key()
).serial_number(
    x509.random_serial_number()
).not_valid_before(
    datetime.datetime.utcnow()
).not_valid_after(
    datetime.datetime.utcnow() + datetime.timedelta(days=3650)
).sign(root_key, hashes.SHA256(), default_backend())

# Now we want to generate a cert from that root
cert_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048,
    backend=default_backend()
)
new_subject = x509.Name([
    x509.NameAttribute(NameOID.COUNTRY_NAME, u"US"),
    x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u"Texas"),
    x509.NameAttribute(NameOID.LOCALITY_NAME, u"Austin"),
    x509.NameAttribute(NameOID.ORGANIZATION_NAME, u"New Org Name!"),
])
cert = x509.CertificateBuilder().subject_name(
    new_subject
).issuer_name(
    root_cert.issuer
).public_key(
    cert_key.public_key()
).serial_number(
    x509.random_serial_number()
).not_valid_before(
    datetime.datetime.utcnow()
).not_valid_after(
datetime.datetime.utcnow() + datetime.timedelta(days=30)
).add_extension(
    x509.SubjectAlternativeName([x509.DNSName(u"somedomain.com")]),
    critical=False,
).sign(root_key, hashes.SHA256(), default_backend())

I have to post an answer since I'm new and can't comment yet 🙄

I heavily relied on Pauls answer for my own implementation, that was very informative and helpful. But I had to add one more extension on the CA certificate in order to get openssl verify -verbose -CAfile ca.crt client.crt to work properly.

Adding .add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True) to the root CertificateBuilder did the trick.

ca_crt = x509.CertificateBuilder() \
    .subject_name(subject) \
    .issuer_name(issuer) \
    .public_key(ca_key.public_key()) \
    .serial_number(x509.random_serial_number()) \
    .not_valid_before(datetime.datetime.today() - one_day) \
    .not_valid_after(datetime.datetime.today() + (one_day * 365)) \
    .add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True) \
    .sign(ca_key, hashes.SHA256(), default_backend())

Did everything else just like Paul.