From 6d4c8e2dc9ae426ec32838b78011f004c8aa4254 Mon Sep 17 00:00:00 2001 From: Solderpunk Date: Sun, 10 May 2020 12:35:46 +0200 Subject: [PATCH] First stab at putting an interactive interface on client certificates. --- av98.py | 81 ++++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 57 insertions(+), 24 deletions(-) diff --git a/av98.py b/av98.py index 3c9b71d..811352d 100755 --- a/av98.py +++ b/av98.py @@ -68,28 +68,6 @@ _MIME_HANDLERS = { "text/gemini": "cat %s", } -protocol = ssl.PROTOCOL_TLS if sys.version_info.minor >=6 else ssl.PROTOCOL_TLSv1_2 -context = ssl.SSLContext(protocol) -context.check_hostname = False -context.verify_mode = ssl.CERT_NONE -# Impose minimum TLS version -## In 3.7 and above, this is easy... -if sys.version_info.minor >= 7: - context.minimum_version = ssl.TLSVersion.TLSv1_2 -## Otherwise, it seems very hard... -## The below is less strict than it ought to be, but trying to disable -## TLS v1.1 here using ssl.OP_NO_TLSv1_1 produces unexpected failures -## with recent versions of OpenSSL. What a mess... -else: - context.options |= ssl.OP_NO_SSLv3 - context.options |= ssl.OP_NO_SSLv2 -# Try to enforce sensible ciphers -try: - context.set_ciphers("AES+DHE:AES+ECDHE:CHACHA20+DHE:CHACHA20+ECDHE:!SHA1:@STRENGTH") -except ssl.SSLError: - # Rely on the server to only support sensible things, I guess... - pass - def fix_ipv6_url(url): if not url.count(":") > 2: # Best way to detect them? return url @@ -219,7 +197,9 @@ class GeminiClient(cmd.Cmd): def __init__(self, restricted=False): cmd.Cmd.__init__(self) - self.prompt = "\x1b[38;5;202m" + "AV-98" + "\x1b[38;5;255m" + "> " + "\x1b[0m" + self.no_cert_prompt = "\x1b[38;5;202m" + "AV-98" + "\x1b[38;5;255m" + "> " + "\x1b[0m" + self.cert_prompt = "\x1b[38;5;202m" + "AV-98 (active cert!)" + "\x1b[38;5;255m" + "> " + "\x1b[0m" + self.prompt = self.no_cert_prompt self.gi = None self.history = [] self.hist_index = 0 @@ -243,6 +223,8 @@ class GeminiClient(cmd.Cmd): "gopher_proxy" : "localhost:1965", "width" : 80, "auto_follow_redirects" : True, + "client_certfile" : None, + "client_keyfile" : None, } self.log = { @@ -428,7 +410,37 @@ Slow internet connection? Use 'set timeout' to be more patient.""") # For Gopher requests, use the configured proxy host, port = self.options["gopher_proxy"].rsplit(":", 1) self._debug("Using gopher proxy: " + self.options["gopher_proxy"]) + + # Do DNS resolution addresses = self._get_addresses(host, port) + + # Prepare TLS context + protocol = ssl.PROTOCOL_TLS if sys.version_info.minor >=6 else ssl.PROTOCOL_TLSv1_2 + context = ssl.SSLContext(protocol) + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + # Impose minimum TLS version + ## In 3.7 and above, this is easy... + if sys.version_info.minor >= 7: + context.minimum_version = ssl.TLSVersion.TLSv1_2 + ## Otherwise, it seems very hard... + ## The below is less strict than it ought to be, but trying to disable + ## TLS v1.1 here using ssl.OP_NO_TLSv1_1 produces unexpected failures + ## with recent versions of OpenSSL. What a mess... + else: + context.options |= ssl.OP_NO_SSLv3 + context.options |= ssl.OP_NO_SSLv2 + # Try to enforce sensible ciphers + try: + context.set_ciphers("AES+DHE:AES+ECDHE:CHACHA20+DHE:CHACHA20+ECDHE:!SHA1:@STRENGTH") + except ssl.SSLError: + # Rely on the server to only support sensible things, I guess... + pass + # Load client certificate if needed + if self.options["client_certfile"]: + context.load_cert_chain(self.options["client_certfile"], + self.options["client_keyfile"]) + # Connect to remote host by any address possible err = None for address in addresses: @@ -657,6 +669,25 @@ Slow internet connection? Use 'set timeout' to be more patient.""") pass self.options[option] = value + @restricted + def do_cert(self, line): + """Set or clear a client certificate""" + if self.options["client_certfile"]: + print("Deactivating client certificate.") + self.options["client_certfile"] = None + self.options["client_keyfile"] = None + self.prompt = self.no_cert_prompt + else: + print("Loading client certificate file, in PEM format (blank line to cancel)") + certfile = input("Certfile path: ") + print("Loading private key file, in PEM format (blank line to cancel)") + keyfile = input("Keyfile path: ") + self.options["client_certfile"] = certfile + self.options["client_keyfile"] = keyfile + self.prompt = self.cert_prompt + + + @restricted def do_handler(self, line): """View or set handler commands for different MIME types.""" @@ -1076,7 +1107,9 @@ def main(): # Act on args if args.tls_cert: # If tls_key is None, python will attempt to load the key from tls_cert. - context.load_cert_chain(args.tls_cert, args.tls_key) + gc.options["client_certfile"] = args.tls_cert + gc.options["client_keyfile"] = args.tls_key + gc.prompt = gc.cert_prompt if args.bookmarks: gc.cmdqueue.append("bookmarks") elif args.url: