Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Python script to check on SSL certificates
# -*- 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)
@inquisitivefrog

This comment has been minimized.

Copy link

@inquisitivefrog inquisitivefrog commented Mar 21, 2019

This helped me out of a jam. thank you.

@kevyteky

This comment has been minimized.

Copy link

@kevyteky kevyteky commented Jul 19, 2019

Appreciate all your efforts! Thanks!

@redmine-cosi

This comment has been minimized.

Copy link

@redmine-cosi redmine-cosi commented Aug 18, 2019

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?

@gdamjan

This comment has been minimized.

Copy link
Owner Author

@gdamjan gdamjan commented Aug 18, 2019

@redmine-cosi:

sure, why not. open a file, read line by line, and split on : or empty space, or anything. maybe use csv module?

@redmine-cosi

This comment has been minimized.

Copy link

@redmine-cosi redmine-cosi commented Aug 18, 2019

Uhm, Things are really complicated for me right now :( I tried so:
...
with open("cert-list.csv") as filecsv:
HOSTS = csv.reader(filecsv, delimiter=',')
for row, line in enumerate(HOSTS):
a = '{}'.format(line)
print(a)
print (type(line))
print (type(row))
...
my .csv file:
pd.trust.it, 443
dev.scinetwork.it, 443
cs.fibc.it, 443
ouput:
['pd.trust.it', ' 443']
<class 'list'>
<class 'int'>
['dev.scinetwork.it', ' 443']
<class 'list'>
<class 'int'>
['cs.fibc.it', ' 443']
<class 'list'>
<class 'int'>
Traceback (most recent call last):
File "cert-ssl-check.py", line 100, in
for hostinfo in e.map(lambda x: get_certificate(x[0], x[1]), HOSTS):
File "/usr/lib/python3.5/concurrent/futures/_base.py", line 548, in map
fs = [self.submit(fn, *args) for args in zip(*iterables)]
File "/usr/lib/python3.5/concurrent/futures/_base.py", line 548, in
fs = [self.submit(fn, *args) for args in zip(*iterables)]
ValueError: I/O operation on closed file.

@gdamjan

This comment has been minimized.

Copy link
Owner Author

@gdamjan gdamjan commented Aug 19, 2019

I don't think gist comments are the best place to learn python

@wiperpaul

This comment has been minimized.

Copy link

@wiperpaul wiperpaul commented Sep 4, 2019

Are you able to distinguish the certificate type from this info e.g. ('OV SSL', 'EV SSL', 'DV SSL') ?

@gdamjan

This comment has been minimized.

Copy link
Owner Author

@gdamjan gdamjan commented Sep 4, 2019

Are you able to distinguish the certificate type from this info e.g. ('OV SSL', 'EV SSL', 'DV SSL') ?

afaik yes, if you give me an example site with those attributes I can take a look how to extract those from the cert info

@wiperpaul

This comment has been minimized.

Copy link

@wiperpaul wiperpaul commented Sep 5, 2019

For example PayPal.com has an expensive EV(Extended Validation) SSL cert but the only indication I've been able to find is occasionally sites have 'Extended Validation Server' in their CN field.

This isn't standard either, some just have a name like 'Google Trust Services' like for Google.com.

Edit - I just found this post clearing some of this up for me, https://security.stackexchange.com/questions/88721/how-to-detect-whether-a-ca-used-ev-for-a-certificate-using-openssl

@gdamjan

This comment has been minimized.

Copy link
Owner Author

@gdamjan gdamjan commented Sep 5, 2019

https://en.wikipedia.org/wiki/Extended_Validation_Certificate

EV certificates are different from domain-validated certificates and organization-validation certificates in that they can be issued only by a subset of certificate authorities (CAs) and require verification of the requesting entity's legal identity before certificate issuance.

so you'll need to have a list of the CAs

@NamanBharti

This comment has been minimized.

Copy link

@NamanBharti NamanBharti commented Feb 20, 2020

Hi gdamjan,

I am trying to get the following information from a certificate through python:

  • Issuer Name

  • Valid From

  • Valid To

  • Site Name

And I was trying your script only to get the following error:

AttributeError: 'X509' object has no attribute 'issuer'

Please Help
Thanks and Regards.

@gdamjan

This comment has been minimized.

Copy link
Owner Author

@gdamjan gdamjan commented Feb 20, 2020

those are all in the script above

@NamanBharti

This comment has been minimized.

Copy link

@NamanBharti NamanBharti commented Feb 20, 2020

Can you help regarding this issue also:
AttributeError: 'X509' object has no attribute 'issuer'
Thanks

@simon-wessel

This comment has been minimized.

Copy link

@simon-wessel simon-wessel commented Jul 9, 2020

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
@gdamjan

This comment has been minimized.

Copy link
Owner Author

@gdamjan gdamjan commented Jul 9, 2020

@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)
@jwkersey

This comment has been minimized.

Copy link

@jwkersey jwkersey commented Sep 13, 2020

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.

@altafparkar

This comment has been minimized.

Copy link

@altafparkar altafparkar commented Sep 22, 2020

How do we get the results in json format?

@gdamjan

This comment has been minimized.

Copy link
Owner Author

@gdamjan gdamjan commented Sep 22, 2020

How do we get the results in json format?

json.dumps(host._as_dict())

@altafparkar

This comment has been minimized.

Copy link

@altafparkar altafparkar commented Sep 22, 2020

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. :)

@gdamjan

This comment has been minimized.

Copy link
Owner Author

@gdamjan gdamjan commented Sep 23, 2020

._asdict()

@daviddemers

This comment has been minimized.

Copy link

@daviddemers daviddemers commented Dec 21, 2020

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?

@alfonsrv

This comment has been minimized.

Copy link

@alfonsrv alfonsrv commented Mar 13, 2021

Insanely dope script. namedtuple just blew my mind.
Any idea on how to verify the chain?

@tomtrkd

This comment has been minimized.

Copy link

@tomtrkd tomtrkd commented Jun 23, 2021

I am having problems with the verify_cert option is this working correctly and is there a way to check a self signed certificate?

@skk294

This comment has been minimized.

Copy link

@skk294 skk294 commented Aug 4, 2021

nice, saved my day

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment