angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Usando Python's pyOpenSSL para verificar los certificados SSL descargados de un host

A partir de noviembre de 2020 la Cadena de Confianza puede ser verificada sin llamar a OpenSSL con Python's subprocess.

17 diciembre 2020
post main image
https://unsplash.com/@ilhamfzn

Mientras escribía un guión para comprobar si los sitios web se redirigían correctamente a 'https:/www'. pensé en añadir algunas comprobaciones de certificados SSL también. Esto significa que tuve que verificar los certificados SSL descargados de un host. ¿El certificado es realmente para este sitio web? Muéstrame la fecha de caducidad. ¿Es la cadena del certificado correcta? ¿Podemos confiar en los certificados?

Inicialmente me quedé atascado donde mucha gente se quedó atascada por lo siguiente. Los certificados intermedios que se descargan de un host no son de confianza y pyOpenSSL no usó la bandera 'untrusted' al verificar una cadena de certificados. Lo que significa que el certificado puede ser marcado como fiable mientras que no lo es.

La única forma de evitarlo era ejecutar el comando de verificación OpenSSL usando subprocess. No es lo que queremos, pero al menos podríamos hacerlo. Hay otras formas pero son mucho más complejas. Esto fue reportado y discutido en 2016. Con el lanzamiento de pyOpenSSL 20.0.0 (2020-11-27) se hicieron los siguientes cambios:

  • un nuevo método 'load_locations()' a X509Store para establecer paquetes de archivos y/o directorios de certificados de confianza
  • un nuevo parámetro "cadena" a X509StoreContext donde se pueden añadir certificados untrusted
  • un nuevo método 'get_verified_chain()' aX509StoreContext devolviendo la cadena completa validada

Con estos cambios, ahora podemos finalmente verificar la Cadena de Confianza.

Python y la criptografía no es fácil

Los sitios web, los servicios se han movido de HTTP a HTTPS. Esto significa que cuando te conectas a un servicio debes comprobar también los certificados. ¿Está Python haciendo esto por usted? El Python requests library está haciendo esto automáticamente para usted. Ni siquiera tienes que añadir un parámetro como 'verify=True'.

Pero aquí estoy buscando una manera de comprobar los certificados SSL en mi propio script Python . A continuación describo algunas formas de hacerlo y algún código Python que escribí para investigar esto. Esto se está ejecutando en mi PC Ubuntu 18.04.

Lectura relevante, viendo

Tal vez quieras empezar leyendo los artículos "Chain of Fools: An Exploration of Certificate Chain Validation Mishaps", y "[Cryptography-dev] on how (not) to chain certs with openssl + pyopenssl", y viendo un bonito video "Digital Certificates: Chain of Trust", ver los enlaces de abajo.

La clase CertInfo

En los siguientes ejemplos utilizo una clase CertInfo(). Esta es una clase que escribí para extraer información de un certificado. El código Python está al final de este artículo.

La cadena del certificado

Una cadena de certificados es una lista vinculada de certificados. En cada certificado hay dos elementos que especifican cómo están vinculados:

  • Subject-CN (nombre común)
  • Issuer-CN (nombre común)

A partir del certificado del servidor, es emitido por el Issuer-CN. El certificado del servidor también se llama certificado de entidad final, certificado de hoja o certificado de abonado. El siguiente certificado que descarguemos debe tener un Subject-CN idéntico al Issuer-CN del certificado del servidor, etc.

A continuación, el primer certificado descargado del servidor es host_cert[0], el segundo host_cert[1], etc. El último certificado es el certificado raíz root_cert.

Example#1: www.badssl.com

INFO - ------------------------------------------------------------
INFO - Dumping certs:  www.badssl.com:443 ...
INFO - * got 2 certs from host
INFO - * got root_cert
INFO - host_cert[0]
INFO - *  Subject-CN: *.badssl.com
INFO - *  Issuer-CN: DigiCert SHA2 Secure Server CA
INFO - host_cert[1]
INFO - *  Subject-CN: DigiCert SHA2 Secure Server CA
INFO - *  Issuer-CN: DigiCert Global Root CA
INFO - root_cert
INFO - *  Subject-CN: DigiCert Global Root CA
INFO - *  Issuer-CN: DigiCert Global Root CA

