2 import subprocess, sys, argparse, time, re
3 from collections import OrderedDict, namedtuple
8 import fcntl, termios, struct
10 result = fcntl.ioctl(1, termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0))
12 # this is not a terminal
14 h, w, hp, wp = struct.unpack('HHHH', result)
15 assert w > 0 and h > 0, "Empty terminal...??"
18 def compute_frac(fracs):
21 for complete, total in fracs:
22 frac += (complete*last_frac/total)
26 def print_progress(state, fracs):
28 w, h = terminal_size()
29 if w < STATE_WIDTH+10: return # not a (wide enough) terminal
30 bar_width = w-STATE_WIDTH-3
31 hashes = int(bar_width*compute_frac(fracs))
32 sys.stdout.write('\r{0} [{1}{2}]'.format(state[:STATE_WIDTH].ljust(STATE_WIDTH), '#'*hashes, ' '*(bar_width-hashes)))
34 def finish_progress():
35 w, h = terminal_size()
36 sys.stdout.write('\r'+(' '*w)+'\r')
41 def list_ciphers(spec="ALL:COMPLEMENTOFALL"):
42 ciphers = subprocess.check_output(["openssl", "ciphers", spec]).decode('UTF-8').strip()
43 return ciphers.split(':')
45 def test_cipher(host, port, protocol, cipher = None, options=[]):
47 if cipher is not None:
48 options = ["-cipher", cipher]+options
49 subprocess.check_call(["openssl", "s_client", "-"+protocol, "-connect", host+":"+str(port)]+options,
50 stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
51 except subprocess.CalledProcessError:
56 def test_protocol(host, port, protocol, ciphers, base_frac, wait_time=0, options=[]):
57 if test_cipher(host, port, protocol, options=options):
58 # the protocol is supported
59 results = OrderedDict()
60 for i in range(len(ciphers)):
62 print_progress(protocol+" "+cipher, base_frac+[(i, len(ciphers))])
63 results[cipher] = test_cipher(host, port, protocol, cipher, options)
65 time.sleep(wait_time/1000)
71 def test_host(host, port, wait_time=0, options=[]):
72 ciphers = list_ciphers()
73 results = OrderedDict()
74 protocols = ('ssl2', 'ssl3', 'tls1', 'tls1_1', 'tls1_2')
75 for i in range(len(protocols)):
76 protocol = protocols[i]
77 print_progress(protocol, [(i, len(protocols))])
78 results[protocol] = test_protocol(host, port, protocol, ciphers, [(i, len(protocols))], wait_time, options)
82 # cipher classification
83 class CipherStrength(Enum):
89 CipherProps = namedtuple('CipherProps', 'bits, strength, isPfs')
91 class CipherPropsProvider:
93 self.exp = set(list_ciphers("EXP"))
94 self.low = set(list_ciphers("LOW"))
95 self.medium = set(list_ciphers("MEDIUM"))
96 self.high = set(list_ciphers("HIGH"))
99 def __getProps(self, cipher):
100 # as OpenSSL about this cipher
101 cipherInfo = subprocess.check_output(["openssl", "ciphers", "-v", cipher]).decode('UTF-8').strip()
102 assert '\n' not in cipherInfo
103 cipherInfoFields = cipherInfo.split()
105 bitMatch = re.match(r'^Enc=[0-9A-Za-z]+\(([0-9]+)\)$', cipherInfoFields[4])
107 raise Exception("Unexpected OpenSSL output: Cannot determine encryption strength from {1}\nComplete output: {0}".format(cipherInfo, cipherInfoFields[4]))
108 bits = int(bitMatch.group(1))
109 # figure out whether the cipher is pfs
110 kxMatch = re.match(r'^Kx=([0-9A-Z/]+)$', cipherInfoFields[2])
112 raise Exception("Unexpected OpenSSL output: Cannot determine key-exchange method from {1}\nComplete output: {0}".format(cipherInfo), cipherInfoFields[2])
113 kx = kxMatch.group(1)
114 isPfs = kx in ('DH', 'ECDH')
115 # determine security level
116 isExp = cipher in self.exp
117 isLow = cipher in self.low
118 isMedium = cipher in self.medium
119 isHigh = cipher in self.high
120 assert isExp+isLow+isMedium+isHigh <= 1, "Cipher is more than one from EXP, LOW, MEDIUM, HIGH"
122 strength = CipherStrength.exp
124 strength = CipherStrength.low
126 strength = CipherStrength.medium
128 strength = CipherStrength.high
130 strength = CipherStrength.unknown
132 return CipherProps(bits=bits, strength=strength, isPfs=isPfs)
134 def getProps(self, cipher):
135 if cipher in self.props:
136 return self.props[cipher]
137 props = self.__getProps(cipher)
138 self.props[cipher] = props
142 if __name__ == "__main__":
143 parser = argparse.ArgumentParser(description='Check TLS ciphers supported by a host')
144 parser.add_argument("--starttls", dest="starttls",
145 help="Use a STARTTLS variant to establish the TLS connection. Possible values include smpt, imap, xmpp.")
146 parser.add_argument("--wait-time", "-t", dest="wait_time", default="10",
147 help="Time (in ms) to wait between two connections to the server. Default is 10ms.")
148 parser.add_argument("host", metavar='HOST[:PORT]',
149 help="The host to check")
150 args = parser.parse_args()
154 host, port = args.host.split(':')
159 # get options and other stuff
160 wait_time = float(args.wait_time)
162 if args.starttls is not None:
163 options += ['-starttls', args.starttls]
166 results = test_host(host, port, wait_time, options)
169 propsProvider = CipherPropsProvider()
170 for protocol, ciphers in results.items():
173 print(" Is not supported by client or server")
175 for cipher, supported in ciphers.items():
177 cipherProps = propsProvider.getProps(cipher)
178 print(" {0} ({1}, {2} bits, {3})".format(cipher, cipherProps.strength.name, cipherProps.bits, "FS" if cipherProps.isPfs else "not FS"))