mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-26 19:21:58 -06:00
Script Functionally FINALLY reinstated
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
"""
|
"""
|
||||||
Self-signed certificate management for Borealis.
|
Server TLS certificate management.
|
||||||
|
|
||||||
The production Flask server and the Vite dev server both consume the same
|
Borealis now issues a dedicated root CA and a leaf server certificate so that
|
||||||
certificate chain so agents and browsers can pin a single trust anchor during
|
agents can pin the CA without requiring a re-enrollment every time the server
|
||||||
enrollment.
|
certificate is refreshed. The CA is persisted alongside the server key so that
|
||||||
|
existing deployments can be upgraded in-place.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -12,32 +13,36 @@ import os
|
|||||||
import ssl
|
import ssl
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Tuple
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
from cryptography import x509
|
from cryptography import x509
|
||||||
from cryptography.hazmat.primitives import hashes, serialization
|
from cryptography.hazmat.primitives import hashes, serialization
|
||||||
from cryptography.hazmat.primitives.asymmetric import ec
|
from cryptography.hazmat.primitives.asymmetric import ec
|
||||||
from cryptography.x509.oid import NameOID
|
from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID
|
||||||
|
|
||||||
from Modules.runtime import ensure_server_certificates_dir, server_certificates_path, runtime_path
|
from Modules.runtime import ensure_server_certificates_dir, runtime_path, server_certificates_path
|
||||||
|
|
||||||
_CERT_DIR = server_certificates_path()
|
_CERT_DIR = server_certificates_path()
|
||||||
_CERT_FILE = _CERT_DIR / "borealis-server-cert.pem"
|
_CERT_FILE = _CERT_DIR / "borealis-server-cert.pem"
|
||||||
_KEY_FILE = _CERT_DIR / "borealis-server-key.pem"
|
_KEY_FILE = _CERT_DIR / "borealis-server-key.pem"
|
||||||
_BUNDLE_FILE = _CERT_DIR / "borealis-server-bundle.pem"
|
_BUNDLE_FILE = _CERT_DIR / "borealis-server-bundle.pem"
|
||||||
|
_CA_KEY_FILE = _CERT_DIR / "borealis-root-ca-key.pem"
|
||||||
|
_CA_CERT_FILE = _CERT_DIR / "borealis-root-ca.pem"
|
||||||
|
|
||||||
_LEGACY_CERT_DIR = runtime_path("certs")
|
_LEGACY_CERT_DIR = runtime_path("certs")
|
||||||
_LEGACY_CERT_FILE = _LEGACY_CERT_DIR / "borealis-server-cert.pem"
|
_LEGACY_CERT_FILE = _LEGACY_CERT_DIR / "borealis-server-cert.pem"
|
||||||
_LEGACY_KEY_FILE = _LEGACY_CERT_DIR / "borealis-server-key.pem"
|
_LEGACY_KEY_FILE = _LEGACY_CERT_DIR / "borealis-server-key.pem"
|
||||||
_LEGACY_BUNDLE_FILE = _LEGACY_CERT_DIR / "borealis-server-bundle.pem"
|
_LEGACY_BUNDLE_FILE = _LEGACY_CERT_DIR / "borealis-server-bundle.pem"
|
||||||
|
|
||||||
# 100-year lifetime (effectively "never" for self-signed deployments).
|
_ROOT_COMMON_NAME = "Borealis Root CA"
|
||||||
_CERT_VALIDITY = timedelta(days=365 * 100)
|
_ORG_NAME = "Borealis"
|
||||||
|
_ROOT_VALIDITY = timedelta(days=365 * 100)
|
||||||
|
_SERVER_VALIDITY = timedelta(days=365 * 5)
|
||||||
|
|
||||||
|
|
||||||
def ensure_certificate(common_name: str = "Borealis Server") -> Tuple[Path, Path, Path]:
|
def ensure_certificate(common_name: str = "Borealis Server") -> Tuple[Path, Path, Path]:
|
||||||
"""
|
"""
|
||||||
Ensure the self-signed certificate and key exist on disk.
|
Ensure the root CA, server certificate, and bundle exist on disk.
|
||||||
|
|
||||||
Returns (cert_path, key_path, bundle_path).
|
Returns (cert_path, key_path, bundle_path).
|
||||||
"""
|
"""
|
||||||
@@ -45,87 +50,210 @@ def ensure_certificate(common_name: str = "Borealis Server") -> Tuple[Path, Path
|
|||||||
ensure_server_certificates_dir()
|
ensure_server_certificates_dir()
|
||||||
_migrate_legacy_material_if_present()
|
_migrate_legacy_material_if_present()
|
||||||
|
|
||||||
regenerate = not (_CERT_FILE.exists() and _KEY_FILE.exists())
|
ca_key, ca_cert, ca_regenerated = _ensure_root_ca()
|
||||||
if not regenerate:
|
|
||||||
try:
|
|
||||||
with _CERT_FILE.open("rb") as fh:
|
|
||||||
cert = x509.load_pem_x509_certificate(fh.read())
|
|
||||||
try:
|
|
||||||
expiry = cert.not_valid_after_utc # type: ignore[attr-defined]
|
|
||||||
except AttributeError:
|
|
||||||
expiry = cert.not_valid_after.replace(tzinfo=timezone.utc)
|
|
||||||
if expiry <= datetime.now(tz=timezone.utc):
|
|
||||||
regenerate = True
|
|
||||||
except Exception:
|
|
||||||
regenerate = True
|
|
||||||
|
|
||||||
if regenerate:
|
server_cert = _load_certificate(_CERT_FILE)
|
||||||
_generate_certificate(common_name)
|
needs_regen = ca_regenerated or _server_certificate_needs_regeneration(server_cert, ca_cert)
|
||||||
|
if needs_regen:
|
||||||
|
server_cert = _generate_server_certificate(common_name, ca_key, ca_cert)
|
||||||
|
|
||||||
if not _BUNDLE_FILE.exists():
|
if server_cert is None:
|
||||||
_BUNDLE_FILE.write_bytes(_CERT_FILE.read_bytes())
|
server_cert = _generate_server_certificate(common_name, ca_key, ca_cert)
|
||||||
|
|
||||||
|
_write_bundle(server_cert, ca_cert)
|
||||||
|
|
||||||
return _CERT_FILE, _KEY_FILE, _BUNDLE_FILE
|
return _CERT_FILE, _KEY_FILE, _BUNDLE_FILE
|
||||||
|
|
||||||
|
|
||||||
def _migrate_legacy_material_if_present() -> None:
|
def _migrate_legacy_material_if_present() -> None:
|
||||||
if _CERT_FILE.exists() and _KEY_FILE.exists():
|
# Promote legacy runtime certificates (Server/Borealis/certs) into the new location.
|
||||||
return
|
if not _CERT_FILE.exists() or not _KEY_FILE.exists():
|
||||||
|
legacy_cert = _LEGACY_CERT_FILE
|
||||||
|
legacy_key = _LEGACY_KEY_FILE
|
||||||
|
if legacy_cert.exists() and legacy_key.exists():
|
||||||
|
try:
|
||||||
|
ensure_server_certificates_dir()
|
||||||
|
if not _CERT_FILE.exists():
|
||||||
|
_safe_copy(legacy_cert, _CERT_FILE)
|
||||||
|
if not _KEY_FILE.exists():
|
||||||
|
_safe_copy(legacy_key, _KEY_FILE)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
legacy_cert = _LEGACY_CERT_FILE
|
|
||||||
legacy_key = _LEGACY_KEY_FILE
|
|
||||||
legacy_bundle = _LEGACY_BUNDLE_FILE
|
|
||||||
|
|
||||||
if not legacy_cert.exists() or not legacy_key.exists():
|
def _ensure_root_ca() -> Tuple[ec.EllipticCurvePrivateKey, x509.Certificate, bool]:
|
||||||
return
|
regenerated = False
|
||||||
|
|
||||||
|
ca_key: Optional[ec.EllipticCurvePrivateKey] = None
|
||||||
|
ca_cert: Optional[x509.Certificate] = None
|
||||||
|
|
||||||
|
if _CA_KEY_FILE.exists() and _CA_CERT_FILE.exists():
|
||||||
|
try:
|
||||||
|
ca_key = _load_private_key(_CA_KEY_FILE)
|
||||||
|
ca_cert = _load_certificate(_CA_CERT_FILE)
|
||||||
|
if ca_cert is not None and ca_key is not None:
|
||||||
|
expiry = _cert_not_after(ca_cert)
|
||||||
|
subject = ca_cert.subject
|
||||||
|
subject_cn = ""
|
||||||
|
try:
|
||||||
|
subject_cn = subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value # type: ignore[index]
|
||||||
|
except Exception:
|
||||||
|
subject_cn = ""
|
||||||
|
try:
|
||||||
|
basic = ca_cert.extensions.get_extension_for_class(x509.BasicConstraints).value # type: ignore[attr-defined]
|
||||||
|
is_ca = bool(basic.ca)
|
||||||
|
except Exception:
|
||||||
|
is_ca = False
|
||||||
|
if (
|
||||||
|
expiry <= datetime.now(tz=timezone.utc)
|
||||||
|
or not is_ca
|
||||||
|
or subject_cn != _ROOT_COMMON_NAME
|
||||||
|
):
|
||||||
|
regenerated = True
|
||||||
|
else:
|
||||||
|
regenerated = True
|
||||||
|
except Exception:
|
||||||
|
regenerated = True
|
||||||
|
else:
|
||||||
|
regenerated = True
|
||||||
|
|
||||||
|
if regenerated or ca_key is None or ca_cert is None:
|
||||||
|
ca_key = ec.generate_private_key(ec.SECP384R1())
|
||||||
|
public_key = ca_key.public_key()
|
||||||
|
|
||||||
|
now = datetime.now(tz=timezone.utc)
|
||||||
|
builder = (
|
||||||
|
x509.CertificateBuilder()
|
||||||
|
.subject_name(
|
||||||
|
x509.Name(
|
||||||
|
[
|
||||||
|
x509.NameAttribute(NameOID.COMMON_NAME, _ROOT_COMMON_NAME),
|
||||||
|
x509.NameAttribute(NameOID.ORGANIZATION_NAME, _ORG_NAME),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.issuer_name(
|
||||||
|
x509.Name(
|
||||||
|
[
|
||||||
|
x509.NameAttribute(NameOID.COMMON_NAME, _ROOT_COMMON_NAME),
|
||||||
|
x509.NameAttribute(NameOID.ORGANIZATION_NAME, _ORG_NAME),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.public_key(public_key)
|
||||||
|
.serial_number(x509.random_serial_number())
|
||||||
|
.not_valid_before(now - timedelta(minutes=5))
|
||||||
|
.not_valid_after(now + _ROOT_VALIDITY)
|
||||||
|
.add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True)
|
||||||
|
.add_extension(
|
||||||
|
x509.KeyUsage(
|
||||||
|
digital_signature=True,
|
||||||
|
content_commitment=False,
|
||||||
|
key_encipherment=False,
|
||||||
|
data_encipherment=False,
|
||||||
|
key_agreement=False,
|
||||||
|
key_cert_sign=True,
|
||||||
|
crl_sign=True,
|
||||||
|
encipher_only=False,
|
||||||
|
decipher_only=False,
|
||||||
|
),
|
||||||
|
critical=True,
|
||||||
|
)
|
||||||
|
.add_extension(
|
||||||
|
x509.SubjectKeyIdentifier.from_public_key(public_key),
|
||||||
|
critical=False,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
builder = builder.add_extension(
|
||||||
|
x509.AuthorityKeyIdentifier.from_issuer_public_key(public_key),
|
||||||
|
critical=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
ca_cert = builder.sign(private_key=ca_key, algorithm=hashes.SHA384())
|
||||||
|
|
||||||
|
_CA_KEY_FILE.write_bytes(
|
||||||
|
ca_key.private_bytes(
|
||||||
|
encoding=serialization.Encoding.PEM,
|
||||||
|
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||||
|
encryption_algorithm=serialization.NoEncryption(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_CA_CERT_FILE.write_bytes(ca_cert.public_bytes(serialization.Encoding.PEM))
|
||||||
|
|
||||||
|
_tighten_permissions(_CA_KEY_FILE)
|
||||||
|
_tighten_permissions(_CA_CERT_FILE)
|
||||||
|
else:
|
||||||
|
regenerated = False
|
||||||
|
|
||||||
|
return ca_key, ca_cert, regenerated
|
||||||
|
|
||||||
|
|
||||||
|
def _server_certificate_needs_regeneration(
|
||||||
|
server_cert: Optional[x509.Certificate],
|
||||||
|
ca_cert: x509.Certificate,
|
||||||
|
) -> bool:
|
||||||
|
if server_cert is None:
|
||||||
|
return True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ensure_server_certificates_dir()
|
if server_cert.issuer != ca_cert.subject:
|
||||||
if not _CERT_FILE.exists():
|
return True
|
||||||
try:
|
|
||||||
legacy_cert.replace(_CERT_FILE)
|
|
||||||
except Exception:
|
|
||||||
_CERT_FILE.write_bytes(legacy_cert.read_bytes())
|
|
||||||
if not _KEY_FILE.exists():
|
|
||||||
try:
|
|
||||||
legacy_key.replace(_KEY_FILE)
|
|
||||||
except Exception:
|
|
||||||
_KEY_FILE.write_bytes(legacy_key.read_bytes())
|
|
||||||
if legacy_bundle.exists() and not _BUNDLE_FILE.exists():
|
|
||||||
try:
|
|
||||||
legacy_bundle.replace(_BUNDLE_FILE)
|
|
||||||
except Exception:
|
|
||||||
_BUNDLE_FILE.write_bytes(legacy_bundle.read_bytes())
|
|
||||||
except Exception:
|
except Exception:
|
||||||
return
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
expiry = _cert_not_after(server_cert)
|
||||||
|
if expiry <= datetime.now(tz=timezone.utc):
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
basic = server_cert.extensions.get_extension_for_class(x509.BasicConstraints).value # type: ignore[attr-defined]
|
||||||
|
if basic.ca:
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
eku = server_cert.extensions.get_extension_for_class(x509.ExtendedKeyUsage).value # type: ignore[attr-defined]
|
||||||
|
if ExtendedKeyUsageOID.SERVER_AUTH not in eku:
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _generate_certificate(common_name: str) -> None:
|
def _generate_server_certificate(
|
||||||
|
common_name: str,
|
||||||
|
ca_key: ec.EllipticCurvePrivateKey,
|
||||||
|
ca_cert: x509.Certificate,
|
||||||
|
) -> x509.Certificate:
|
||||||
private_key = ec.generate_private_key(ec.SECP384R1())
|
private_key = ec.generate_private_key(ec.SECP384R1())
|
||||||
public_key = private_key.public_key()
|
public_key = private_key.public_key()
|
||||||
|
|
||||||
now = datetime.now(tz=timezone.utc)
|
now = datetime.now(tz=timezone.utc)
|
||||||
|
ca_expiry = _cert_not_after(ca_cert)
|
||||||
|
candidate_expiry = now + _SERVER_VALIDITY
|
||||||
|
not_after = min(ca_expiry - timedelta(days=1), candidate_expiry)
|
||||||
|
|
||||||
builder = (
|
builder = (
|
||||||
x509.CertificateBuilder()
|
x509.CertificateBuilder()
|
||||||
.subject_name(
|
.subject_name(
|
||||||
x509.Name(
|
x509.Name(
|
||||||
[
|
[
|
||||||
x509.NameAttribute(NameOID.COMMON_NAME, common_name),
|
x509.NameAttribute(NameOID.COMMON_NAME, common_name),
|
||||||
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Borealis"),
|
x509.NameAttribute(NameOID.ORGANIZATION_NAME, _ORG_NAME),
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.issuer_name(
|
|
||||||
x509.Name(
|
|
||||||
[
|
|
||||||
x509.NameAttribute(NameOID.COMMON_NAME, common_name),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.issuer_name(ca_cert.subject)
|
||||||
.public_key(public_key)
|
.public_key(public_key)
|
||||||
.serial_number(x509.random_serial_number())
|
.serial_number(x509.random_serial_number())
|
||||||
.not_valid_before(now - timedelta(minutes=5))
|
.not_valid_before(now - timedelta(minutes=5))
|
||||||
.not_valid_after(now + _CERT_VALIDITY)
|
.not_valid_after(not_after)
|
||||||
.add_extension(
|
.add_extension(
|
||||||
x509.SubjectAlternativeName(
|
x509.SubjectAlternativeName(
|
||||||
[
|
[
|
||||||
@@ -136,10 +264,36 @@ def _generate_certificate(common_name: str) -> None:
|
|||||||
),
|
),
|
||||||
critical=False,
|
critical=False,
|
||||||
)
|
)
|
||||||
.add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True)
|
.add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True)
|
||||||
|
.add_extension(
|
||||||
|
x509.KeyUsage(
|
||||||
|
digital_signature=True,
|
||||||
|
content_commitment=False,
|
||||||
|
key_encipherment=False,
|
||||||
|
data_encipherment=False,
|
||||||
|
key_agreement=False,
|
||||||
|
key_cert_sign=False,
|
||||||
|
crl_sign=False,
|
||||||
|
encipher_only=False,
|
||||||
|
decipher_only=False,
|
||||||
|
),
|
||||||
|
critical=True,
|
||||||
|
)
|
||||||
|
.add_extension(
|
||||||
|
x509.ExtendedKeyUsage([ExtendedKeyUsageOID.SERVER_AUTH]),
|
||||||
|
critical=False,
|
||||||
|
)
|
||||||
|
.add_extension(
|
||||||
|
x509.SubjectKeyIdentifier.from_public_key(public_key),
|
||||||
|
critical=False,
|
||||||
|
)
|
||||||
|
.add_extension(
|
||||||
|
x509.AuthorityKeyIdentifier.from_issuer_public_key(ca_key.public_key()),
|
||||||
|
critical=False,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
certificate = builder.sign(private_key=private_key, algorithm=hashes.SHA384())
|
certificate = builder.sign(private_key=ca_key, algorithm=hashes.SHA384())
|
||||||
|
|
||||||
_KEY_FILE.write_bytes(
|
_KEY_FILE.write_bytes(
|
||||||
private_key.private_bytes(
|
private_key.private_bytes(
|
||||||
@@ -150,10 +304,30 @@ def _generate_certificate(common_name: str) -> None:
|
|||||||
)
|
)
|
||||||
_CERT_FILE.write_bytes(certificate.public_bytes(serialization.Encoding.PEM))
|
_CERT_FILE.write_bytes(certificate.public_bytes(serialization.Encoding.PEM))
|
||||||
|
|
||||||
# Propagate filesystem permissions to restrict accidental disclosure.
|
|
||||||
_tighten_permissions(_KEY_FILE)
|
_tighten_permissions(_KEY_FILE)
|
||||||
_tighten_permissions(_CERT_FILE)
|
_tighten_permissions(_CERT_FILE)
|
||||||
|
|
||||||
|
return certificate
|
||||||
|
|
||||||
|
|
||||||
|
def _write_bundle(server_cert: x509.Certificate, ca_cert: x509.Certificate) -> None:
|
||||||
|
try:
|
||||||
|
server_pem = server_cert.public_bytes(serialization.Encoding.PEM).decode("utf-8").strip()
|
||||||
|
ca_pem = ca_cert.public_bytes(serialization.Encoding.PEM).decode("utf-8").strip()
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
|
bundle = f"{server_pem}\n{ca_pem}\n"
|
||||||
|
_BUNDLE_FILE.write_text(bundle, encoding="utf-8")
|
||||||
|
_tighten_permissions(_BUNDLE_FILE)
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_copy(src: Path, dst: Path) -> None:
|
||||||
|
try:
|
||||||
|
dst.write_bytes(src.read_bytes())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _tighten_permissions(path: Path) -> None:
|
def _tighten_permissions(path: Path) -> None:
|
||||||
try:
|
try:
|
||||||
@@ -163,11 +337,33 @@ def _tighten_permissions(path: Path) -> None:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _load_private_key(path: Path) -> ec.EllipticCurvePrivateKey:
|
||||||
|
with path.open("rb") as fh:
|
||||||
|
return serialization.load_pem_private_key(fh.read(), password=None)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_certificate(path: Path) -> Optional[x509.Certificate]:
|
||||||
|
try:
|
||||||
|
return x509.load_pem_x509_certificate(path.read_bytes())
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _cert_not_after(cert: x509.Certificate) -> datetime:
|
||||||
|
try:
|
||||||
|
return cert.not_valid_after_utc # type: ignore[attr-defined]
|
||||||
|
except AttributeError:
|
||||||
|
value = cert.not_valid_after
|
||||||
|
if value.tzinfo is None:
|
||||||
|
return value.replace(tzinfo=timezone.utc)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
def build_ssl_context() -> ssl.SSLContext:
|
def build_ssl_context() -> ssl.SSLContext:
|
||||||
cert_path, key_path, _bundle_path = ensure_certificate()
|
cert_path, key_path, bundle_path = ensure_certificate()
|
||||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||||
context.minimum_version = ssl.TLSVersion.TLSv1_3
|
context.minimum_version = ssl.TLSVersion.TLSv1_3
|
||||||
context.load_cert_chain(certfile=str(cert_path), keyfile=str(key_path))
|
context.load_cert_chain(certfile=str(bundle_path), keyfile=str(key_path))
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8323,6 +8323,6 @@ if __name__ == "__main__":
|
|||||||
app,
|
app,
|
||||||
host="0.0.0.0",
|
host="0.0.0.0",
|
||||||
port=5000,
|
port=5000,
|
||||||
certfile=TLS_CERT_PATH,
|
certfile=TLS_BUNDLE_PATH,
|
||||||
keyfile=TLS_KEY_PATH,
|
keyfile=TLS_KEY_PATH,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user