Ejemplo#2: two-intermediate-certs-example.org

INFO - ------------------------------------------------------------
INFO - Dumping certs:  two-intermediate-certs-example.org:443 ...
INFO - * got 3 certs from host
INFO - * got root_cert
INFO - host_cert[0]
INFO - *  Subject-CN:  two-intermediate-certs-example.org
INFO - *  Issuer-CN: Sectigo RSA Domain Validation Secure Server CA
INFO - host_cert[1]
INFO - *  Subject-CN: Sectigo RSA Domain Validation Secure Server CA
INFO - *  Issuer-CN: USERTrust RSA Certification Authority
INFO - host_cert[2]
INFO - *  Subject-CN: USERTrust RSA Certification Authority
INFO - *  Issuer-CN: AAA Certificate Services
INFO - root_cert
INFO - *  Subject-CN: AAA Certificate Services
INFO - *  Issuer-CN: AAA Certificate Services

Example#3: www.example.org

INFO - ------------------------------------------------------------
INFO - Dumping certs:  www.example.org:443 ...
INFO - * got 3 certs from host
INFO - * got root_cert
INFO - host_cert[0]
INFO - *  Subject-CN:  www.example.org
INFO - *  Issuer-CN: DigiCert TLS RSA SHA256 2020 CA1
INFO - host_cert[1]
INFO - *  Subject-CN: DigiCert TLS RSA SHA256 2020 CA1
INFO - *  Issuer-CN: DigiCert Global Root CA
INFO - host_cert[2]
INFO - *  Subject-CN: DigiCert Global Root CA
INFO - *  Issuer-CN: DigiCert Global Root CA
INFO - root_cert
INFO - *  Subject-CN: DigiCert Global Root CA
INFO - *  Issuer-CN: DigiCert Global Root CA

Con estos datos podemos comprobar si la cadena que comienza en el certificado del servidor termina en el certificado raíz. Si queremos verificar si los certificados están correctamente configurados, es esencial que lo hagamos porque OpenSSL no comprueba esta secuencia! Obsérvese que www.badssl.com tiene un certificado intermedio y two-intermediate-certs-example.org y www.example.org tienen dos certificados intermedios. www.example.org es un caso especial, lo discutiré más adelante.

Aquí está el código Python para comprobar la cadena de certificados:

    def check_chain_order(self):

        if self.root_cert is  None:
            return False

        # link issuer-subject, start with server_cert
        for i, cert in enumerate(self.host_certs):
            if i < 1:
                # we need two
                continue
            ci1 =  CertInfo(self.host_certs[i - 1])
            ci2 =  CertInfo(self.host_certs[i])
            if ci1.issuer_cn != ci2.subject_cn:
                return False

        # link issuer-subject, root_cert
        ci1 =  CertInfo(self.host_certs[-1])
        ci2 =  CertInfo(self.root_cert)
        if ci1.issuer_cn != ci2.subject_cn:
            return False

        return True

Obtener el certificado raíz

Para verificar la cadena de certificados también necesitamos el certificado raíz. En muchos casos se puede incluir una ruta CA root a un directorio de su dispositivo y la función de verificación buscará el certificado raíz automáticamente. En mi PC puedo establecer el parámetro CApath OpenSSL a:

/etc/ssl/certs

¿Pero qué pasa si queremos obtener el certificado CA root para utilizarlo en la función de verificación de la cadena de certificados como se ha descrito anteriormente? No vi cómo puedo hacer esto con pyOpenSSL pero puede hacerse de otras maneras.

Primero, podemos hacer esto con OpenSSL y subprocess. El comando OpenSSL es:

openssl  x509 -noout -issuer_hash -in cert.pem

