#!/usr/bin/python3

# Runs SSLyze on the TLS endpoints of a box and outputs
# the results so we can inspect the settings and compare
# against a known good version in tls_results.txt.
#
# Make sure you have SSLyze available:
# wget https://github.com/nabla-c0d3/sslyze/releases/download/release-0.11/sslyze-0_11-linux64.zip
# unzip sslyze-0_11-linux64.zip
#
# Then run:
#
# python3 tls.py yourservername
#
# If you are on a residential network that blocks outbound
# port 25 connections, then you can proxy the connections
# through some other host you can ssh into (maybe the box
# itself?):
#
# python3 --proxy user@ssh_host yourservername
#
# (This will launch "ssh -N -L10023:yourservername:testport user@ssh_host"
# to create a tunnel.)

import sys, subprocess, re, time, json, csv, io, urllib.request

######################################################################

# PARSE COMMAND LINE

proxy = None
args = list(sys.argv[1:])
while len(args) > 0:
	if args[0] == "--proxy":
		args.pop(0)
		proxy = args.pop(0)
	break

if len(args) == 0:
	print("Usage: python3 tls.py [--proxy ssh_host] hostname")
	sys.exit(0)

host = args[0]

######################################################################

SSLYZE = "sslyze-0_11-linux64/sslyze/sslyze.py"

common_opts = ["--sslv2", "--sslv3", "--tlsv1", "--tlsv1_1", "--tlsv1_2", "--reneg", "--resum",
	"--hide_rejected_ciphers", "--compression", "--heartbleed"]

# Recommendations from Mozilla as of May 20, 2015 at
# https://wiki.mozilla.org/Security/Server_Side_TLS.
#
# The 'modern' ciphers support Firefox 27, Chrome 22, IE 11,
# Opera 14, Safari 7, Android 4.4, Java 8. Assumes TLSv1.1,
# TLSv1.2 only, though we may also be allowing TLSv3.
#
# The 'intermediate' ciphers support Firefox 1, Chrome 1, IE 7,
# Opera 5, Safari 1, Windows XP IE8, Android 2.3, Java 7.
# Assumes TLSv1, TLSv1.1, TLSv1.2.
#
# The 'old' ciphers bring compatibility back to Win XP IE 6.
MOZILLA_CIPHERS_MODERN = "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256"
MOZILLA_CIPHERS_INTERMEDIATE = "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS"
MOZILLA_CIPHERS_OLD = "ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:DES-CBC3-SHA:HIGH:SEED:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!RSAPSK:!aDH:!aECDH:!EDH-DSS-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA:!SRP"

######################################################################

def sslyze(opts, port, ok_ciphers):
	# Print header.
	header = ("PORT %d" % port)
	print(header)
	print("-" * (len(header)))

	# What ciphers should we expect?
	ok_ciphers = subprocess.check_output(["openssl", "ciphers", ok_ciphers]).decode("utf8").strip().split(":")

	# Form the SSLyze connection string.
	connection_string = host + ":" + str(port)

	# Proxy via SSH.
	proxy_proc = None
	if proxy:
		connection_string = "localhost:10023"
		proxy_proc = subprocess.Popen(["ssh", "-N", "-L10023:%s:%d" % (host, port), proxy])
		time.sleep(3)

	try:
		# Execute SSLyze.
		out = subprocess.check_output([SSLYZE] + common_opts + opts + [connection_string])
		out = out.decode("utf8")

		# Trim output to make better for storing in git.
		if "SCAN RESULTS FOR" not in out:
			# Failed. Just output the error.
			out = re.sub("[\w\W]*CHECKING HOST\(S\) AVAILABILITY\n\s*-+\n", "", out) # chop off header that shows the host we queried
		out = re.sub("[\w\W]*SCAN RESULTS FOR.*\n\s*-+\n", "", out) # chop off header that shows the host we queried
		out = re.sub("SCAN COMPLETED IN .*", "", out)
		out = out.rstrip(" \n-") + "\n"

		# Print.
		print(out)

		# Pull out the accepted ciphers list for each SSL/TLS protocol
		# version outputted.
		accepted_ciphers = set()
		for ciphers in re.findall(" Accepted:([\w\W]*?)\n *\n", out):
			accepted_ciphers |= set(re.findall("\n\s*(\S*)", ciphers))

		# Compare to what Mozilla recommends, for a given modernness-level.
		print("  Should Not Offer: " + (", ".join(sorted(accepted_ciphers-set(ok_ciphers))) or "(none -- good)"))
		print("  Could Also Offer: " + (", ".join(sorted(set(ok_ciphers)-accepted_ciphers)) or "(none -- good)"))

		# What clients does that mean we support on this protocol?
		supported_clients = { }
		for cipher in accepted_ciphers:
			if cipher in cipher_clients:
				for client in cipher_clients[cipher]:
					supported_clients[client] = supported_clients.get(client, 0) + 1
		print("  Supported Clients: " + (", ".join(sorted(supported_clients.keys(), key = lambda client : -supported_clients[client]))))

		# Blank line.
		print()

	finally:
		if proxy_proc:
			proxy_proc.terminate()
			try:
				proxy_proc.wait(5)
			except TimeoutExpired:
				proxy_proc.kill()

# Get a list of OpenSSL cipher names.
cipher_names = { }
for cipher in csv.DictReader(io.StringIO(urllib.request.urlopen("https://raw.githubusercontent.com/mail-in-a-box/user-agent-tls-capabilities/master/cipher_names.csv").read().decode("utf8"))):
	# not sure why there are some multi-line values, use first line:
	cipher["OpenSSL"] = cipher["OpenSSL"].split("\n")[0]
	cipher_names[cipher["IANA"]] = cipher["OpenSSL"]

# Get a list of what clients support what ciphers, using OpenSSL cipher names.
client_compatibility = json.loads(urllib.request.urlopen("https://raw.githubusercontent.com/mail-in-a-box/user-agent-tls-capabilities/master/clients.json").read().decode("utf8"))
cipher_clients = { }
for client in client_compatibility:
	if len(set(client['protocols']) & set(["TLS 1.0", "TLS 1.1", "TLS 1.2"])) == 0: continue # does not support TLS
	for cipher in client['ciphers']:
		cipher_clients.setdefault(cipher_names.get(cipher), set()).add("/".join(x for x in [client['client']['name'], client['client']['version'], client['client']['platform']] if x))

# Run SSLyze on various ports.

# SMTP
sslyze(["--starttls=smtp"], 25, MOZILLA_CIPHERS_OLD)

# SMTP Submission
sslyze(["--starttls=smtp"], 587, MOZILLA_CIPHERS_MODERN)

# HTTPS
sslyze(["--http_get", "--chrome_sha1", "--hsts"], 443, MOZILLA_CIPHERS_INTERMEDIATE)

# IMAP
sslyze([], 993, MOZILLA_CIPHERS_MODERN)

# POP3
sslyze([], 995, MOZILLA_CIPHERS_MODERN)
