La biblioteca "cryptography" o cómo generar CSRs en Python

OpenSSL es una biblioteca grande y compleja. Se utiliza para todo: criptografía simétrica, criptografía asimétrica, hashes, SSL, TLS y toda la gestión X.509: generar certificados X.509, generar CSRs, firmar certificados X.509 como autoridad de certificación y realizar todas las verificaciones necesarias. El resultado es una biblioteca grande y pesada en la que se encuentran problemas de seguridad con frecuencia y que evoluciona muy despacio.

Hace mucho que quiero utilizar alternativas a OpenSSL para algunas tareas, pero lo cierto es que hasta hace poco no había nada que le hiciese competencia. Esto ha cambiado -para algunas actividades- con la aparición de la biblioteca Cryptography.

Cryptography es una biblioteca Python con el objetivo claro de ser LA biblioteca criptográfica para Python. El proyecto evoluciona rápido y bien, la lista de correo es muy amigable, soporta Python 3 y, en general, me da muy buenas vibraciones.

Sigo el proyecto desde sus inicios, pero no he podido utilizarla en producción hasta la versión 1.0 al incluir, por fin, soporte para muchas operaciones necesarias para trabajar con certificados X.509.

Hace unos días StartSSL me envió varios emails para renovar los certificados de los dominios que detallo en ¡Cifrad, cifrad, malditos!. Era la oportunidad perfecta para empezar a hacer algún pinito con Cryptography y empezar a relegar OpenSSL para otras tareas.

El proceso para obtener un certificado X.509 para un servidor web [2] es algo así:

  1. Generamos una clave pública y una clave privada.
  2. Tomamos la clave pública, añadimos metadatos como información de contacto y los dominios a los que queremos asociar esa clave pública. Con esto generamos un CSR o, lo que es lo mismo, un Certificate signing request.
  3. Enviamos ese CSR a la autoridad de certificación de nuestra elección.
  4. La autoridad de certificación realiza las comprobaciones de identidad que estima necesarias y nos devuelve un certificado X.509 válido conteniendo la clave pública original y diversos metadatos, todo ello firmado con la clave pública de la autoridad de certificación.
  5. Copiamos ese certificado X.509 al servidor web, vinculamos ese certificado y la clave secreta que le corresponde a un dominio web determinado y ¡plof! ya podemos servir páginas web de forma segura a través de HTTPS.

Tradicionalmente la generación de las claves pública y privada y del Certificate signing request es algo que se hace con OpenSSL. Yo ya no. Yo ahora utilizo Cryptography para esto.

El código:

 #!/usr/bin/env python3

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

 import sys, time, os, argparse

 parser = argparse.ArgumentParser(description='''Genera claves pública, privada
 y CSR para su firma por parte de una autoridad de certificación.''')
 parser.add_argument('dominio',
                     help='Dominio para el que generamos las claves y el CSR')
 args = parser.parse_args()
 dominio = args.dominio  # Esto es UNICODE


 # 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

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

 builder = x509.CertificateSigningRequestBuilder()

 builder = builder.subject_name(x509.Name([
         x509.NameAttribute(NameOID.COMMON_NAME, dominio),
     ]))
 builder = builder.add_extension(
         x509.BasicConstraints(ca=False, path_length=None), critical=True,
     )
 csr = builder.sign(
         private_key, hashes.SHA256(), default_backend()
     )

 dominio = "%s-%d" %(dominio, time.time())

 with open(dominio+'.key', 'wb') as f :
     os.fchmod(f.fileno(), 0o400)
     f.write(private_key.private_bytes(
            encoding=serialization.Encoding.PEM,
             format=serialization.PrivateFormat.TraditionalOpenSSL,
             encryption_algorithm=serialization.NoEncryption(),
         ))

 with open(dominio+'.csr', 'wb') as f :
     f.write(csr.public_bytes(serialization.Encoding.PEM))

Este código es para Python 3 (línea 1).

En las líneas 9-14 interpretamos los parámetros para recibir el dominio para el que queremos generar el Certificate signing request. En las líneas 27-31 generamos una clave pública y otra privada de 2048 bits, segura según los estándares actuales. En las líneas 33-43 generamos en CSR y añadimos algunos metadatos, incluyendo el dominio que nos interesa [1].

En las líneas 47-53 guardamos la clave privada (que incluye la pública) en disco, teniendo buen cuidado (línea 48) en protegerla con los permisos correctos. No ciframos esa clave para poder usarla fácilmente en un servidor. Nos conformamos con protegerla mediante los permisos del sistema operativo.

En las líneas 55-56 guardamos el CSR en disco. Ese fichero CSR es lo que enviamos a la autoridad de certificación para su verificación y firma.

ATENCIÓN: El CSR creado con este programa solo funcionará en StartSSL o en autoridades de certificación que funcionen de forma similar [1].
[1] (1, 2)

Un Certificate signing request normal requiere muchos más datos. Se puede ver un ejemplo completo y funcional en el tutorial X.509 de Cryptography.

En mi caso concreto no es necesario usar ningún metadato en absoluto, ni siquiera los que ya uso. Mi autoridad de certificación es StartSSL que simplemente aprovecha la clave pública contenida en el CSR y tira a la basura todo lo demás.

Yo he preferido no añadir código que no puedo probar con mi autoridad de certificación, pero tú siéntete libre para mejorar el código :-).

[2]

Se emplean certificados X.509 para muchas más cosas que servir páginas web a través de HTTPS. Se emplean para cualquier conexión SSL o TLS, como correo electrónico, IRC, XMPP y cualquier otro servicio online con el que se establezca una conexión segura.

También se pueden emplear certificados X.509 para autenticar a los propios usuarios. En España, por ejemplo, existen certificados X.509 oficiales con fuerza legal (DNI Electrónico y los certificados de la Fábrica Nacional de Moneda y Timbre).