z-generar_certificado-20250408.py (Código fuente)

#!/usr/bin/env python3

# (c) 2018-2023 jcea@jcea.es - https://www.jcea.es/
# Código liberado como Dominio Público.
# Haz con él lo que quieras.

import os
import sys
import argparse
import datetime
import ipaddress

# Ver https://cryptography.readthedocs.org/en/latest/x509/tutorial/
# Ver https://cryptography.io/en/latest/x509/reference/#cryptography.x509.CertificateSigningRequestBuilder

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

parser = argparse.ArgumentParser(
            description='Genera un certificado con SubjectAlternativeName.')
parser.add_argument('--CA', required=True, nargs=1,
                    help='CA que se usará para firmar')
parser.add_argument('--days', default=365, metavar='DÍAS', type=int,
                    help='Expiración en días (365 por defecto)')
parser.add_argument('NAME', help='Nombre del certificado y el CN')
parser.add_argument('SAN', nargs='+', help='Nombres de dominio e IPs')

args = parser.parse_args()
name = args.NAME  # Esto es UNICODE
san = args.SAN  # Esto es UNICODE
CA = args.CA[0]

print(f'Generamos un certificado que expira en {args.days} días',
      file=sys.stderr)

key = rsa.generate_private_key(
        public_exponent=65537,
        key_size=2048,
        backend=default_backend()
    )

with open(CA + '.cert', 'rb') as f:
    CA_certificate = f.read()
CA_certificate = x509.load_pem_x509_certificate(CA_certificate,
                                                backend=default_backend())

try:
    is_CA = CA_certificate.extensions.get_extension_for_class(
                x509.BasicConstraints).value.ca
except x509.ExtensionNotFound:
    is_CA = False
if not is_CA:
    raise RuntimeError(f'El certificado "{CA}" '
                       'no parece ser una entidad de certificación')

with open(CA + '.key', 'rb') as f:
    CA_key = f.read()
CA_key = serialization.load_pem_private_key(CA_key, password=None,
                                            backend=default_backend())

subject = [i for i in CA_certificate.subject if i.oid != NameOID.COMMON_NAME]
subject.append(x509.NameAttribute(NameOID.COMMON_NAME, name))
subject = x509.Name(subject)

SubjectAlternativeName = []
for element in san:
    try:
        ip = ipaddress.ip_address(element)
        element = x509.IPAddress(ip)
    except ValueError:
        element = x509.DNSName(element)
    SubjectAlternativeName.append(element)
SubjectAlternativeName = x509.SubjectAlternativeName(SubjectAlternativeName)

builder = x509.CertificateBuilder()
builder = builder.subject_name(subject)
builder = builder.issuer_name(CA_certificate.subject)
builder = builder.public_key(key.public_key())
builder = builder.serial_number(x509.random_serial_number())
builder = builder.not_valid_before(datetime.datetime.utcnow())
builder = (builder.not_valid_after(datetime.datetime.utcnow() +
           datetime.timedelta(days=args.days)))
builder = builder.add_extension(SubjectAlternativeName, critical=False)
builder = builder.add_extension(x509.BasicConstraints(ca=False,
                                                      path_length=None),
                                critical=True)
builder = builder.add_extension(x509.ExtendedKeyUsage([
                                    x509.oid.ExtendedKeyUsageOID.SERVER_AUTH,
                                    x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH
                                    ]),
                                critical=True)

cert = builder.sign(CA_key, hashes.SHA256(), backend=default_backend())

with open(name + '.cert', 'wb') as f:
    f.write(cert.public_bytes(serialization.Encoding.PEM))
with open(name + '.key', 'wb',
          opener=lambda x, y: os.open(x, y, mode=0o600)) as f:
    f.write(key.private_bytes(
              encoding=serialization.Encoding.PEM,
              format=serialization.PrivateFormat.TraditionalOpenSSL,
              encryption_algorithm=serialization.NoEncryption())
            )