rename: ssl-check -> tls-check
[tls-check.git] / tls-check
1 #!/usr/bin/python3
2 import subprocess, sys, argparse
3 from collections import OrderedDict
4 from enum import Enum
5
6 # progress bar
7 def terminal_size():
8     import fcntl, termios, struct
9     try:
10         result = fcntl.ioctl(1, termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0))
11     except OSError:
12         # this is not a terminal
13         return 0, 0
14     h, w, hp, wp = struct.unpack('HHHH', result)
15     assert w > 0 and h > 0, "Empty terminal...??"
16     return w, h
17
18 def compute_frac(fracs):
19     frac = 0.0
20     last_frac = 1.0
21     for complete, total in fracs:
22         frac += (complete*last_frac/total)
23         last_frac *= 1/total
24     return frac
25
26 def print_progress(state, fracs):
27     STATE_WIDTH = 30
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)))
33     sys.stdout.flush()
34 def finish_progress():
35     w, h = terminal_size()
36     sys.stdout.write('\r'+(' '*w)+'\r')
37     sys.stdout.flush()
38
39 # cipher check
40 def list_ciphers():
41     ciphers = subprocess.check_output(["openssl", "ciphers", "ALL:COMPLEMENTOFALL"]).decode('UTF-8').strip()
42     return ciphers.split(':')
43
44 def test_cipher(host, port, protocol, cipher = None, options=[]):
45     try:
46         if cipher is not None:
47             options = ["-cipher", cipher]+options
48         subprocess.check_call(["openssl", "s_client", "-"+protocol, "-connect", host+":"+str(port)]+options,
49                               stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
50     except subprocess.CalledProcessError:
51         return False
52     else:
53         return True
54
55 def test_protocol(host, port, protocol, ciphers, base_frac, options=[]):
56     if test_cipher(host, port, protocol, options=options):
57         # the protocol is supported
58         results = OrderedDict()
59         for i in range(len(ciphers)):
60             cipher = ciphers[i]
61             print_progress(protocol+" "+cipher, base_frac+[(i, len(ciphers))])
62             results[cipher] = test_cipher(host, port, protocol, cipher, options)
63         return results
64     else:
65         # it is not supported
66         return None
67
68 def test_host(host, port, options=[]):
69     ciphers = list_ciphers()
70     results = OrderedDict()
71     protocols = ('ssl2', 'ssl3', 'tls1', 'tls1_1', 'tls1_2')
72     for i in range(len(protocols)):
73         protocol = protocols[i]
74         print_progress(protocol, [(i, len(protocols))])
75         results[protocol] = test_protocol(host, port, protocol, ciphers, [(i, len(protocols))], options)
76     finish_progress()
77     return results
78
79 if __name__ == "__main__":
80     parser = argparse.ArgumentParser(description='Check TLS ciphers supported by a host')
81     parser.add_argument("--starttls", dest="starttls",
82                         help="Use a STARTTLS variant to establish the TLS connection. Possible values include smpt, imap, xmpp.")
83     parser.add_argument("host", metavar='HOST[:PORT]',
84                         help="The host to check")
85     args = parser.parse_args()
86     
87     # get host, port
88     if ':' in args.host:
89         host, port = args.host.split(':')
90     else:
91         host = args.host
92         port = 443
93     
94     # get options
95     options = []
96     if args.starttls is not None:
97         options += ['-starttls', args.starttls]
98     
99     # run the test
100     results = test_host(host, port, options)
101     
102     # print the results
103     for protocol, ciphers in results.items():
104         print(protocol+":")
105         if ciphers is None:
106             print("    Is not supported by client or server")
107         else:
108             for cipher, supported in ciphers.items():
109                 if supported:
110                     print("    "+cipher)
111         print()