donde cert.pm es el último certificado (intermedio) obtenido del host (asumiendo que el orden del certificado es correcto). Existen algunas limitaciones, pero en la mayoría de los casos esto funcionará. Esto devuelve un número hexadecimal como:

4a6481c9

Con un '.0' adjunto esto es un symlink al certificado raíz:

ls -l /etc/ssl/certs/4a6481c9.0

Resultado:

lrwxrwxrwx 1 root root 27 okt 14  2017 /etc/ssl/certs/4a6481c9.0 -> GlobalSign_Root_CA_-_R2.pem

Si quiere saber por qué esto funciona así, entonces vea la utilidad c_rehash que viene con OpenSSL. Esto construye un hash para una rápida búsqueda de certificados raíz.

En Python utilizo subprocess para ejecutar el comando OpenSSL , y luego escribo el archivo raíz en mi directorio para un fácil acceso. Note que uso STDIN para alimentar el PEM a OpenSSL y uso STDOUT para capturar el resultado.

    def get_root_cert(self, pem):
        cmd = ['openssl', 'x509', '-noout', '-issuer_hash']
        p =  subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        out = p.communicate(input=pem.encode())[0].decode('utf-8').strip()

        if p.returncode != 0:
            return  None

        root_pem_file = os.path.join(os.sep, 'etc', 'ssl', 'certs', out  +  '.0')
        if not os.path.isfile(root_pem_file):
            return  None

        with open(root_pem_file, 'r') as fh:
            root_pem = fh.read()

        with open('root.pem', 'w') as fh:
            fh.write(root_pem)
        return crypto.load_certificate(crypto.FILETYPE_PEM, root_pem)

Otra forma de obtener el certificado raíz si tenemos un paquete de certificado raíz en nuestro sistema es descargando este paquete, por ejemplo desde el sitio web de Curl :

https://curl.haxx.se/docs/caextract.html

Este archivo contiene el CA root PEMs. Si queremos utilizarlo debemos extraer los certificados y luego construir un diccionario con la clave (de índice) Subject-CN y el valor PEM. La primera vez que construimos y almacenamos esto en un archivo (usando Pickle), las siguientes veces cargamos este archivo, indexamos el directorio y devolvemos el PEM del certificado CA root .

    def get_root_cert(self, pem):
        root_subject_cn2pems_file = 'root_subject_cn2pems.pickle'
        cacert_file = 'cacert.pem'

        cert = crypto.load_certificate(crypto.FILETYPE_PEM, pem)
        ci =  CertInfo(cert)
        issuer_cn = ci.issuer_cn

        if os.path.isfile(root_subject_cn2pems_file):
            # use stored root pems
            with open(root_subject_cn2pems_file, 'rb') as fh:
                root_subject_cn2pems = pickle.load(fh)
        else:
            # create root pems and store
            with open(cacert_file, 'r') as fh:
                capems = fh.read()

            pem_begin = '-----BEGIN CERTIFICATE-----'
            pem_end = '-----END CERTIFICATE-----'

            root_subject_cn2pems = {}
            for part in capems.split(pem_begin)[1:]:
                if pem_end not in part:
                    continue
                pem_part, rem = part.split(pem_end, 1)
                pem = str(pem_begin  +  pem_part  +  pem_end)

                cert = crypto.load_certificate(crypto.FILETYPE_PEM, pem)
                ci =  CertInfo(cert)
                if ci.subject_cn is  None:
                    continue
                root_subject_cn2pems[ci.subject_cn] = pem

            with open(root_subject_cn2pems_file, 'wb') as fh:
                pickle.dump(root_subject_cn2pems, fh)

        # try to get root pem
        if issuer_cn not in root_subject_cn2pems:
            return  None

        root_pem = root_subject_cn2pems[issuer_cn]
        with open('root.pem', 'w') as fh:
            fh.write(root_pem)
        return crypto.load_certificate(crypto.FILETYPE_PEM, root_pem)

De nuevo, CertInfo() es una clase que escribí para extraer información de un certificado. El código Python está al final de este artículo.

Verificando la cadena de confianza

No basta con comprobar si el certificado del servidor se vincula a un certificado raíz de su PC, teléfono, dispositivo. También existe lo que se llama "Cadena de Confianza". Los certificados que descargamos de un servidor no son de confianza. El único certificado en el que se puede confiar es el certificado raíz que está en tu dispositivo, PC, en un directorio especial, en mi PC Ubuntu :

/etc/ssl/certs

Usando el certificado raíz podemos comprobar si podemos confiar en el siguiente certificado (intermedio). Si es de confianza, entonces usamos este certificado intermedio y comprobamos si el siguiente certificado puede ser de confianza. Hacemos esto hasta que llegamos al certificado del servidor.

El comando OpenSSL para el sitio web www.badssl.com :

openssl  verify -x509_strict -CApath /etc/ssl/certs -untrusted   1.pem   0.pem

Si tenemos el certificado raíz, podemos hacerlo:

openssl  verify -x509_strict -no-CApath -CAfile  root.pem  -untrusted   1.pem   0.pem

La bandera "untrusted" le dice a OpenSSL que 1.pem no puede ser confiable y debe serlo antes de revisar 0.pem. De manera similar, podemos comprobar la cadena para el sitio web two-intermediate-certs-example.org que tiene dos certificados intermedios:

openssl  verify -x509_strict -no-CApath -CAfile  root.pem  -untrusted   2.pem  -untrusted   1.pem   0.pem

Obsérvese que si se intercambian los certificados intermedios 1.pem y 2.pem por el sitio web two-intermediate-certs-example.org , el resultado es el mismo en ambos casos. Esto significa que OpenSSL está primero tratando de encontrar el orden de la cadena y luego comienza la operación de confianza. En este caso también podemos concatenar los dos certificados intermedios en 12.pem y ejecutar:

openssl  verify -x509_strict -no-CApath -CAfile  root.pem  -untrusted   12.pem   0.pem

Los certificados SSL que descargué del host están en self.host_certs y también almacenados en los archivos 0.pem, 1.pem, ... Usando Python's subprocess, el código es:

    def chain_is_trusted(self):
        cmd = ['openssl', 'verify', '-x509_strict', '-no-CApath', '-CAfile', 'root.pem']
        for i in range(self.host_certs_len - 1, 0, -1):
            cmd.extend([
                '-untrusted',
                str(i)  +  '.pem',        
            ])
        cmd.extend([
            '0.pem',
        ])
        p =  subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        out = p.communicate()[0].decode('utf-8').strip()

        if p.returncode == 0:
            return True
        return False

Esto es feo y requiere que almacenemos los archivos PEM primero antes de llamar a subprocess.

Desde pyOpenSSL 20.0.0 podemos hacer esto mucho más elegante:

    def chain_is_trusted(self):

        store = crypto.X509Store()
        store.set_flags(crypto.X509StoreFlags.X509_STRICT)
        store.load_locations(None, capath='/etc/ssl/certs')

        # server cert
        server_cert =  self.host_certs[0]

        # intermediate certs
         untrusted_certs =  self.host_certs[1:]

        store_ctx = crypto.X509StoreContext(store, server_cert, chain=untrusted_certs)

        try:
            store_ctx.verify_certificate()
            # optional
            # certs = store_ctx.get_verified_chain()
            return True
        except crypto.X509StoreContextError as e:
            pass
        return False

El método verify_certificate() genera una Excepción es si la cadena no puede ser verificada.

La cadena de confianza, paso a paso

Arriba comprobamos la Cadena de Confianza con un solo comando OpenSSL . Pero también podemos hacer esto de otra manera. Comenzando con el certificado raíz de confianza en mi PC, primero intentamos confiar en el último certificado intermedio del host. Si esto pasa, usamos el ahora certificado intermedio de confianza para tratar de confiar en el siguiente certificado untrusted hasta el certificado del servidor. Con OpenSSL usamos la bandera "partial_chain".

Por ejemplo, para el sitio web two-intermediate-certs-example.org podemos emitir los siguientes comandos:

openssl  verify -x509_strict -no-CApath -CAfile  root.pem  -partial_chain   2.pem

openssl  verify -x509_strict -no-CApath -CAfile  2.pem  -partial_chain   1.pem

openssl  verify -x509_strict -no-CApath -CAfile  1.pem  -partial_chain   0.pem

Certificados autofirmados (intermedios)

Cuando un certificado intermedio es autofirmado, OpenSSL detiene la validación(?). Esto significa que queremos comprobar si un certificado descargado del host es autofirmado.

Un certificado es autofirmado cuando:

  • el Subject-CN y el Issuer-CN coinciden
  • el subjectKeyIdentifier y el authorityKeyIdentifier coinciden
  • el certificado contiene una extensión de uso de la clave con el conjunto de bits KU_KEY_CERT_SIGN

Sólo implementé los dos primeros, no tengo ni idea de cómo hacer el tercer artículo.

Mientras probaba encontré para autofirmado.badssl.com:

subjectKeyIdentifier:  None
authorityKeyIdentifier:  None

pero para www.example.org:

subjectKeyIdentifier: 03:DE:50:35:56:D1:4C:BB:66:F0:A3:E2:1B:1B:C3:97:B2:3D:D1:55
authorityKeyIdentifier: keyid:03:DE:50:35:56:D1:4C:BB:66:F0:A3:E2:1B:1B:C3:97:B2:3D:D1:55

Asumo que debo quitar aquí el "keyid" principal: antes de comparar el subjectKeyIdentifier con el authorityKeyIdentifier.

    def cert_is_self_signed(self, cert):
        ci =  CertInfo(cert)

        # strip optional keyid from authority_key_identifier
        keyid = 'keyid:'
        keyid_len = len(keyid)
        extension_authority_key_identifier = ci.extension_authority_key_identifier
        if extension_authority_key_identifier is not  None:
            if extension_authority_key_identifier[:keyid_len] == keyid:
                extension_authority_key_identifier = extension_authority_key_identifier[keyid_len:]

        # subject_key_identifier, authority_key_identifier: both  None  or match
        if (ci.subject_cn == ci.issuer_cn) and \
            ((ci.extension_subject_key_identifier is  None  and ci.extension_authority_key_identifier is  None) or \
            (ci.extension_subject_key_identifier == extension_authority_key_identifier)):
                return True
        return False

Se puede encontrar más información sobre los certificados autofirmados en el artículo "How to know if certificate is self-signed", ver enlaces abajo.

Aquí también se refieren a RFC 3280:

"Un certificado es auto-emitido si los DNs que aparecen en los campos de sujeto y emisor son idénticos y no están vacíos. En general, el emisor y el sujeto de los certificados que conforman una ruta son diferentes para cada certificado. Sin embargo, una CA puede emitirse un certificado a sí misma para apoyar la renovación de claves o los cambios en las políticas de los certificados. Estos certificados autoemitidos no se tienen en cuenta al evaluar la longitud de la trayectoria o las limitaciones de nombre".

"El campo keyIdentifier de la extensión authorityKeyIdentifier DEBE incluirse en todos los certificados generados por las CA conformes para facilitar la construcción de la ruta de certificación. Hay una excepción; cuando una CA distribuye su clave pública en forma de certificado "autofirmado", el identificador de clave de autoridad PUEDE ser omitido. La firma de un certificado autofirmado se genera con la clave privada asociada a la clave pública del sujeto del certificado. (Esto demuestra que el emisor posee tanto la clave pública como la privada.) En este caso, los identificadores de la clave de sujeto y de autoridad serían idénticos, pero sólo se necesita el identificador de la clave de sujeto para la construcción del camino de certificación".

Confusión con example.com, example.org

Durante las pruebas también usé example.com y example.org. El anfitrión devolvió tres certificados. Estaba comprobando el resultado reemplazando los certificados y viendo qué pasaba.

