134 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			134 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
import os
 | 
						|
import threading
 | 
						|
import time
 | 
						|
from subprocess import PIPE, Popen
 | 
						|
from uuid import uuid4
 | 
						|
 | 
						|
from . import debug, info
 | 
						|
 | 
						|
 | 
						|
def join_with_script_dir(path: str) -> str:
 | 
						|
    return os.path.join(os.path.dirname(os.path.abspath(__file__)), path)
 | 
						|
 | 
						|
 | 
						|
class CertificateCreator:
 | 
						|
    cakey = join_with_script_dir("ca.key")
 | 
						|
    cacert = join_with_script_dir("ca.crt")
 | 
						|
    certkey = join_with_script_dir("cert.key")
 | 
						|
    certdir = join_with_script_dir("certs/")
 | 
						|
 | 
						|
    lock = threading.Lock()
 | 
						|
 | 
						|
    def validate(self) -> None:
 | 
						|
        # Verify the CertificateAuthority files exist
 | 
						|
        if not os.path.isfile(CertificateCreator.cakey):
 | 
						|
            raise Exception(f"Cannot find {CertificateCreator.cakey}")
 | 
						|
        if not os.path.isfile(CertificateCreator.cacert):
 | 
						|
            raise Exception(f"Cannot find {CertificateCreator.cacert}")
 | 
						|
        if not os.path.isfile(CertificateCreator.certkey):
 | 
						|
            raise Exception(f"Cannot find {CertificateCreator.certkey}")
 | 
						|
        if not os.path.isdir(CertificateCreator.certdir):
 | 
						|
            raise Exception(f"Cannot find {CertificateCreator.certdir}")
 | 
						|
        # Verify the `certs` dir is reachable
 | 
						|
        try:
 | 
						|
            test_file_location = f"{CertificateCreator.certdir}/{uuid4()}.txt"
 | 
						|
            debug(
 | 
						|
                f"Writing test file to {test_file_location} to verify the directory is writable..."
 | 
						|
            )
 | 
						|
            with open(test_file_location, "w") as file:
 | 
						|
                file.write("test")
 | 
						|
            os.remove(test_file_location)
 | 
						|
        except Exception:
 | 
						|
            info("Failed to write test file")
 | 
						|
            info(
 | 
						|
                f"The directory {CertificateCreator.certdir} does not seem to be writable"
 | 
						|
            )
 | 
						|
            raise
 | 
						|
 | 
						|
    def create(self, path: str) -> str:
 | 
						|
        """
 | 
						|
        Create an SSL certificate for the supplied hostname.
 | 
						|
        This method will return a path to the certificate.
 | 
						|
        """
 | 
						|
        full_name = path.split(":")[0]
 | 
						|
 | 
						|
        with CertificateCreator.lock:
 | 
						|
            # We don't want to create certificates for every possible endpoint
 | 
						|
            # Especially with randomly named S3-buckets
 | 
						|
 | 
						|
            # We can create certificates that match wildcards to reduce the total number
 | 
						|
            # For example:
 | 
						|
            # Hostname:      somebucket.s3.amazonaws.com
 | 
						|
            # Certificate:   *.s3.amazonaws.com
 | 
						|
            #
 | 
						|
            # All requests that match this wildcard certificate will reuse it
 | 
						|
 | 
						|
            wildcard_name = f"*.{'.'.join(full_name.split('.')[1:])}"
 | 
						|
            server_csr = f"{self.certdir.rstrip('/')}/{wildcard_name}.csr"
 | 
						|
 | 
						|
            # Verify if the certificate already exists
 | 
						|
            certpath = f"{self.certdir.rstrip('/')}/{wildcard_name}.crt"
 | 
						|
            if not os.path.isfile(certpath):
 | 
						|
                # Create a Config-file that contains the wildcard-name
 | 
						|
                with open(f"{self.certdir.rstrip('/')}/req.conf.tmpl", "r") as f:
 | 
						|
                    config_template = f.read()
 | 
						|
                config_template = config_template.replace("{{full_name}}", full_name)
 | 
						|
                config_template = config_template.replace(
 | 
						|
                    "{{wildcard_name}}", wildcard_name
 | 
						|
                )
 | 
						|
                config_template_name = (
 | 
						|
                    f"{self.certdir.rstrip('/')}/{wildcard_name}.conf"
 | 
						|
                )
 | 
						|
                with open(config_template_name, "w") as f:
 | 
						|
                    f.write(config_template)
 | 
						|
 | 
						|
                # Create an Certificate Signing Request
 | 
						|
                #
 | 
						|
                subject = f"/CN={full_name}"[0:64]
 | 
						|
                commands = [
 | 
						|
                    "openssl",
 | 
						|
                    "req",
 | 
						|
                    "-new",
 | 
						|
                    "-key",
 | 
						|
                    self.certkey,
 | 
						|
                    "-out",
 | 
						|
                    server_csr,
 | 
						|
                ]
 | 
						|
                commands.extend(["-subj", subject, "-config", config_template_name])
 | 
						|
 | 
						|
                p1 = Popen(commands)
 | 
						|
                p1.communicate()
 | 
						|
                debug(f"Created CSR in {server_csr}")
 | 
						|
 | 
						|
                # Create the actual certificate used by the requests
 | 
						|
                p2 = Popen(
 | 
						|
                    [
 | 
						|
                        "openssl",
 | 
						|
                        "x509",
 | 
						|
                        "-req",
 | 
						|
                        "-in",
 | 
						|
                        server_csr,
 | 
						|
                        "-days",
 | 
						|
                        "3650",
 | 
						|
                        "-CA",
 | 
						|
                        self.cacert,
 | 
						|
                        "-CAkey",
 | 
						|
                        self.cakey,
 | 
						|
                        "-set_serial",
 | 
						|
                        f"{int(time.time() * 1000)}",
 | 
						|
                        "-out",
 | 
						|
                        certpath,
 | 
						|
                        "-extensions",
 | 
						|
                        "req_ext",
 | 
						|
                        "-extfile",
 | 
						|
                        config_template_name,
 | 
						|
                    ],
 | 
						|
                    stderr=PIPE,
 | 
						|
                )
 | 
						|
                p2.communicate()
 | 
						|
                debug(f"Created certificate for {path} called {certpath}")
 | 
						|
                os.remove(server_csr)
 | 
						|
                os.remove(config_template_name)
 | 
						|
                debug(f"Removed intermediate certificates for {certpath}")
 | 
						|
        return certpath
 |