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.
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
Leer más
Cryptography pyOpenSSL
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.
Hello,
Very interesting. Could you share full script/class? It looks truncated.
Regards!
This example is not complete. So I re-iterate the previous comment. Please could you share the full script/class?
Recientes
- Gráfico de series temporales con Flask, Bootstrap y Chart.js
- Utilización de IPv6 con Microk8s
- Uso de Ingress para acceder a RabbitMQ en un clúster Microk8s
- Galería de vídeo simple con Flask, Jinja, Bootstrap y JQuery
- Programación básica de trabajos con APScheduler
- Un conmutador de base de datos con HAProxy y el HAProxy Runtime API
Más vistos
- Usando PyInstaller y Cython para crear un ejecutable de Python
- Reducir los tiempos de respuesta de las páginas de un sitio Flask SQLAlchemy web
- Usando Python's pyOpenSSL para verificar los certificados SSL descargados de un host
- Conectarse a un servicio en un host Docker desde un contenedor Docker
- Usando UUIDs en lugar de Integer Autoincrement Primary Keys con SQLAlchemy y MariaDb
- SQLAlchemy: Uso de Cascade Deletes para eliminar objetos relacionados