openssl  verify -x509_strict -no-CApath -CAfile  root.pem  -untrusted   2.pem  -untrusted   1.pem   0.pem

Fue un desastre. Pude eliminar el certificado intermedio 2.pem y OpenSSL aún decía: OK. Podía reemplazar 1.pem por otro certificado aleatorio y OpenSSL aún decía: OK. ¿Qué estaba pasando aquí?

Entonces miré los certificados devueltos por el host y encontré que el tercer certificado, 2.pem, era autofirmado y era idéntico al certificado raíz de mi PC. ¿Esto significa que estos dominios me envían un certificado raíz? OpenSSL no se queja, los navegadores no se quejan.

No he investigado esto más a fondo, pero pensé en mencionarlo para evitar dolores de cabeza... :-(

La comprobación del host equivocado

El control de anfitrión equivocado debe asegurarse de que el certificado es para este anfitrión. No vi cómo se puede hacer esto con pyOpenSSL, de hecho no vi una forma de hacerlo con OpenSSL. OpenSSL no le advierte si el certificado es para un host diferente.

Podemos usar Wget:

wget  wrong.host.badssl.com

Resultado:

ERROR: no certificate subject alternative name matches requested host name ‘wrong.host.badssl.com’.

O podemos usar Curl:

curl -L  wrong.host.badssl.com

Resultado:

curl: (51) SSL: no alternative certificate subject name matches target host name 'wrong.host.badssl.com'

En Python podemos hacerlo con el Python requests library. Un host equivocado da la excepción:

    ...
    request.get(...)
    ...

Resultado:

HTTPSConnectionPool(host='wrong.host.badssl.com', port=443): Max retries exceeded with url: / (Caused by SSLError(CertificateError("hostname 'wrong.host.badssl.com' doesn't match either of '*.badssl.com', 'badssl.com'",),))

Note que la excepción "hostname ... doesn't match" sólo aparece si otras pruebas pasan. Si hay un problema con los certificados en algún lugar se obtiene una Excepción 'certificate verify failed'.

La clase CertInfo()

Escribí un código Python para jugar con esto. Lo más importante es la clase CertInfo(). Aquí decodifico el certificado.

# cert_info.py

import  datetime
from  OpenSSL  import crypto


class  CertInfo:
    
    def __init__(
        self,
        cert=None,
        ):
        self.cert = cert

    def decode_x509name_obj(self, o):
        parts = []
        for c in o.get_components():
            parts.append(c[0].decode('utf-8')  +  '='  +  c[1].decode('utf-8'))
        return ', '.join(parts)

    def cert_date_to_gmt_date(self, d):
        return  datetime.datetime.strptime(d.decode('ascii'), '%Y%m%d%H%M%SZ')

    def cert_date_to_gmt_date_string(self, d):
        return self.cert_date_to_gmt_date(d).strftime("%Y-%m-%d %H:%M:%S GMT")

    def get_item(self, item, extension=None, return_as=None, algo=None):
        try:
            if item == 'subject':
                return self.decode_x509name_obj(self.cert.get_subject())

            elif item == 'subject_o':
                return self.cert.get_subject().O.strip()

            elif item == 'subject_cn':
                return self.cert.get_subject().CN.strip()

            elif item == 'extensions':
                ext_count = self.cert.get_extension_count()
                if extension is  None:
                    ext_infos = []
                    for i in range (0, ext_count):
                        ext = self.cert.get_extension(i)
                        ext_infos.append(ext.get_short_name().decode('utf-8'))
                    return ext_infos

                for i in range (0, ext_count):
                    ext = self.cert.get_extension(i)
                    if extension in str(ext.get_short_name()):
                        return ext.__str__().strip()
                return  None

            elif item == 'version':
                return self.cert.get_version()

            elif item == 'pubkey_type':
                pk_type = self.cert.get_pubkey().type()
                if pk_type == crypto.TYPE_RSA:
                    return 'RSA'
                elif pk_type == crypto.TYPE_DSA:
                    return 'DSA'
                return 'Unknown'

            elif item == 'pubkey_pem':
                return crypto.dump_publickey(crypto.FILETYPE_PEM, self.cert.get_pubkey()).decode('utf-8')

            elif item == 'serial_number':
                return self.cert.get_serial_number()

            elif item == 'not_before':
                not_before = self.cert.get_notBefore()
                if return_as == 'string':
                    return self.cert_date_to_gmt_date_string(not_before)
                return self.cert_date_to_gmt_date(not_before)

            elif item == 'not_after':
                not_after = self.cert.get_notAfter()
                if return_as == 'string':
                    return self.cert_date_to_gmt_date_string(not_after)
                return self.cert_date_to_gmt_date(not_after)

            elif item == 'has_expired':
                return self.cert.has_expired()

            elif item == 'issuer':
                return self.decode_x509name_obj(self.cert.get_issuer())

            elif item == 'issuer_o':
                return self.cert.get_issuer().O.strip()

            elif item == 'issuer_cn':
                return self.cert.get_issuer().CN.strip()

            elif item == 'signature_algorithm':
                return self.cert.get_signature_algorithm().decode('utf-8')

            elif item == 'digest':
                # ['md5', 'sha1', 'sha256', 'sha512']
                return self.cert.digest(algo)

            elif item == 'pem':
                return crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert).decode('utf-8')

            else:
                return  None

        except Exception as e:
            logger.error('item = {}, exception, e = {}'.format(item, e))
            return  None

    @property
    def subject(self):
        return self.get_item('subject')

    @property
    def subject_o(self):
        return self.get_item('subject_o')

    @property
    def subject_cn(self):
        return self.get_item('subject_cn')

    @property
    def subject_name_hash(self):
        return self.get_item('subject_name_hash')

    @property
    def extension_count(self):
        return self.get_item('extension_count')

    @property
    def extensions(self):
        return self.get_item('extensions')

    @property
    def extension_basic_constraints(self):
        return self.get_item('extensions', extension='basicConstraints')

    @property
    def extension_subject_key_identifier(self):
        return self.get_item('extensions', extension='subjectKeyIdentifier')

    @property
    def extension_authority_key_identifier(self):
        return self.get_item('extensions', extension='authorityKeyIdentifier')

    @property
    def extension_subject_alt_name(self):
        return self.get_item('extensions', extension='subjectAltName')

    @property
    def version(self):
        return self.get_item('version')

    @property
    def pubkey_type(self):
        return self.get_item('pubkey_type')

    @property
    def pubkey_pem(self):
        return self.get_item('pubkey_pem')

    @property
    def serial_number(self):
        return self.get_item('serial_number')

    @property
    def not_before(self):
        return self.get_item('not_before')

    @property
    def not_before_s(self):
        return self.get_item('not_before', return_as='string')

    @property
    def not_after(self):
        return self.get_item('not_after')

    @property
    def not_after_s(self):
        return self.get_item('not_after', return_as='string')

    @property
    def has_expired(self):
        return self.get_item('has_expired')

    @property
    def issuer(self):
        return self.get_item('issuer')

    @property
    def issuer_o(self):
        return self.get_item('issuer_o')

    @property
    def issuer_cn(self):
        return self.get_item('issuer_cn')

    @property
    def signature_algorithm(self):
        return self.get_item('signature_algorithm')

    @property
    def digest_sha256(self):
        return self.get_item('digest', algo='sha256')

    @property
    def pem(self):
        return self.get_item('pem')

No todas las propiedades del certificado están disponibles. Por ejemplo, no encontré una manera fácil de obtener la firma.

Para imprimir los elementos del certificado:

    def print_cert_items(self, cert_id, cert):
            
        def format_cert_items(m):
            return '{}: {}'.format(m[0], m[1])

        ci =  CertInfo(cert)

        cert_items = [
            ('Subject', ci.subject),
            ('Subject-CN', ci.subject_cn),
            ('Subject name hash', ci.subject_name_hash),
            ('Issuer', ci.issuer),
            ('Issuer-CN', ci.issuer_cn),
            ('Extensions', ci.extensions),
            ('Extension-basicConstraints', ci.extension_basic_constraints),
            ('Extension-subjectKeyIdentifier', ci.extension_subject_key_identifier),
            ('Extension-authorityKeyIdentifier', ci.extension_authority_key_identifier),
            ('Extension-subjectAltName (SAN)', ci.extension_subject_alt_name),
            ('Version', ci.version),
            ('Serial_number', ci.serial_number),
            ('Public key-type', ci.pubkey_type),
            ('Public key-pem', ci.pubkey_pem),
            ('Not before', ci.not_before_s),
            ('Not after', ci.not_after_s),
            ('Has expired', ci.has_expired),
            ('Signature algortihm', ci.signature_algorithm),
            ('Digest-sha256', ci.digest_sha256),
            ('PEM', ci.pem),
        ]

        print('{}'.format(cert_id))
        cert_item_lines = map(format_cert_items, cert_items)
        print('{}'.format('\n'.join(cert_item_lines)))

Resumen

Este fue un largo viaje. Estaba buscando una solución Python pyOpenSSL solamente para verificar la Cadena de Confianza sin subprocess y todo el tiempo me topé con páginas que me decían que esto no se podía hacer con pyOpenSSL solamente. Después de esto revisé el repositorio pyOpenSSL en Github.com y me dijo que esto había sido añadido muy recientemente. El changelog mostraba los parámetros y métodos y después de leer los documentos era fácil de implementar.

También encontré formas de comprobar la cadena de certificados, si un certificado es autofirmado, caducado y una forma de comprobar si hay un host erróneo. Ahora en la extensión de mi script de verificación del servidor ...

Enlaces / créditos

[Cryptography-dev] on how (not) to chain certs with openssl + pyopenssl
https://mail.badssl.com/pipermail/cryptography-dev/2016-August/000676.html

certvalidator
https://github.com/wbond/certvalidator

Chain of Fools: An Exploration of Certificate Chain Validation Mishaps
https://duo.com/labs/research/chain-of-fools

Cheat Sheet - OpenSSL
https://megamorf.gitlab.io/cheat-sheets/openssl/

Digital Certificates: Chain of Trust
https://www.youtube.com/watch?v=heacxYUnFHA

Get or build PEM certificate chain in Python
https://stackoverflow.com/questions/51039393/get-or-build-pem-certificate-chain-in-python

Get your certificate chain right
https://medium.com/@superseb/get-your-certificate-chain-right-4b117a9c0fce

How to know if certificate is self-signed
https://security.stackexchange.com/questions/93162/how-to-know-if-certificate-is-self-signed/93163

How to validate / verify an X509 Certificate chain of trust in Python?
https://stackoverflow.com/questions/30700348/how-to-validate-verify-an-x509-certificate-chain-of-trust-in-python

PyOpenSSL - how can I get SAN(Subject Alternative Names) list
https://stackoverflow.com/questions/49491732/pyopenssl-how-can-i-get-sansubject-alternative-names-list

ssl-check.py
https://gist.github.com/gdamjan/55a8b9eec6cf7b771f92021d93b87b2c

Use openssl to individually verify components of a certificate chain
https://security.stackexchange.com/questions/118062/use-openssl-to-individually-verify-components-of-a-certificate-chain

Verify a certificate chain using openssl verify
https://stackoverflow.com/questions/25482199/verify-a-certificate-chain-using-openssl-verify

X509StoreContext objects
https://www.pyopenssl.org/en/stable/api/crypto.html#x509storecontext-objects

Deje un comentario

Comente de forma anónima o inicie sesión para comentar.

Comentarios (2)

Deje una respuesta.

Responda de forma anónima o inicie sesión para responder.

avatar

Hello,
Very interesting. Could you share full script/class? It looks truncated.
Regards!

avatar

This example is not complete. So I re-iterate the previous comment. Please could you share the full script/class?