diff --git a/bebop/browser/gemini.py b/bebop/browser/gemini.py index c153c0e..cca6131 100644 --- a/bebop/browser/gemini.py +++ b/bebop/browser/gemini.py @@ -352,8 +352,9 @@ def create_identity(browser: Browser, url: str): return None browser.set_status("Generating certificate…") + gen_command = browser.config["generate_client_cert_command"] try: - mangled_name = create_certificate(url, common_name) + mangled_name = create_certificate(url, common_name, gen_command) except ClientCertificateException as exc: browser.set_status_error(exc.message) return None diff --git a/bebop/config.py b/bebop/config.py index d27b6ee..62701d0 100644 --- a/bebop/config.py +++ b/bebop/config.py @@ -16,6 +16,18 @@ DEFAULT_CONFIG = { "external_command_default": ["xdg-open"], "home": "bebop:welcome", "render_mode": "fancy", + "generate_client_cert_command": [ + "openssl", "req", + "-newkey", "rsa:4096", + "-nodes", + "-keyform", "PEM", + "-keyout", "{key_path}", + "-x509", + "-days", "28140", # https://www.youtube.com/watch?v=F9L4q-0Pi4E + "-outform", "PEM", + "-out", "{cert_path}", + "-subj", "/CN={common_name}", + ], } RENDER_MODES = ("fancy", "dumb") diff --git a/bebop/help.py b/bebop/help.py index e8f90f4..cb6792c 100644 --- a/bebop/help.py +++ b/bebop/help.py @@ -68,6 +68,7 @@ Here are the available options: * external_command_default (see note 1): default command to open files. * home (string): home page. * render_mode (string): default render mode to use ("fancy" or "dumb"). +* generate_client_cert_command (see note 3): command to generate a client cert. Notes: @@ -75,6 +76,8 @@ Notes: 2: the external_commands dict maps MIME types to commands just as above. For example, if you want to open video files with VLC and audio files in Clementine, you can use the following dict: `{"audio": ["clementine"], "video": ["vlc"]}`. For now only "main" MIME types are supported, i.e. you cannot specify precise types like "audio/flac", just "audio". +3: the generate_client_cert_command uses the same format as other commands (specified in note 1 above), with the exception that if the strings "{cert_path}", "{key_path}" or "{common_name}" are present in any string for the list, they will be replaced respectively by the certificate output path, the key output path and the CN to use. + Your current configuration is: """ diff --git a/bebop/identity.py b/bebop/identity.py index 7e4b98b..4b379ba 100644 --- a/bebop/identity.py +++ b/bebop/identity.py @@ -83,24 +83,23 @@ def get_cert_and_key(cert_id: str): return directory / f"{cert_id}.crt", directory / f"{cert_id}.key" -def create_certificate(url: str, common_name: str): +def create_certificate(url: str, common_name: str, gen_command: list): """Create a secure self-signed certificate using system's OpenSSL.""" identities_path = get_identities_path() mangled_name = get_mangled_name(url, common_name) cert_path = identities_path / f"{mangled_name}.crt" key_path = identities_path / f"{mangled_name}.key" - command = [ - "openssl", "req", - "-newkey", "rsa:4096", - "-nodes", - "-keyform", "PEM", - "-keyout", str(key_path), - "-x509", - "-days", "28140", # https://www.youtube.com/watch?v=F9L4q-0Pi4E - "-outform", "PEM", - "-out", str(cert_path), - "-subj", f"/CN={common_name}", - ] + + command = [] + for part in gen_command: + if "{key_path}" in part: + part = part.format(key_path=str(key_path)) + if "{cert_path}" in part: + part = part.format(cert_path=str(cert_path)) + if "{common_name}" in part: + part = part.format(common_name=common_name) + command.append(part) + try: subprocess.check_call( command, diff --git a/bebop/protocol.py b/bebop/protocol.py index ab044cb..bbf11b1 100644 --- a/bebop/protocol.py +++ b/bebop/protocol.py @@ -1,5 +1,6 @@ """Gemini protocol implementation.""" +import logging import re import socket import ssl @@ -157,7 +158,15 @@ class Request: # Setup TLS. context = Request.get_ssl_context() if self.identity: - context.load_cert_chain(*self.identity) + try: + context.load_cert_chain(*self.identity) + except FileNotFoundError as exc: + sock.close() + self.state = Request.STATE_CONNECTION_FAILED + self.error = "Could not load identity files." + logging.error(f"Failed to load identity files {self.identity}") + return False + try: self.ssock = context.wrap_socket(sock, server_hostname=hostname) except OSError as exc: