# -*- encoding: utf-8 -*- | |
# requires a recent enough python with idna support in socket | |
# pyopenssl, cryptography and idna | |
from OpenSSL import SSL | |
from cryptography import x509 | |
from cryptography.x509.oid import NameOID | |
import idna | |
from socket import socket | |
from collections import namedtuple | |
HostInfo = namedtuple(field_names='cert hostname peername', typename='HostInfo') | |
HOSTS = [ | |
('damjan.softver.org.mk', 443), | |
('expired.badssl.com', 443), | |
('wrong.host.badssl.com', 443), | |
('ca.ocsr.nl', 443), | |
('faß.de', 443), | |
('самодеј.мкд', 443), | |
] | |
def verify_cert(cert, hostname): | |
# verify notAfter/notBefore, CA trusted, servername/sni/hostname | |
cert.has_expired() | |
# service_identity.pyopenssl.verify_hostname(client_ssl, hostname) | |
# issuer | |
def get_certificate(hostname, port): | |
hostname_idna = idna.encode(hostname) | |
sock = socket() | |
sock.connect((hostname, port)) | |
peername = sock.getpeername() | |
ctx = SSL.Context(SSL.SSLv23_METHOD) # most compatible | |
ctx.check_hostname = False | |
ctx.verify_mode = SSL.VERIFY_NONE | |
sock_ssl = SSL.Connection(ctx, sock) | |
sock_ssl.set_connect_state() | |
sock_ssl.set_tlsext_host_name(hostname_idna) | |
sock_ssl.do_handshake() | |
cert = sock_ssl.get_peer_certificate() | |
crypto_cert = cert.to_cryptography() | |
sock_ssl.close() | |
sock.close() | |
return HostInfo(cert=crypto_cert, peername=peername, hostname=hostname) | |
def get_alt_names(cert): | |
try: | |
ext = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName) | |
return ext.value.get_values_for_type(x509.DNSName) | |
except x509.ExtensionNotFound: | |
return None | |
def get_common_name(cert): | |
try: | |
names = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME) | |
return names[0].value | |
except x509.ExtensionNotFound: | |
return None | |
def get_issuer(cert): | |
try: | |
names = cert.issuer.get_attributes_for_oid(NameOID.COMMON_NAME) | |
return names[0].value | |
except x509.ExtensionNotFound: | |
return None | |
def print_basic_info(hostinfo): | |
s = '''» {hostname} « … {peername} | |
\tcommonName: {commonname} | |
\tSAN: {SAN} | |
\tissuer: {issuer} | |
\tnotBefore: {notbefore} | |
\tnotAfter: {notafter} | |
'''.format( | |
hostname=hostinfo.hostname, | |
peername=hostinfo.peername, | |
commonname=get_common_name(hostinfo.cert), | |
SAN=get_alt_names(hostinfo.cert), | |
issuer=get_issuer(hostinfo.cert), | |
notbefore=hostinfo.cert.not_valid_before, | |
notafter=hostinfo.cert.not_valid_after | |
) | |
print(s) | |
def check_it_out(hostname, port): | |
hostinfo = get_certificate(hostname, port) | |
print_basic_info(hostinfo) | |
import concurrent.futures | |
if __name__ == '__main__': | |
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as e: | |
for hostinfo in e.map(lambda x: get_certificate(x[0], x[1]), HOSTS): | |
print_basic_info(hostinfo) |
Everything. Figured it out
names_o = cert.issuer.get_attributes_for_oid(NameOID.ORGANIZATION_NAME)
when I get a list of sites and one of them does not have a TLS certificate configured, it generates an error:
sock.connect((hostname, port))
BlockingIOError: [Errno 11] Resource temporarily unavailable
Would it be possible to convert the cert to a byte string or an instance of asn1crypto.x509.Certificate please?
Hi, the script is useful, but it's possible put these values: ('damjan.softver.org.mk', 443), ('expired.badssl.com', 443), ('wrong.host.badssl.com', 443), ('ca.ocsr.nl', 443), ('faß.de', 443), ('самодеј.мкд', 443), inside a text file? I mean exist an elegant way to read lines of file into list?
you can change HOSTS into this:
HOSTS = []
for line in hosts_file:
host, port = line.strip().split(':')
HOSTS.append((host, int(port)))
I really appreciate that you took the time to share this with the world... super useful !!! Thank you.
Thank you so much for being so helpful.
How to add the output of the organization that issued the certificate to the script. For example
(O) Let's Encrypt