# -*- 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) |
Are you able to distinguish the certificate type from this info e.g. ('OV SSL', 'EV SSL', 'DV SSL') ?
You may go through the extensions (certificate.get_extension(i)
) and search for the EV policy id:
2.23.140.1.2.1 DV
2.23.140.1.2.2 OV
2.23.140.1.1 EV
@simon-wessel cool.
So, def get_certificate(hostname, port):
returns the HostInfo object. Its .cert
field has the .extensions
list.
An example:
def cert_type(cert):
for ext in cert.extensions:
if ext.oid.dotted_string == '2.23.140.1.2.1':
return 'DV type'
if ext.oid.dotted_string == '2.23.140.1.2.2':
return 'OV type'
if ext.oid.dotted_string == '2.23.140.1.1':
return 'EV' type
return 'Normal certificate type'
host = get_certificate('example.net')
cert_type(host.cert)
This works great at a basic level, I'm new with python, I struggled a bit figuring out where to run PIP from to get pyopenssl and the other dependencies installed to run it , but it was easy if when you install python, you get pip installed from the beginning , obviously you set the path variable, then you just use pip from the windows CMD and it works. I would like this code to have a separate alerts field in the output for certs expiring in 60 days I would also like to figure out getting email built into it.
How do we get the results in json format?
How do we get the results in json format?
json.dumps(host._as_dict())
How do we get the results in json format?
json.dumps(host._as_dict())
Thanks - but this is what I get
Code:
host = get_certificate('google.co.nz', 443)
print(host)
json.dumps(host._as_dict())
Error:
HostInfo(cert=<Certificate(subject=<Name(C=US,ST=California,L=Mountain View,O=Google LLC,CN=*.google.co.nz)>, ...)>, hostname='google.co.nz', peername=('142.250.66.195', 443))
Traceback (most recent call last):
File ".\ssl-check.py", line 110, in
print_basic_info(hostinfo)
File ".\ssl-check.py", line 91, in print_basic_info
json.dumps(host._as_dict())
AttributeError: 'HostInfo' object has no attribute '_as_dict'
I am able to build the json manually but am thinking of a cooler option. :)
._asdict()
I'm running into a suspected firewall issue when I attempt to access few hosts, and the script is failing and erroring out with:
Traceback (most recent call last):
File "/usr/local/adm/checkcerts.py", line 53, in get_certificate
sock.connect((hostname, port))
TimeoutError: [Errno 110] Connection timed out
For the specific hosts in my issue, I intend to address the issue with my networking team, but does anyone have a good suggestion for handling that error in the script?
Insanely dope script. namedtuple
just blew my mind.
Any idea on how to verify the chain?
I am having problems with the verify_cert option is this working correctly and is there a way to check a self signed certificate?
How to add the output of the organization that issued the certificate to the script. For example
(O) Let's Encrypt
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.
Can you help regarding this issue also:
AttributeError: 'X509' object has no attribute 'issuer'
Thanks