#!/usr/bin/python3
-import subprocess, sys, argparse
-from collections import OrderedDict
+import subprocess, sys, argparse, time, re
+from collections import OrderedDict, namedtuple
from enum import Enum
# progress bar
w, h = terminal_size()
sys.stdout.write('\r'+(' '*w)+'\r')
sys.stdout.flush()
+
# cipher check
-def list_ciphers():
- ciphers = subprocess.check_output(["openssl", "ciphers", "ALL:COMPLEMENTOFALL"]).decode('UTF-8').strip()
+def list_ciphers(spec="ALL:COMPLEMENTOFALL"):
+ ciphers = subprocess.check_output(["openssl", "ciphers", spec]).decode('UTF-8').strip()
return ciphers.split(':')
def test_cipher(host, port, protocol, cipher = None, options=[]):
else:
return True
-def test_protocol(host, port, protocol, ciphers, base_frac, options=[]):
+def test_protocol(host, port, protocol, ciphers, base_frac, wait_time=0, options=[]):
if test_cipher(host, port, protocol, options=options):
# the protocol is supported
results = OrderedDict()
cipher = ciphers[i]
print_progress(protocol+" "+cipher, base_frac+[(i, len(ciphers))])
results[cipher] = test_cipher(host, port, protocol, cipher, options)
+ # throttle
+ time.sleep(wait_time/1000)
return results
else:
# it is not supported
return None
-def test_host(host, port, options=[]):
+def test_host(host, port, wait_time=0, options=[]):
ciphers = list_ciphers()
results = OrderedDict()
protocols = ('ssl2', 'ssl3', 'tls1', 'tls1_1', 'tls1_2')
for i in range(len(protocols)):
protocol = protocols[i]
print_progress(protocol, [(i, len(protocols))])
- results[protocol] = test_protocol(host, port, protocol, ciphers, [(i, len(protocols))], options)
+ results[protocol] = test_protocol(host, port, protocol, ciphers, [(i, len(protocols))], wait_time, options)
finish_progress()
return results
+# cipher classification
+class CipherStrength(Enum):
+ unknown = -1
+ exp = 0
+ low = 1
+ medium = 2
+ high = 3
+CipherProps = namedtuple('CipherProps', 'bits, strength, isPfs')
+
+class CipherPropsProvider:
+ def __init__(self):
+ self.exp = set(list_ciphers("EXP"))
+ self.low = set(list_ciphers("LOW"))
+ self.medium = set(list_ciphers("MEDIUM"))
+ self.high = set(list_ciphers("HIGH"))
+ self.props = {}
+
+ def __getProps(self, cipher):
+ # as OpenSSL about this cipher
+ cipherInfo = subprocess.check_output(["openssl", "ciphers", "-v", cipher]).decode('UTF-8').strip()
+ assert '\n' not in cipherInfo
+ cipherInfoFields = cipherInfo.split()
+ # get # of bits
+ bitMatch = re.match(r'^Enc=[0-9A-Za-z]+\(([0-9]+)\)$', cipherInfoFields[4])
+ if bitMatch is None:
+ raise Exception("Unexpected OpenSSL output: Cannot determine encryption strength from {1}\nComplete output: {0}".format(cipherInfo, cipherInfoFields[4]))
+ bits = int(bitMatch.group(1))
+ # figure out whether the cipher is pfs
+ kxMatch = re.match(r'^Kx=([0-9A-Z/]+)$', cipherInfoFields[2])
+ if kxMatch is None:
+ raise Exception("Unexpected OpenSSL output: Cannot determine key-exchange method from {1}\nComplete output: {0}".format(cipherInfo), cipherInfoFields[2])
+ kx = kxMatch.group(1)
+ isPfs = kx in ('DH', 'ECDH')
+ # determine security level
+ isExp = cipher in self.exp
+ isLow = cipher in self.low
+ isMedium = cipher in self.medium
+ isHigh = cipher in self.high
+ assert isExp+isLow+isMedium+isHigh <= 1, "Cipher is more than one from EXP, LOW, MEDIUM, HIGH"
+ if isExp:
+ strength = CipherStrength.exp
+ elif isLow:
+ strength = CipherStrength.low
+ elif isMedium:
+ strength = CipherStrength.medium
+ elif isHigh:
+ strength = CipherStrength.high
+ else:
+ strength = CipherStrength.unknown
+ # done!
+ return CipherProps(bits=bits, strength=strength, isPfs=isPfs)
+
+ def getProps(self, cipher):
+ if cipher in self.props:
+ return self.props[cipher]
+ props = self.__getProps(cipher)
+ self.props[cipher] = props
+ return props
+
+# main program
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Check TLS ciphers supported by a host')
parser.add_argument("--starttls", dest="starttls",
help="Use a STARTTLS variant to establish the TLS connection. Possible values include smpt, imap, xmpp.")
+ parser.add_argument("--wait-time", "-t", dest="wait_time", default="10",
+ help="Time (in ms) to wait between two connections to the server. Default is 10ms.")
parser.add_argument("host", metavar='HOST[:PORT]',
help="The host to check")
args = parser.parse_args()
host = args.host
port = 443
- # get options
+ # get options and other stuff
+ wait_time = float(args.wait_time)
options = []
if args.starttls is not None:
options += ['-starttls', args.starttls]
# run the test
- results = test_host(host, port, options)
+ results = test_host(host, port, wait_time, options)
# print the results
+ propsProvider = CipherPropsProvider()
for protocol, ciphers in results.items():
print(protocol+":")
if ciphers is None:
else:
for cipher, supported in ciphers.items():
if supported:
- print(" "+cipher)
+ cipherProps = propsProvider.getProps(cipher)
+ print(" {0} ({1}, {2} bits, {3})".format(cipher, cipherProps.strength.name, cipherProps.bits, "FS" if cipherProps.isPfs else "not FS"))
print()