Skip to content

Setup TLS

Setting up TLS is a painful becuase generating the keys and certificates. Thus, FAsterAPI included utility function to do it for you!

generate_root_ca()

Create a root CA certificate and private key.

Parameters:

Name Type Description Default
expiration_days int

the nuber of days before expiration. Defaults to 3650.

3650
common_name str

the common name. Defaults to "Root CA".

'Root CA'
subject_alternative_names Optional[List[str]]

the subject alternative names. Defaults to None.

None
directory Optional[str]

the directory to save the files. Defaults to None.

None

Returns:

Type Description
Tuple[RSAPrivateKey, Certificate]

Tuple[rsa.RSAPrivateKey, Certificate]: returns the CA key and certifcate

Source code in FasterAPI\utils.py
def generate_root_ca(
    expiration_days: int = 3650,
    common_name: str = "Root CA",
    subject_alternative_names: Optional[List[str]] = None,
    directory: Optional[str] = None,
) -> Tuple[rsa.RSAPrivateKey, Certificate]:
    """Create a root CA certificate and private key.

    Args:
        expiration_days (int, optional): the nuber of days before expiration. Defaults to 3650.
        common_name (str, optional): the common name. Defaults to "Root CA".
        subject_alternative_names (Optional[List[str]], optional): the subject alternative names. Defaults to None.
        directory (Optional[str], optional): the directory to save the files. Defaults to None.

    Returns:
        Tuple[rsa.RSAPrivateKey, Certificate]: returns the CA key and certifcate
    """

    key = rsa.generate_private_key(
        public_exponent=65537, key_size=2048, backend=default_backend()
    )

    subject = issuer = x509.Name(
        [
            x509.NameAttribute(NameOID.COMMON_NAME, common_name),
        ]
    )

    builder = x509.CertificateBuilder()
    builder = builder.subject_name(subject)
    builder = builder.issuer_name(issuer)
    builder = builder.public_key(key.public_key())
    builder = builder.serial_number(x509.random_serial_number())
    builder = builder.not_valid_before(datetime.now(timezone.utc))
    builder = builder.not_valid_after(
        datetime.now(timezone.utc) + timedelta(days=expiration_days)
    )
    builder = builder.add_extension(
        x509.BasicConstraints(ca=True, path_length=None),
        critical=True,
    )
    if subject_alternative_names:
        san_dns_names = x509.SubjectAlternativeName(
            [x509.DNSName(name) for name in subject_alternative_names]
        )
        builder = builder.add_extension(san_dns_names, critical=False)
    cert = builder.sign(key, hashes.SHA256(), default_backend())

    if directory:
        with open(f"{directory}/root-key.pem", "wb") as f:
            f.write(
                key.private_bytes(
                    encoding=serialization.Encoding.PEM,
                    format=serialization.PrivateFormat.TraditionalOpenSSL,
                    encryption_algorithm=serialization.NoEncryption(),
                )
            )
        with open(f"{directory}/root-cert.pem", "wb") as f:
            f.write(cert.public_bytes(encoding=serialization.Encoding.PEM))

    return key, cert

The above function create a root ca (key and certificate). If a directory is given, the generated files will be saved as PEM format. This should be your first step for server certifcate generation.

generate_key_and_csr()

Generate a private key and certificate signing request (CSR).

Parameters:

Name Type Description Default
common_name str

the common name of the server.

required
san_dns_names List[str]

the subject alternative names of the server.

required
directory Optional[str]

the directory to save the files. Defaults to None.

None

Returns:

Name Type Description
Certificate Tuple[RSAPrivateKey, CertificateSigningRequest]

returns the sever private key and certifcate signing request.

Source code in FasterAPI\utils.py
def generate_key_and_csr(
    common_name: str, san_dns_names: List[str], directory: Optional[str] = None
) -> Tuple[rsa.RSAPrivateKey, CertificateSigningRequest]:
    """Generate a private key and certificate signing request (CSR).

    Args:
        common_name (str): the common name of the server.
        san_dns_names (List[str]): the subject alternative names of the server.
        directory (Optional[str], optional): the directory to save the files. Defaults to None.

    Returns:
        Certificate: returns the sever private key and certifcate signing request.
    """
    key = rsa.generate_private_key(
        public_exponent=65537, key_size=2048, backend=default_backend()
    )

    subject = x509.Name(
        [
            x509.NameAttribute(NameOID.COMMON_NAME, common_name),
        ]
    )

    if san_dns_names:
        extensions = x509.SubjectAlternativeName(
            [x509.DNSName(name) for name in san_dns_names]
        )
    else:
        extensions = x509.SubjectAlternativeName(
            [x509.DNSName(common_name)]
        )

    csr = (
        x509.CertificateSigningRequestBuilder()
        .subject_name(subject)
        .add_extension(extensions, critical=False)
        .sign(key, hashes.SHA256(), default_backend())
    )

    if directory:
        with open(f"{directory}/server-key.pem", "wb") as f:
            f.write(
                key.private_bytes(
                    encoding=serialization.Encoding.PEM,
                    format=serialization.PrivateFormat.TraditionalOpenSSL,
                    encryption_algorithm=serialization.NoEncryption(),
                )
            )
        with open(f"{directory}/server-csr.pem", "wb") as f:
            f.write(csr.public_bytes(encoding=serialization.Encoding.PEM))

    return key, csr

The above function generate the private key and certificate signing request for your sever. If a directory is given, both files will be saved as PEM format. Note that, the signing request (CSR) is not a certifcate yet. It must be signed by a CA, which can be the one created by step one or your own.

sign_certificate()

Sign the certifcate signing request

Parameters:

Name Type Description Default
csr CertificateSigningRequest

the certificate signing request.

required
issuer_key Optional[RSAPrivateKey]

the issuer private key.

None
issuer_key_path Optional[str]

the issuer private key path.

None
issuer_cert Optional[Certificate]

the issuer certificate.

None
issuer_cert_path Optional[str]

the issuer certificate path.

None
validity_days int

the number of days before expiration. Defaults to 365.

365
directory Optional[str]

the directory to save the files. Defaults to None.

None

Raises:

Type Description
IssuerKeyNotDefined

raise if both issuer_key and issuer_key_path are provided or both ot provided.

IssuerCertNotDefined

raise if both issuer_cert and issuer_cert_path are provided or both ot provided.

Returns:

Name Type Description
Certificate Certificate

returns the signed certificate.

Source code in FasterAPI\utils.py
def sign_certificate(
    csr: CertificateSigningRequest,
    issuer_key: Optional[rsa.RSAPrivateKey] = None,
    issuer_key_path: Optional[str] = None,
    issuer_cert: Optional[Certificate] = None,
    issuer_cert_path: Optional[str] = None,
    validity_days=365,
    directory: Optional[str] = None,
) -> Certificate:
    """Sign the certifcate signing request

    Args:
        csr (CertificateSigningRequest): the certificate signing request.
        issuer_key (Optional[rsa.RSAPrivateKey]): the issuer private key.
        issuer_key_path (Optional[str]): the issuer private key path.
        issuer_cert (Optional[Certificate]): the issuer certificate.
        issuer_cert_path (Optional[str]): the issuer certificate path.
        validity_days (int, optional): the number of days before expiration. Defaults to 365.
        directory (Optional[str], optional): the directory to save the files. Defaults to None.

    Raises:
        IssuerKeyNotDefined: raise if both issuer_key and issuer_key_path are provided or both ot provided.
        IssuerCertNotDefined: raise if both issuer_cert and issuer_cert_path are provided or both ot provided.

    Returns:
        Certificate: returns the signed certificate.
    """

    class IssuerKeyNotDefined(Exception):
        pass

    class IssuerCertNotDefined(Exception):
        pass

    if issuer_key and issuer_key_path is None:
        _issuer_key = issuer_key
    elif issuer_key_path and issuer_key is None:
        with open(issuer_key_path, "rb") as key_file:
            _issuer_key = serialization.load_pem_private_key(
                key_file.read(), password=None, backend=default_backend()
            )
    else:
        raise IssuerKeyNotDefined(
            "Either issuer_key or issuer_key_path must be provided.")

    if issuer_cert and issuer_cert_path is None:
        _issuer_cert = issuer_cert
    elif issuer_cert_path and issuer_cert is None:
        with open(issuer_cert_path, "rb") as cert_file:
            _issuer_cert = x509.load_pem_x509_certificate(
                cert_file.read(), default_backend()
            )
    else:
        raise IssuerCertNotDefined(
            "Either issuer_cert or issuer_cert_path must be provided.")

    cert = (
        x509.CertificateBuilder()
        .subject_name(csr.subject)
        .issuer_name(_issuer_cert.subject)
        .public_key(csr.public_key())
        .serial_number(x509.random_serial_number())
        .not_valid_before(datetime.now(timezone.utc))
        .not_valid_after(datetime.now(timezone.utc) + timedelta(days=validity_days))
        .add_extension(
            x509.BasicConstraints(ca=False, path_length=None),
            critical=True,
        )
        .add_extension(
            csr.extensions.get_extension_for_class(
                x509.SubjectAlternativeName).value,
            critical=False,
        )
        .sign(_issuer_key, hashes.SHA256(), default_backend())  # type: ignore
    )

    if directory:
        with open(f"{directory}/server-cert.pem", "wb") as f:
            f.write(cert.public_bytes(encoding=serialization.Encoding.PEM))
    return cert

The above function signs the server certifcate with a CA given by your choice.

Using TLS

uvicorn.run(
    "FasterAPI.app:app",
    host="0.0.0.0",
    port=PORT,
    ssl_certfile="path/to/server/certifcate",
    ssl_keyfile="path/to/server/key",
)