Factor out client certificate management into its own class/file.
This commit is contained in:
parent
607223c25a
commit
de7e5dc254
305
av98.py
305
av98.py
|
@ -19,7 +19,6 @@ import cgi
|
||||||
import codecs
|
import codecs
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import getpass
|
import getpass
|
||||||
import glob
|
|
||||||
import logging
|
import logging
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
|
@ -34,7 +33,6 @@ import tempfile
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import uuid
|
|
||||||
import webbrowser
|
import webbrowser
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -44,6 +42,8 @@ except ModuleNotFoundError:
|
||||||
|
|
||||||
from cache import Cache
|
from cache import Cache
|
||||||
from tofu import TofuStore
|
from tofu import TofuStore
|
||||||
|
from clientcerts import ClientCertificateManager
|
||||||
|
|
||||||
_VERSION = "1.0.2dev"
|
_VERSION = "1.0.2dev"
|
||||||
|
|
||||||
_MAX_REDIRECTS = 5
|
_MAX_REDIRECTS = 5
|
||||||
|
@ -258,13 +258,6 @@ class GeminiClient(cmd.Cmd):
|
||||||
self.visited_hosts = set()
|
self.visited_hosts = set()
|
||||||
self.waypoints = []
|
self.waypoints = []
|
||||||
|
|
||||||
self.client_certs = {
|
|
||||||
"active": None
|
|
||||||
}
|
|
||||||
self.active_cert_domains = []
|
|
||||||
self.active_is_transient = False
|
|
||||||
self.transient_certs_created = []
|
|
||||||
|
|
||||||
self.options = {
|
self.options = {
|
||||||
"debug" : False,
|
"debug" : False,
|
||||||
"ipv6" : True,
|
"ipv6" : True,
|
||||||
|
@ -294,6 +287,7 @@ class GeminiClient(cmd.Cmd):
|
||||||
}
|
}
|
||||||
|
|
||||||
self.tofu_store = TofuStore(self.config_dir)
|
self.tofu_store = TofuStore(self.config_dir)
|
||||||
|
self.client_cert_manager = ClientCertificateManager(self.config_dir)
|
||||||
self.cache = Cache()
|
self.cache = Cache()
|
||||||
|
|
||||||
ui_out.debug("Raw buffer: ", self.raw_file_buffer)
|
ui_out.debug("Raw buffer: ", self.raw_file_buffer)
|
||||||
|
@ -459,7 +453,13 @@ Slow internet connection? Use 'set timeout' to be more patient.""")
|
||||||
|
|
||||||
# Client cert
|
# Client cert
|
||||||
elif status.startswith("6"):
|
elif status.startswith("6"):
|
||||||
self._handle_cert_request(meta, status, gi.host)
|
if self.restricted:
|
||||||
|
print("The server is requesting a client certificate.")
|
||||||
|
print("These are not supported in restricted mode, sorry.")
|
||||||
|
raise UserAbortException()
|
||||||
|
|
||||||
|
if not self.client_cert_manager.handle_cert_request(meta, status, gi.host):
|
||||||
|
raise UserAbortException()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Invalid status
|
# Invalid status
|
||||||
|
@ -509,37 +509,6 @@ Slow internet connection? Use 'set timeout' to be more patient.""")
|
||||||
host, port = self.options["http_proxy"].rsplit(":",1)
|
host, port = self.options["http_proxy"].rsplit(":",1)
|
||||||
ui_out.debug("Using http proxy: " + self.options["http_proxy"])
|
ui_out.debug("Using http proxy: " + self.options["http_proxy"])
|
||||||
|
|
||||||
# Be careful with client certificates!
|
|
||||||
# Are we crossing a domain boundary?
|
|
||||||
if self.active_cert_domains and gi.host not in self.active_cert_domains:
|
|
||||||
if self.active_is_transient:
|
|
||||||
print("Permanently delete currently active transient certificate?")
|
|
||||||
resp = input("Y/N? ")
|
|
||||||
if resp.strip().lower() in ("y", "yes"):
|
|
||||||
print("Destroying certificate.")
|
|
||||||
self._deactivate_client_cert()
|
|
||||||
else:
|
|
||||||
print("Staying here.")
|
|
||||||
raise UserAbortException()
|
|
||||||
else:
|
|
||||||
print("PRIVACY ALERT: Deactivate client cert before connecting to a new domain?")
|
|
||||||
resp = input("Y/N? ")
|
|
||||||
if resp.strip().lower() in ("n", "no"):
|
|
||||||
print("Keeping certificate active for {}".format(gi.host))
|
|
||||||
else:
|
|
||||||
print("Deactivating certificate.")
|
|
||||||
self._deactivate_client_cert()
|
|
||||||
|
|
||||||
# Suggest reactivating previous certs
|
|
||||||
if not self.client_certs["active"] and gi.host in self.client_certs:
|
|
||||||
print("PRIVACY ALERT: Reactivate previously used client cert for {}?".format(gi.host))
|
|
||||||
resp = input("Y/N? ")
|
|
||||||
if resp.strip().lower() in ("y", "yes"):
|
|
||||||
self._activate_client_cert(*self.client_certs[gi.host])
|
|
||||||
else:
|
|
||||||
print("Remaining unidentified.")
|
|
||||||
self.client_certs.pop(gi.host)
|
|
||||||
|
|
||||||
# Do DNS resolution
|
# Do DNS resolution
|
||||||
try:
|
try:
|
||||||
addresses = self._get_addresses(host, port)
|
addresses = self._get_addresses(host, port)
|
||||||
|
@ -549,44 +518,9 @@ Slow internet connection? Use 'set timeout' to be more patient.""")
|
||||||
raise err
|
raise err
|
||||||
|
|
||||||
# Prepare TLS context
|
# Prepare TLS context
|
||||||
def _newest_supported_protocol():
|
context = self._prepare_SSL_context(self.options["tls_mode"])
|
||||||
if sys.version_info >= (3, 10):
|
if not self.client_cert_manager.associate_client_cert(context, gi):
|
||||||
return ssl.PROTOCOL_TLS_CLIENT
|
raise UserAbortException()
|
||||||
elif sys.version_info >= (3, 6):
|
|
||||||
return ssl.PROTOCOL_TLS
|
|
||||||
else:
|
|
||||||
return ssl.PROTOCOL_TLSv1_2
|
|
||||||
context = ssl.SSLContext(_newest_supported_protocol())
|
|
||||||
|
|
||||||
# Use CAs or TOFU
|
|
||||||
if self.options["tls_mode"] == "ca":
|
|
||||||
context.verify_mode = ssl.CERT_REQUIRED
|
|
||||||
context.check_hostname = True
|
|
||||||
context.load_default_certs()
|
|
||||||
else:
|
|
||||||
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("AESGCM+ECDHE:AESGCM+DHE:CHACHA20+ECDHE:CHACHA20+DHE:!DSS:!SHA1:!MD5:@STRENGTH")
|
|
||||||
except ssl.SSLError:
|
|
||||||
# Rely on the server to only support sensible things, I guess...
|
|
||||||
pass
|
|
||||||
# Load client certificate if needed
|
|
||||||
if self.client_certs["active"]:
|
|
||||||
certfile, keyfile = self.client_certs["active"]
|
|
||||||
context.load_cert_chain(certfile, keyfile)
|
|
||||||
|
|
||||||
# Connect to remote host by any address possible
|
# Connect to remote host by any address possible
|
||||||
err = None
|
err = None
|
||||||
|
@ -622,11 +556,6 @@ Slow internet connection? Use 'set timeout' to be more patient.""")
|
||||||
cert = s.getpeercert(binary_form=True)
|
cert = s.getpeercert(binary_form=True)
|
||||||
self.tofu_store.validate_cert(address[4][0], host, cert)
|
self.tofu_store.validate_cert(address[4][0], host, cert)
|
||||||
|
|
||||||
# Remember that we showed the current cert to this domain...
|
|
||||||
if self.client_certs["active"]:
|
|
||||||
self.active_cert_domains.append(gi.host)
|
|
||||||
self.client_certs[gi.host] = self.client_certs["active"]
|
|
||||||
|
|
||||||
# Send request and wrap response in a file descriptor
|
# Send request and wrap response in a file descriptor
|
||||||
ui_out.debug("Sending %s<CRLF>" % gi.url)
|
ui_out.debug("Sending %s<CRLF>" % gi.url)
|
||||||
s.sendall((gi.url + CRLF).encode("UTF-8"))
|
s.sendall((gi.url + CRLF).encode("UTF-8"))
|
||||||
|
@ -708,47 +637,45 @@ Slow internet connection? Use 'set timeout' to be more patient.""")
|
||||||
|
|
||||||
return addresses
|
return addresses
|
||||||
|
|
||||||
def _handle_cert_request(self, meta, status, host):
|
def _prepare_SSL_context(self, cert_validation_mode="tofu"):
|
||||||
|
# Flail against version churn
|
||||||
# Don't do client cert stuff in restricted mode, as in principle
|
if sys.version_info >= (3, 10):
|
||||||
# it could be used to fill up the disk by creating a whole lot of
|
_newest_supported_protocol = ssl.PROTOCOL_TLS_CLIENT
|
||||||
# certificates
|
elif sys.version_info >= (3, 6):
|
||||||
if self.restricted:
|
_newest_supported_protocol = ssl.PROTOCOL_TLS
|
||||||
print("The server is requesting a client certificate.")
|
|
||||||
print("These are not supported in restricted mode, sorry.")
|
|
||||||
raise UserAbortException()
|
|
||||||
|
|
||||||
print("SERVER SAYS: ", meta)
|
|
||||||
# Present different messages for different 6x statuses, but
|
|
||||||
# handle them the same.
|
|
||||||
if status in ("64", "65"):
|
|
||||||
print("The server rejected your certificate because it is either expired or not yet valid.")
|
|
||||||
elif status == "63":
|
|
||||||
print("The server did not accept your certificate.")
|
|
||||||
print("You may need to e.g. coordinate with the admin to get your certificate fingerprint whitelisted.")
|
|
||||||
else:
|
else:
|
||||||
print("The site {} is requesting a client certificate.".format(host))
|
_newest_supported_protocol = ssl.PROTOCOL_TLSv1_2
|
||||||
print("This will allow the site to recognise you across requests.")
|
context = ssl.SSLContext(_newest_supported_protocol)
|
||||||
|
|
||||||
# Give the user choices
|
# Use CAs or TOFU
|
||||||
print("What do you want to do?")
|
if cert_validation_mode == "ca":
|
||||||
print("1. Give up.")
|
context.verify_mode = ssl.CERT_REQUIRED
|
||||||
print("2. Generate a new transient certificate.")
|
context.check_hostname = True
|
||||||
print("3. Generate a new persistent certificate.")
|
context.load_default_certs()
|
||||||
print("4. Load a previously generated certificate.")
|
|
||||||
print("5. Load a certificate from an external file.")
|
|
||||||
choice = input("> ").strip()
|
|
||||||
if choice == "2":
|
|
||||||
self._generate_transient_cert_cert()
|
|
||||||
elif choice == "3":
|
|
||||||
self._generate_persistent_client_cert()
|
|
||||||
elif choice == "4":
|
|
||||||
self._choose_client_cert()
|
|
||||||
elif choice == "5":
|
|
||||||
self._load_client_cert()
|
|
||||||
else:
|
else:
|
||||||
print("Giving up.")
|
context.check_hostname = False
|
||||||
raise UserAbortException()
|
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("AESGCM+ECDHE:AESGCM+DHE:CHACHA20+ECDHE:CHACHA20+DHE:!DSS:!SHA1:!MD5:@STRENGTH")
|
||||||
|
except ssl.SSLError:
|
||||||
|
# Rely on the server to only support sensible things, I guess...
|
||||||
|
pass
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
def _get_handler_cmd(self, mimetype):
|
def _get_handler_cmd(self, mimetype):
|
||||||
# Now look for a handler for this mimetype
|
# Now look for a handler for this mimetype
|
||||||
|
@ -848,110 +775,6 @@ Slow internet connection? Use 'set timeout' to be more patient.""")
|
||||||
self.log["ipv6_requests"] += 1
|
self.log["ipv6_requests"] += 1
|
||||||
self.log["ipv6_bytes_recvd"] += size
|
self.log["ipv6_bytes_recvd"] += size
|
||||||
|
|
||||||
def _load_client_cert(self):
|
|
||||||
"""
|
|
||||||
Interactively load a TLS client certificate from the filesystem in PEM
|
|
||||||
format.
|
|
||||||
"""
|
|
||||||
print("Loading client certificate file, in PEM format (blank line to cancel)")
|
|
||||||
certfile = input("Certfile path: ").strip()
|
|
||||||
if not certfile:
|
|
||||||
print("Aborting.")
|
|
||||||
return
|
|
||||||
certfile = os.path.expanduser(certfile)
|
|
||||||
if not os.path.isfile(certfile):
|
|
||||||
print("Certificate file {} does not exist.".format(certfile))
|
|
||||||
return
|
|
||||||
print("Loading private key file, in PEM format (blank line to cancel)")
|
|
||||||
keyfile = input("Keyfile path: ").strip()
|
|
||||||
if not keyfile:
|
|
||||||
print("Aborting.")
|
|
||||||
return
|
|
||||||
keyfile = os.path.expanduser(keyfile)
|
|
||||||
if not os.path.isfile(keyfile):
|
|
||||||
print("Private key file {} does not exist.".format(keyfile))
|
|
||||||
return
|
|
||||||
self._activate_client_cert(certfile, keyfile)
|
|
||||||
|
|
||||||
def _generate_transient_cert_cert(self):
|
|
||||||
"""
|
|
||||||
Use `openssl` command to generate a new transient client certificate
|
|
||||||
with 24 hours of validity.
|
|
||||||
"""
|
|
||||||
certdir = os.path.join(self.config_dir, "transient_certs")
|
|
||||||
name = str(uuid.uuid4())
|
|
||||||
self._generate_client_cert(certdir, name, transient=True)
|
|
||||||
self.active_is_transient = True
|
|
||||||
self.transient_certs_created.append(name)
|
|
||||||
|
|
||||||
def _generate_persistent_client_cert(self):
|
|
||||||
"""
|
|
||||||
Interactively use `openssl` command to generate a new persistent client
|
|
||||||
certificate with one year of validity.
|
|
||||||
"""
|
|
||||||
certdir = os.path.join(self.config_dir, "client_certs")
|
|
||||||
print("What do you want to name this new certificate?")
|
|
||||||
print("Answering `mycert` will create `{0}/mycert.crt` and `{0}/mycert.key`".format(certdir))
|
|
||||||
name = input("> ")
|
|
||||||
if not name.strip():
|
|
||||||
print("Aborting.")
|
|
||||||
return
|
|
||||||
self._generate_client_cert(certdir, name)
|
|
||||||
|
|
||||||
def _generate_client_cert(self, certdir, basename, transient=False):
|
|
||||||
"""
|
|
||||||
Use `openssl` binary to generate a client certificate (which may be
|
|
||||||
transient or persistent) and save the certificate and private key to the
|
|
||||||
specified directory with the specified basename.
|
|
||||||
"""
|
|
||||||
if not os.path.exists(certdir):
|
|
||||||
os.makedirs(certdir)
|
|
||||||
certfile = os.path.join(certdir, basename+".crt")
|
|
||||||
keyfile = os.path.join(certdir, basename+".key")
|
|
||||||
cmd = "openssl req -x509 -newkey rsa:2048 -days {} -nodes -keyout {} -out {}".format(1 if transient else 365, keyfile, certfile)
|
|
||||||
if transient:
|
|
||||||
cmd += " -subj '/CN={}'".format(basename)
|
|
||||||
os.system(cmd)
|
|
||||||
self._activate_client_cert(certfile, keyfile)
|
|
||||||
|
|
||||||
def _choose_client_cert(self):
|
|
||||||
"""
|
|
||||||
Interactively select a previously generated client certificate and
|
|
||||||
activate it.
|
|
||||||
"""
|
|
||||||
certdir = os.path.join(self.config_dir, "client_certs")
|
|
||||||
certs = glob.glob(os.path.join(certdir, "*.crt"))
|
|
||||||
if len(certs) == 0:
|
|
||||||
print("There are no previously generated certificates.")
|
|
||||||
return
|
|
||||||
certdir = {}
|
|
||||||
for n, cert in enumerate(certs):
|
|
||||||
certdir[str(n+1)] = (cert, os.path.splitext(cert)[0] + ".key")
|
|
||||||
print("{}. {}".format(n+1, os.path.splitext(os.path.basename(cert))[0]))
|
|
||||||
choice = input("> ").strip()
|
|
||||||
if choice in certdir:
|
|
||||||
certfile, keyfile = certdir[choice]
|
|
||||||
self._activate_client_cert(certfile, keyfile)
|
|
||||||
else:
|
|
||||||
print("What?")
|
|
||||||
|
|
||||||
def _activate_client_cert(self, certfile, keyfile):
|
|
||||||
self.client_certs["active"] = (certfile, keyfile)
|
|
||||||
self.active_cert_domains = []
|
|
||||||
self.prompt = self.cert_prompt
|
|
||||||
ui_out.debug("Using ID {} / {}.".format(*self.client_certs["active"]))
|
|
||||||
|
|
||||||
def _deactivate_client_cert(self):
|
|
||||||
if self.active_is_transient:
|
|
||||||
for filename in self.client_certs["active"]:
|
|
||||||
os.remove(filename)
|
|
||||||
for domain in self.active_cert_domains:
|
|
||||||
self.client_certs.pop(domain)
|
|
||||||
self.client_certs["active"] = None
|
|
||||||
self.active_cert_domains = []
|
|
||||||
self.prompt = self.no_cert_prompt
|
|
||||||
self.active_is_transient = False
|
|
||||||
|
|
||||||
# Cmd implementation follows
|
# Cmd implementation follows
|
||||||
|
|
||||||
def default(self, line):
|
def default(self, line):
|
||||||
|
@ -1047,25 +870,7 @@ Slow internet connection? Use 'set timeout' to be more patient.""")
|
||||||
def do_cert(self, line):
|
def do_cert(self, line):
|
||||||
"""Manage client certificates"""
|
"""Manage client certificates"""
|
||||||
print("Managing client certificates")
|
print("Managing client certificates")
|
||||||
if self.client_certs["active"]:
|
self.client_cert_manager.manage()
|
||||||
print("Active certificate: {}".format(self.client_certs["active"][0]))
|
|
||||||
print("1. Deactivate client certificate.")
|
|
||||||
print("2. Generate new certificate.")
|
|
||||||
print("3. Load previously generated certificate.")
|
|
||||||
print("4. Load externally created client certificate from file.")
|
|
||||||
print("Enter blank line to exit certificate manager.")
|
|
||||||
choice = input("> ").strip()
|
|
||||||
if choice == "1":
|
|
||||||
print("Deactivating client certificate.")
|
|
||||||
self._deactivate_client_cert()
|
|
||||||
elif choice == "2":
|
|
||||||
self._generate_persistent_client_cert()
|
|
||||||
elif choice == "3":
|
|
||||||
self._choose_client_cert()
|
|
||||||
elif choice == "4":
|
|
||||||
self._load_client_cert()
|
|
||||||
else:
|
|
||||||
print("Aborting.")
|
|
||||||
|
|
||||||
@restricted
|
@restricted
|
||||||
def do_handler(self, line):
|
def do_handler(self, line):
|
||||||
|
@ -1460,12 +1265,8 @@ current gemini browsing session."""
|
||||||
# Clean up after ourself
|
# Clean up after ourself
|
||||||
os.unlink(self.raw_file_buffer)
|
os.unlink(self.raw_file_buffer)
|
||||||
os.unlink(self.rendered_file_buffer)
|
os.unlink(self.rendered_file_buffer)
|
||||||
|
self.client_cert_manager.cleanup()
|
||||||
for cert in self.transient_certs_created:
|
# Say goodbye
|
||||||
for ext in (".crt", ".key"):
|
|
||||||
certfile = os.path.join(self.config_dir, "transient_certs", cert+ext)
|
|
||||||
if os.path.exists(certfile):
|
|
||||||
os.remove(certfile)
|
|
||||||
print()
|
print()
|
||||||
print("Thank you for flying AV-98!")
|
print("Thank you for flying AV-98!")
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
@ -1522,7 +1323,7 @@ def main():
|
||||||
# Act on args
|
# Act on args
|
||||||
if args.tls_cert:
|
if args.tls_cert:
|
||||||
# If tls_key is None, python will attempt to load the key from tls_cert.
|
# If tls_key is None, python will attempt to load the key from tls_cert.
|
||||||
gc._activate_client_cert(args.tls_cert, args.tls_key)
|
gc.client_cert_manager._activate_client_cert(args.tls_cert, args.tls_key)
|
||||||
if args.bookmarks:
|
if args.bookmarks:
|
||||||
gc.cmdqueue.append("bookmarks")
|
gc.cmdqueue.append("bookmarks")
|
||||||
elif args.url:
|
elif args.url:
|
||||||
|
|
|
@ -0,0 +1,233 @@
|
||||||
|
import glob
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
ui_out = logging.getLogger("av98_logger")
|
||||||
|
|
||||||
|
class ClientCertificateManager:
|
||||||
|
|
||||||
|
def __init__(self, config_dir):
|
||||||
|
|
||||||
|
self.config_dir = config_dir
|
||||||
|
self.client_certs = {
|
||||||
|
"active": None
|
||||||
|
}
|
||||||
|
self.active_cert_domains = []
|
||||||
|
self.active_is_transient = False
|
||||||
|
self.transient_certs_created = []
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
for cert in self.transient_certs_created:
|
||||||
|
for ext in (".crt", ".key"):
|
||||||
|
certfile = os.path.join(self.config_dir, "transient_certs", cert+ext)
|
||||||
|
if os.path.exists(certfile):
|
||||||
|
os.remove(certfile)
|
||||||
|
|
||||||
|
def manage(self):
|
||||||
|
if self.client_certs["active"]:
|
||||||
|
print("Active certificate: {}".format(self.client_certs["active"][0]))
|
||||||
|
print("1. Deactivate client certificate.")
|
||||||
|
print("2. Generate new certificate.")
|
||||||
|
print("3. Load previously generated certificate.")
|
||||||
|
print("4. Load externally created client certificate from file.")
|
||||||
|
print("Enter blank line to exit certificate manager.")
|
||||||
|
choice = input("> ").strip()
|
||||||
|
if choice == "1":
|
||||||
|
print("Deactivating client certificate.")
|
||||||
|
self._deactivate_client_cert()
|
||||||
|
elif choice == "2":
|
||||||
|
self._generate_persistent_client_cert()
|
||||||
|
elif choice == "3":
|
||||||
|
self._choose_client_cert()
|
||||||
|
elif choice == "4":
|
||||||
|
self._load_client_cert()
|
||||||
|
else:
|
||||||
|
print("Aborting.")
|
||||||
|
|
||||||
|
def associate_client_cert(self, context, gi):
|
||||||
|
# Be careful with client certificates!
|
||||||
|
# Are we crossing a domain boundary?
|
||||||
|
if self.client_certs["active"] and gi.host not in self.active_cert_domains:
|
||||||
|
if self.active_is_transient:
|
||||||
|
print("Permanently delete currently active transient certificate?")
|
||||||
|
resp = input("Y/N? ")
|
||||||
|
if resp.strip().lower() in ("y", "yes"):
|
||||||
|
print("Destroying certificate.")
|
||||||
|
self._deactivate_client_cert()
|
||||||
|
else:
|
||||||
|
print("Staying here.")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
print("PRIVACY ALERT: Deactivate client cert before connecting to a new domain?")
|
||||||
|
resp = input("Y/N? ")
|
||||||
|
if resp.strip().lower() in ("n", "no"):
|
||||||
|
print("Keeping certificate active for {}".format(gi.host))
|
||||||
|
self.active_cert_domains.append(gi.host)
|
||||||
|
self.client_certs[gi.host] = self.client_certs["active"]
|
||||||
|
else:
|
||||||
|
print("Deactivating certificate.")
|
||||||
|
self._deactivate_client_cert()
|
||||||
|
|
||||||
|
# Suggest reactivating previous certs
|
||||||
|
if not self.client_certs["active"] and gi.host in self.client_certs:
|
||||||
|
print("PRIVACY ALERT: Reactivate previously used client cert for {}?".format(gi.host))
|
||||||
|
resp = input("Y/N? ")
|
||||||
|
if resp.strip().lower() in ("y", "yes"):
|
||||||
|
self._activate_client_cert(*self.client_certs[gi.host])
|
||||||
|
else:
|
||||||
|
print("Remaining unidentified.")
|
||||||
|
self.client_certs.pop(gi.host)
|
||||||
|
|
||||||
|
# Associate certs to context based on above
|
||||||
|
if self.client_certs["active"]:
|
||||||
|
certfile, keyfile = self.client_certs["active"]
|
||||||
|
context.load_cert_chain(certfile, keyfile)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def handle_cert_request(self, meta, status, host):
|
||||||
|
|
||||||
|
# Don't do client cert stuff in restricted mode, as in principle
|
||||||
|
# it could be used to fill up the disk by creating a whole lot of
|
||||||
|
# certificates
|
||||||
|
print("SERVER SAYS: ", meta)
|
||||||
|
# Present different messages for different 6x statuses, but
|
||||||
|
# handle them the same.
|
||||||
|
if status in ("64", "65"):
|
||||||
|
print("The server rejected your certificate because it is either expired or not yet valid.")
|
||||||
|
elif status == "63":
|
||||||
|
print("The server did not accept your certificate.")
|
||||||
|
print("You may need to e.g. coordinate with the admin to get your certificate fingerprint whitelisted.")
|
||||||
|
else:
|
||||||
|
print("The site {} is requesting a client certificate.".format(host))
|
||||||
|
print("This will allow the site to recognise you across requests.")
|
||||||
|
|
||||||
|
# Give the user choices
|
||||||
|
print("What do you want to do?")
|
||||||
|
print("1. Give up.")
|
||||||
|
print("2. Generate a new transient certificate.")
|
||||||
|
print("3. Generate a new persistent certificate.")
|
||||||
|
print("4. Load a previously generated certificate.")
|
||||||
|
print("5. Load a certificate from an external file.")
|
||||||
|
choice = input("> ").strip()
|
||||||
|
if choice == "2":
|
||||||
|
self._generate_transient_cert_cert()
|
||||||
|
elif choice == "3":
|
||||||
|
self._generate_persistent_client_cert()
|
||||||
|
elif choice == "4":
|
||||||
|
self._choose_client_cert()
|
||||||
|
elif choice == "5":
|
||||||
|
self._load_client_cert()
|
||||||
|
else:
|
||||||
|
print("Giving up.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.client_certs["active"]:
|
||||||
|
self.active_cert_domains.append(host)
|
||||||
|
self.client_certs[host] = self.client_certs["active"]
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _load_client_cert(self):
|
||||||
|
"""
|
||||||
|
Interactively load a TLS client certificate from the filesystem in PEM
|
||||||
|
format.
|
||||||
|
"""
|
||||||
|
print("Loading client certificate file, in PEM format (blank line to cancel)")
|
||||||
|
certfile = input("Certfile path: ").strip()
|
||||||
|
if not certfile:
|
||||||
|
print("Aborting.")
|
||||||
|
return
|
||||||
|
certfile = os.path.expanduser(certfile)
|
||||||
|
if not os.path.isfile(certfile):
|
||||||
|
print("Certificate file {} does not exist.".format(certfile))
|
||||||
|
return
|
||||||
|
print("Loading private key file, in PEM format (blank line to cancel)")
|
||||||
|
keyfile = input("Keyfile path: ").strip()
|
||||||
|
if not keyfile:
|
||||||
|
print("Aborting.")
|
||||||
|
return
|
||||||
|
keyfile = os.path.expanduser(keyfile)
|
||||||
|
if not os.path.isfile(keyfile):
|
||||||
|
print("Private key file {} does not exist.".format(keyfile))
|
||||||
|
return
|
||||||
|
self._activate_client_cert(certfile, keyfile)
|
||||||
|
|
||||||
|
def _generate_transient_cert_cert(self):
|
||||||
|
"""
|
||||||
|
Use `openssl` command to generate a new transient client certificate
|
||||||
|
with 24 hours of validity.
|
||||||
|
"""
|
||||||
|
certdir = os.path.join(self.config_dir, "transient_certs")
|
||||||
|
name = str(uuid.uuid4())
|
||||||
|
self._generate_client_cert(certdir, name, transient=True)
|
||||||
|
self.active_is_transient = True
|
||||||
|
self.transient_certs_created.append(name)
|
||||||
|
|
||||||
|
def _generate_persistent_client_cert(self):
|
||||||
|
"""
|
||||||
|
Interactively use `openssl` command to generate a new persistent client
|
||||||
|
certificate with one year of validity.
|
||||||
|
"""
|
||||||
|
certdir = os.path.join(self.config_dir, "client_certs")
|
||||||
|
print("What do you want to name this new certificate?")
|
||||||
|
print("Answering `mycert` will create `{0}/mycert.crt` and `{0}/mycert.key`".format(certdir))
|
||||||
|
name = input("> ")
|
||||||
|
if not name.strip():
|
||||||
|
print("Aborting.")
|
||||||
|
return
|
||||||
|
self._generate_client_cert(certdir, name)
|
||||||
|
|
||||||
|
def _generate_client_cert(self, certdir, basename, transient=False):
|
||||||
|
"""
|
||||||
|
Use `openssl` binary to generate a client certificate (which may be
|
||||||
|
transient or persistent) and save the certificate and private key to the
|
||||||
|
specified directory with the specified basename.
|
||||||
|
"""
|
||||||
|
if not os.path.exists(certdir):
|
||||||
|
os.makedirs(certdir)
|
||||||
|
certfile = os.path.join(certdir, basename+".crt")
|
||||||
|
keyfile = os.path.join(certdir, basename+".key")
|
||||||
|
cmd = "openssl req -x509 -newkey rsa:2048 -days {} -nodes -keyout {} -out {}".format(1 if transient else 365, keyfile, certfile)
|
||||||
|
if transient:
|
||||||
|
cmd += " -subj '/CN={}'".format(basename)
|
||||||
|
os.system(cmd)
|
||||||
|
self._activate_client_cert(certfile, keyfile)
|
||||||
|
|
||||||
|
def _choose_client_cert(self):
|
||||||
|
"""
|
||||||
|
Interactively select a previously generated client certificate and
|
||||||
|
activate it.
|
||||||
|
"""
|
||||||
|
certdir = os.path.join(self.config_dir, "client_certs")
|
||||||
|
certs = glob.glob(os.path.join(certdir, "*.crt"))
|
||||||
|
if len(certs) == 0:
|
||||||
|
print("There are no previously generated certificates.")
|
||||||
|
return
|
||||||
|
certdir = {}
|
||||||
|
for n, cert in enumerate(certs):
|
||||||
|
certdir[str(n+1)] = (cert, os.path.splitext(cert)[0] + ".key")
|
||||||
|
print("{}. {}".format(n+1, os.path.splitext(os.path.basename(cert))[0]))
|
||||||
|
choice = input("> ").strip()
|
||||||
|
if choice in certdir:
|
||||||
|
certfile, keyfile = certdir[choice]
|
||||||
|
self._activate_client_cert(certfile, keyfile)
|
||||||
|
else:
|
||||||
|
print("What?")
|
||||||
|
|
||||||
|
def _activate_client_cert(self, certfile, keyfile):
|
||||||
|
self.client_certs["active"] = (certfile, keyfile)
|
||||||
|
self.active_cert_domains = []
|
||||||
|
ui_out.debug("Using ID {} / {}.".format(*self.client_certs["active"]))
|
||||||
|
|
||||||
|
def _deactivate_client_cert(self):
|
||||||
|
if self.active_is_transient:
|
||||||
|
for filename in self.client_certs["active"]:
|
||||||
|
os.remove(filename)
|
||||||
|
for domain in self.active_cert_domains:
|
||||||
|
self.client_certs.pop(domain)
|
||||||
|
self.client_certs["active"] = None
|
||||||
|
self.active_cert_domains = []
|
||||||
|
self.active_is_transient = False
|
Loading…
Reference in New Issue