2 import subprocess, sys, argparse, time, re
3 from collections import OrderedDict, namedtuple
6 # progress bar and other console output fun
10 import fcntl, termios, struct
12 result = fcntl.ioctl(1, termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0))
14 # this is not a terminal
16 h, w, hp, wp = struct.unpack('HHHH', result)
17 assert w > 0 and h > 0, "Empty terminal...??"
20 def compute_frac(fracs):
23 for complete, total in fracs:
24 frac += (complete*last_frac/total)
28 def print_progress(state, fracs):
29 w, h = terminal_size()
30 if w < STATE_WIDTH+10: return # not a (wide enough) terminal
31 bar_width = w-STATE_WIDTH-3
32 hashes = int(bar_width*compute_frac(fracs))
33 sys.stdout.write('\r{0} [{1}{2}]'.format(state[:STATE_WIDTH].ljust(STATE_WIDTH), '#'*hashes, ' '*(bar_width-hashes)))
35 def finish_progress():
36 w, h = terminal_size()
37 sys.stdout.write('\r'+(' '*w)+'\r')
41 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
43 COLOR_SEQ = "\033[1;3%dm"
47 def color(text, color):
48 return (ConsoleFormat.COLOR_SEQ % color) + text + ConsoleFormat.RESET_SEQ
52 def list_ciphers(spec="ALL:COMPLEMENTOFALL"):
53 ciphers = subprocess.check_output(["openssl", "ciphers", spec]).decode('UTF-8').strip()
54 return ciphers.split(':')
56 def test_cipher(host, port, protocol, cipher = None, wait_time=0, options=[]):
58 time.sleep(wait_time/1000)
60 if cipher is not None:
61 options = ["-cipher", cipher]+options
62 subprocess.check_call(["openssl", "s_client", "-"+protocol, "-connect", host+":"+str(port), "-servername", host]+options,
63 stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
64 except subprocess.CalledProcessError:
69 def test_protocol(host, port, protocol, ciphers, base_frac, wait_time=0, options=[]):
70 if test_cipher(host, port, protocol, wait_time=wait_time, options=options):
71 # the protocol is supported
72 results = OrderedDict()
73 for i in range(len(ciphers)):
75 print_progress(protocol+" "+cipher, base_frac+[(i, len(ciphers))])
76 results[cipher] = test_cipher(host, port, protocol, cipher=cipher, wait_time=wait_time, options=options)
82 def test_host(host, port, wait_time=0, options=[]):
83 ciphers = list_ciphers()
84 results = OrderedDict()
85 protocols = ('ssl2', 'ssl3', 'tls1', 'tls1_1', 'tls1_2')
86 for i in range(len(protocols)):
87 protocol = protocols[i]
88 print_progress(protocol, [(i, len(protocols))])
89 results[protocol] = test_protocol(host, port, protocol, ciphers, [(i, len(protocols))], wait_time, options)
93 # cipher classification
94 class CipherStrength(Enum):
99 if self.value == CipherStrength.high.value:
100 return ConsoleFormat.color(self.name, ConsoleFormat.GREEN)
102 return ConsoleFormat.color(self.name, ConsoleFormat.YELLOW)
104 CipherProps = namedtuple('CipherProps', 'bits, strength, isPfs')
106 class CipherPropsProvider:
108 self.high = set(list_ciphers("HIGH"))
111 def getProps(self, protocol, cipher):
112 # strip the sub-version-number from the protocol
113 pos = protocol.find('_')
115 protocol = protocol[:pos]
116 # as OpenSSL about this cipher
117 cipherInfo = subprocess.check_output(["openssl", "ciphers", "-v", "-"+protocol, cipher]).decode('UTF-8').strip()
118 cipherInfoFields = None
119 for line in cipherInfo.split('\n'):
121 if line[0] == cipher:
122 cipherInfoFields = line
124 if cipherInfoFields is None:
125 raise Exception("Cannot determine cipher properties of {0} (protocol: {1})".format(cipher, protocol))
127 encMatch = re.match(r'^Enc=([0-9A-Za-z]+)\(([0-9]+)\)$', cipherInfoFields[4])
129 raise Exception("Unexpected OpenSSL output: Cannot determine encryption strength from {1}\nComplete output: {0}".format(cipherInfo, cipherInfoFields[4]))
130 encCipher = encMatch.group(1)
131 bits = int(encMatch.group(2))
132 if encCipher == '3DES':
133 # OpenSSL gives the key size, which however for 3DES is a totally bad estimate
135 # figure out whether the cipher is pfs
136 kxMatch = re.match(r'^Kx=([0-9A-Z/()]+)$', cipherInfoFields[2])
138 raise Exception("Unexpected OpenSSL output: Cannot determine key-exchange method from {1}\nComplete output: {0}".format(cipherInfo, cipherInfoFields[2]))
139 kx = kxMatch.group(1)
140 isPfs = kx in ('DH', 'DH(512)', 'ECDH')
141 # determine security level
142 if cipher in self.high:
143 strength = CipherStrength.high
145 strength = CipherStrength.unknown
147 return CipherProps(bits=bits, strength=strength, isPfs=isPfs)
150 if __name__ == "__main__":
151 parser = argparse.ArgumentParser(description='Check TLS ciphers supported by a host')
152 parser.add_argument("--starttls", dest="starttls",
153 help="Use a STARTTLS variant to establish the TLS connection. Possible values include smtp, imap.")
154 parser.add_argument("--wait-time", "-t", dest="wait_time", default="10",
155 help="Time (in ms) to wait between two connections to the server. Default is 10ms.")
156 parser.add_argument("host", metavar='HOST[:PORT]',
157 help="The host to check")
158 args = parser.parse_args()
162 host, port = args.host.split(':')
167 # get options and other stuff
168 wait_time = float(args.wait_time)
170 if args.starttls is not None:
171 options += ['-starttls', args.starttls]
174 results = test_host(host, port, wait_time, options)
177 propsProvider = CipherPropsProvider()
178 for protocol, ciphers in results.items():
181 print(" Is not supported by client or server")
183 for cipher, supported in ciphers.items():
185 cipherProps = propsProvider.getProps(protocol, cipher)
186 fsText = ConsoleFormat.color("FS", ConsoleFormat.GREEN) if cipherProps.isPfs else ConsoleFormat.color("no FS", ConsoleFormat.RED)
187 bitColor = ConsoleFormat.GREEN if cipherProps.bits >= 128 else (ConsoleFormat.YELLOW if cipherProps.bits >= 100 else ConsoleFormat.RED)
188 print(" {0} ({1}, {2}, {3})".format(cipher.ljust(STATE_WIDTH), cipherProps.strength.colorName(), ConsoleFormat.color(str(cipherProps.bits)+" bits", bitColor), fsText))