X-Git-Url: https://git.ralfj.de/tls-check.git/blobdiff_plain/ac0e7156a0d16de49f07c4d56c988c52393e2ecc..c24ab0b379d16957df05cd2f32ed7271b5d11fd6:/tls-check?ds=sidebyside diff --git a/tls-check b/tls-check index 9d84f42..27651d5 100755 --- a/tls-check +++ b/tls-check @@ -1,6 +1,6 @@ #!/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 @@ -35,10 +35,11 @@ def finish_progress(): 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=[]): @@ -52,7 +53,7 @@ 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() @@ -60,26 +61,90 @@ def test_protocol(host, port, protocol, ciphers, base_frac, options=[]): 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() @@ -91,15 +156,17 @@ if __name__ == "__main__": 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: @@ -107,5 +174,6 @@ if __name__ == "__main__": 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()