Verify SSL Expiration with Python 3
If you’ve ever had a need to verify multiple SSL certificates for expiration times in a batch and wanted to script it in Python, you’ll find this article interesting.
When I try to solve a problem, programatically, I usually start with the “what makes sense” question. In this case, the SSL cert main problem was not knowing when a site expired. With that said, there are several other things that are important to know:
- HTTP Response Codes
- Known text on the URI
- Which Cipher Suites are Used on the SSL?
- What version of SSL/TLS
- What is the key strength of the cipher in use
- SSL Serials
- Domain Matching
- Other tests such as Beast vulnerability
- Is TLS compression in use? (CRIME vulnerability)
- Do I have Session Ticket Support?
- Do I have Ephemeral Key Support?
And the list goes on! As new vulnerabilities come up you need to know things about the certs you’ve chosen to use on your servers, and the clients you connect with.
Filter On Important/Wanted Info
As you can see there are many reasons you should review your SSL certs. For this script, I started with just a few:
- TLS Version
- Reason: Vulnerability attacks such as Beast on version 1.0
- SSl x.509 Version
- Reason: 1, 2, or 3 with 3 being the preferred
- Expiration Date
- Reason: For simple SSL cert management I need to know when to renew my certs!
- Cipher Suite
- Reason: Currently RSA 256*8 (2048) length is probably ok, but knowing the length and cipher suite is important to know what needs to be upgraded when the next attack of the day proves a cipher is no longer strong enough. There are several weak ciphers and both the client and server must agree on one. This shows my python client at least.
Tools such as SSLyze, sslcaudit and tlssled (all in the Kali toolset and available on git), show this type of information too. However, I wanted to send warning emails for certificate renewals and provide reports across an enterprise environment (and learn more about SSL/TLS in general).
Sample Data to Scan SSL
First, we need sample data and a list of servers to check. This could easily be made much larger (thousands of sites), but for our demo/tutorial purposes we will use the following 4 sites. This type of data structure is a dictionary {} of lists []. YOu can see that I also added a key (1000-1003) as I intend to eventually track these records in mysql using pymsql. The “200” you see is the expected http status code field, but I didn’t actually do anything with it in the script.
1 2 3 4 5 6 |
{ "1000": ["howsmyssl.com", 443, 200, "https://", "/", "N/A"], "1001": ["www.google.com", 443, 200, "https://", "/", "N/A"], "1002": ["wellsfargo.com", 443, 200, "https://", "/", "N/A"], "1003": ["rubysash.com", 443, 200, "https://", "/", "N/A"] } |
Script to Test SSL Expirations
The final version of this script will send email to alert, store periodic checks for DNS load times, TCP handshake times, and page load times as well as scanning the page for specific text to make sure the page is up. In this way we can monitor thousands of sites. Being proactive when issues appear, before they are issues is always the best way!
You will also notice that this python script does the SSL scans fast. The first version was single threaded. We had to wait on DNS look ups, TCP Handshake, etc 1 at a time. I rewrote it to use threading and now it’s very fast. It will scan hundreds of sites in just a few seconds.
Anyway, here is the script as-is. It is simple enough to learn from, but is missing some of the features that I will be putting into it over the next few days/weeks:
- Add Email Alert Function
- Add Text on URI verification
- Store in MySQL
- Finally – run from Cron
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 |
''' ssl tester 2/08/20 Main purpose was to verify my cert bot expiry times so I could have plenty of time to renew them. I'm now thinking of what I want to look at across the board on all of my servers. Ideas welcome! todo: - add email notification warning if too close to expire - look at poodle or other known vulns - add in tcp handshake time - add in dns time to monitor dns requests - add in total page load time - check page text for known and/or fingerprint - warn on status code alerts like 500 or !200 - pull and id server version? - fix up the ctrl c thing. It works but not like I intended - put entire thing in tkinter so others can more easily use it - store into over time graph on a sql type database topics learned/refreshed: modules ctrl+c without error threading benchmarking dictionary of list data structure loops definitions time formatting time deltas ssl and https basic checks json read/write ''' #https://docs.python.org/3/library/ssl.html from urllib.request import Request, urlopen, ssl, socket from urllib.error import URLError, HTTPError # for dumping our sites dictionary to json import json # for getting response codes from header import urllib.request # for date calcs from datetime import datetime # for proper ctrl + c captures from signal import signal, SIGINT from sys import exit # actual code start time import time startTime = time.time() import threading # we want to multi thread this from queue import Queue # and have queue management ''' Sample output (single threaded 40~ sites) Running. Press CTRL+C to exit. ... Run Time: 42.77 seconds Sample output (multi threaded 40~ sites) Running. Press CTRL+C to exit. ... Run Time: 10.23 seconds (DNS fresh) Run Time: 4.03 seconds (DNS cached) sites = { "1000": ["howsmyssl.com",443,200,"https://","/","N/A"], "1001": ["www.google.com",443,200,"https://","/","N/A"], "1002": ["wellsfargo.com",443,200,"https://","/","N/A"], "1003": ["rubysash.com",443,200,"https://","/","N/A" ] } # write it out to json so I can just run it from a json load later print("Writing 'sites.json' file...") with open ('sites.json', 'w') as outfile: json.dump(sites, outfile, indent=2) # after I write out my sample, I can write the rest or manually add # todo: create function to add sites to data easily ''' ''' # threader thread pulls worker from queue and processes ''' def threader(): while True: # gets worker from queue worker = q.get() # run job with available worker in queue (thread) getSSLInfo(worker) # complete with the job, shut down thread q.task_done() ''' This was supposed to do a clean exit on ctrl+c but instead It requires a few ctrl+c to work. It doesnt' error now at least when ctrl+c is pressed. ''' def handler(signal_received, frame): # Handle any cleanup here print('SIGINT or CTRL-C detected. Exiting gracefully') exit(0) ''' Just an http status code return from uri input todo: strip off the new lines ''' def getResponseCode(uri): conn = urllib.request.urlopen(uri) return conn.getcode() ''' Pulls some info from ssl: expiry, serial, ssl version, bits, cipher, Expire time, domain given (not verified), port checked, response code expiry is a negative countdown, or in the case of http, just a 1 (see below) Running. Press CTRL+C to exit. 1000 TLSv1.2 3 -82 May 4 02:04:04 2020 GMT 256 ECDHE-RSA-CHACHA20-POLY1305 howsmyssl.com:443 200 1001 TLSv1.2 3 -62 Apr 14 08:16:35 2020 GMT 256 ECDHE-ECDSA-CHACHA20-POLY1305 www.google.com:443 200 1002 TLSv1.2 3 -58 Apr 9 12:00:00 2020 GMT 256 ECDHE-RSA-AES256-GCM-SHA384 wellsfargo.com:443 200 1003 TLSv1.2 3 -103 May 24 23:59:59 2020 GMT 256 ECDHE-RSA-AES256-GCM-SHA384 rubysash.com:443 200 ''' def getSSLInfo(kid): host = str(sites[kid][0]) # rubysash.com port = str(sites[kid][1]) # 80 proto = str(sites[kid][3]) # http:// or https:// path = str(sites[kid][4]) # / or /somepath.php text = str(sites[kid][5]) # text to validate on the site # It's either going to be http or https # if it's http, we just put place holders for now checkthis = proto + host + ":" + port + path if (proto == 'http://'): nd[kid] = [1,0,0,0,0,0,host + ":" + str(port),str(getResponseCode(checkthis))] else: try: # if it's https, this will work, or should work context = ssl.create_default_context() with socket.create_connection((host, port)) as sock: with context.wrap_socket(sock, server_hostname=host) as ssock: # get the ssl/tls info: d = ssock.getpeercert() # dumps a 3 part list: encryption, version, bits cipherinfo = ssock.cipher() # time from input dt1 = datetime.strptime(d['notAfter'], '%b %d %H:%M:%S %Y GMT') # time difference timediff = dt2 - dt1 #print(str(timediff.days) + ",", end='') #print(d['serialNumber'] + ",", end='') # 56E41941EC60E545555 #print(str(d['version']) + ",", end='') # 3 #print(ssock.version() + ",", end='') # TLSv1.2 (or SSLv2, SSLv3, TLSv1, TLSv1.1) #print(str(cipherinfo[2]) + ",", end='') # 256 (x8 for 2048 RSA key) #print(cipherinfo[0] + ",", end='') # ECDHE-RSA-AES256-GCM-SHA384 #print(d['notAfter'] + ",", end='') # May 24 23:59:59 2020 GMT #print(host + ":" + str(port) + ",", end='') # rubysash:443 #print(getResponseCode(checkthis)) nd[kid] = [ssock.version(),d['version'],str(timediff.days),d['notAfter'],cipherinfo[2],cipherinfo[0],host + ":" + str(port),str(getResponseCode(checkthis))] except: # whoops, it's probably not https. Do something better here nd[kid] = [1,0,0,0,0,0,host + ":" + str(port),str(getResponseCode(checkthis))] # get the current time for expiry time calculations # current time is only needed once at start now = datetime.now() dt2 = datetime.strptime(now.strftime("%Y-%m-%d %H:%M:%S"), '%Y-%m-%d %H:%M:%S') # loop over our dictionary of lists # Tell Python to run the handler() function when SIGINT is recieved signal(SIGINT, handler) # preload from sites.json file instead of data in script with open('sites.json', 'r') as infile: # put it into a dictionary called "data" sites = json.load(infile) # we have to load this guy up quickly then print out when it's complete # the with print_lock slows things down too much nd = {} # run the program inside a ctrl + c check print('Running. Press CTRL+C to exit.') while True: # create queue and threader q = Queue() for x in range(200): # thread id t = threading.Thread(target = threader) # classifying as a daemon, so they will die when the main dies t.daemon = True # begins, must come after daemon definition t.start() # this is the range or variable passed to the worker pool # we are loading up the thread pool here # work is done in the threader, not here for worker in (sites.keys()): q.put(worker) # wait until thrad terminates, then reassemble q.join() # now that we built this dictionary threaded, spit it out in single thread # this allows a sort and seemed the fastest way instead of print locking # todo: I'm sure this isn't the pythonic way to make a CSV! for kid in sorted (nd.keys()): print(str(kid) + "\t" + str(nd[kid][0]) + "\t" + str(nd[kid][1]) + "\t" + str(nd[kid][2]) + "\t" + str(nd[kid][3]) + "\t" + str(nd[kid][4]) + "\t" + str(nd[kid][5]) + "\t" + str(nd[kid][6]) + "\t" + str(nd[kid][7])) # ok, give us a final time report runtime = float("%0.2f" % (time.time() - startTime)) print("Run Time: ", runtime, "seconds") # end of ctrl + c check too exit(0) |