ENGINE: Moved Certificate Store Location

This commit is contained in:
2025-10-29 00:25:30 -06:00
parent 4478f7c33b
commit 11a08f04a8
7 changed files with 519 additions and 15 deletions

View File

@@ -8,8 +8,8 @@
"intervalSec": "10", "intervalSec": "10",
"label": "API Request", "label": "API Request",
"result": "{\n \"status\": \"ok\"\n}", "result": "{\n \"status\": \"ok\"\n}",
"url": "http://localhost:5000/health", "url": "https://localhost:5000/health",
"useProxy": "true" "useProxy": "false"
}, },
"dragHandle": ".borealis-node-header", "dragHandle": ".borealis-node-header",
"dragging": false, "dragging": false,

View File

@@ -23,6 +23,7 @@ import time
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from .security import certificates as engine_certificates
from .server import EngineContext, create_app from .server import EngineContext, create_app
@@ -231,13 +232,8 @@ def _ensure_web_ui_build(staging_root: Path, logger: logging.Logger, *, mode: st
def _ensure_tls_material(context: EngineContext) -> None: def _ensure_tls_material(context: EngineContext) -> None:
"""Ensure TLS certificate material exists, updating the context if created.""" """Ensure TLS certificate material exists, updating the context if created."""
try: # Lazy import so Engine still starts if legacy modules are unavailable.
from Modules.crypto import certificates # type: ignore
except Exception:
return
try: try:
cert_path, key_path, bundle_path = certificates.ensure_certificate() cert_path, key_path, bundle_path = engine_certificates.ensure_certificate()
except Exception as exc: except Exception as exc:
context.logger.error("Failed to auto-provision Engine TLS certificates: %s", exc) context.logger.error("Failed to auto-provision Engine TLS certificates: %s", exc)
return return

View File

@@ -31,8 +31,9 @@ defaults that mirror the legacy server runtime. Key environment variables are
``DOMAIN``). ``DOMAIN``).
``BOREALIS_TLS_*`` TLS certificate, private key, and bundle paths. ``BOREALIS_TLS_*`` TLS certificate, private key, and bundle paths.
When TLS values are not provided explicitly the Engine falls back to the When TLS values are not provided explicitly the Engine provisions certificates
certificate helper shipped with the legacy server, ensuring bundling parity. under ``Engine/Certificates`` (migrating any legacy material) so the runtime
remains self-contained.
Logs are written to ``Logs/Engine/engine.log`` with daily rotation and Logs are written to ``Logs/Engine/engine.log`` with daily rotation and
errors are additionally duplicated to ``Logs/Engine/error.log`` so the errors are additionally duplicated to ``Logs/Engine/error.log`` so the
runtime integrates with the platform's logging policy. runtime integrates with the platform's logging policy.
@@ -47,10 +48,7 @@ from logging.handlers import TimedRotatingFileHandler
from pathlib import Path from pathlib import Path
from typing import Any, Iterable, List, Mapping, MutableMapping, Optional, Sequence, Tuple from typing import Any, Iterable, List, Mapping, MutableMapping, Optional, Sequence, Tuple
try: # pragma: no-cover - optional dependency during early migration stages. from .security import certificates
from Modules.crypto import certificates # type: ignore
except Exception: # pragma: no-cover - Engine configuration still works without it.
certificates = None # type: ignore[assignment]
ENGINE_DIR = Path(__file__).resolve().parent ENGINE_DIR = Path(__file__).resolve().parent

View File

@@ -0,0 +1,12 @@
# ======================================================
# Data\Engine\security\__init__.py
# Description: Exposes Engine-specific security helpers including TLS certificates and code-signing utilities.
#
# API Endpoints (if applicable): None
# ======================================================
"""Security helper exports for the Borealis Engine runtime."""
from . import certificates, signing
__all__ = ["certificates", "signing"]

View File

@@ -0,0 +1,408 @@
# ======================================================
# Data\Engine\security\certificates.py
# Description: Generates and maintains Engine TLS certificate material under Engine/Certificates without legacy fallback.
#
# API Endpoints (if applicable): None
# ======================================================
"""Engine-managed TLS certificate helpers.
The Engine runtime keeps its writable artefacts under ``Engine/`` so they can
be regenerated when the staging tree under ``Data/`` is refreshed. This
module provisions the server's TLS certificates within ``Engine/Certificates``
and ensures a sibling ``Engine/Certificates/Code-Signing`` directory exists so
code-signing keys can live alongside the TLS bundle. No legacy migration or
fallback is performed; the Engine manages its own material exclusively.
"""
from __future__ import annotations
import os
from datetime import datetime, timedelta, timezone
from functools import lru_cache
from pathlib import Path
from typing import Optional, Tuple
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID
__all__ = [
"certificate_paths",
"ensure_certificate",
"engine_certificates_root",
"engine_code_signing_root",
"project_root_path",
]
_ROOT_COMMON_NAME = "Borealis Root CA"
_ORG_NAME = "Borealis"
_ROOT_VALIDITY = timedelta(days=365 * 100)
_SERVER_VALIDITY = timedelta(days=365 * 5)
def _env_path(name: str) -> Optional[Path]:
value = os.environ.get(name)
if not value:
return None
try:
return Path(value).expanduser().resolve()
except Exception:
try:
return Path(value).expanduser()
except Exception:
return Path(value)
@lru_cache(maxsize=None)
def _project_root() -> Path:
env = _env_path("BOREALIS_PROJECT_ROOT")
if env and env.is_dir():
return env
current = Path(__file__).resolve()
for parent in (current, *current.parents):
if (parent / "Borealis.ps1").is_file():
return parent
try:
return current.parents[3]
except IndexError:
return current.parent
def project_root_path() -> Path:
"""Expose the detected project root."""
return _project_root()
@lru_cache(maxsize=None)
def _engine_runtime_root() -> Path:
env = _env_path("BOREALIS_ENGINE_ROOT") or _env_path("BOREALIS_ENGINE_RUNTIME")
if env:
env.mkdir(parents=True, exist_ok=True)
return env
root = _project_root() / "Engine"
root.mkdir(parents=True, exist_ok=True)
return root
@lru_cache(maxsize=None)
def engine_certificates_root() -> Path:
"""Base directory for Engine TLS and code-signing certificates."""
env = _env_path("BOREALIS_ENGINE_CERT_ROOT")
if env:
env.mkdir(parents=True, exist_ok=True)
return env
root = _engine_runtime_root() / "Certificates"
root.mkdir(parents=True, exist_ok=True)
return root
@lru_cache(maxsize=None)
def engine_code_signing_root() -> Path:
"""Location under which Engine code-signing keys are stored."""
root = engine_certificates_root() / "Code-Signing"
root.mkdir(parents=True, exist_ok=True)
return root
_CERT_DIR = engine_certificates_root()
_CERT_FILE = _CERT_DIR / "borealis-server-cert.pem"
_KEY_FILE = _CERT_DIR / "borealis-server-key.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"
def _tighten_permissions(path: Path) -> None:
try:
if os.name != "nt":
path.chmod(0o600)
except Exception:
pass
def _load_private_key(path: Path) -> Optional[ec.EllipticCurvePrivateKey]:
try:
return serialization.load_pem_private_key(path.read_bytes(), password=None)
except Exception:
return 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 _ensure_root_ca() -> Tuple[ec.EllipticCurvePrivateKey, x509.Certificate, bool]:
regenerated = False
ca_key = None
ca_cert = None
if _CA_KEY_FILE.exists() and _CA_CERT_FILE.exists():
ca_key = _load_private_key(_CA_KEY_FILE)
ca_cert = _load_certificate(_CA_CERT_FILE)
if ca_key is None or ca_cert is None:
regenerated = True
else:
expiry = _cert_not_after(ca_cert)
try:
subject = ca_cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value # type: ignore[index]
except Exception:
subject = ""
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 subject != _ROOT_COMMON_NAME or not is_ca:
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)
cert_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,
)
)
ca_cert = 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.PKCS8,
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)
return ca_key, ca_cert, regenerated
def _server_certificate_needs_regeneration(
certificate: Optional[x509.Certificate],
issuer: x509.Certificate,
) -> bool:
if certificate is None:
return True
try:
subject_cn = certificate.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value # type: ignore[index]
except Exception:
subject_cn = ""
if subject_cn != "Borealis Server":
return True
try:
eku = certificate.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
try:
san = certificate.extensions.get_extension_for_class(x509.SubjectAlternativeName).value # type: ignore[attr-defined]
names = {entry.value for entry in san.get_values_for_type(x509.DNSName)}
except Exception:
names = set()
if {"localhost", "127.0.0.1", "::1"} - names:
return True
try:
aki = certificate.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier).value # type: ignore[attr-defined]
ski = issuer.extensions.get_extension_for_class(x509.SubjectKeyIdentifier).value # type: ignore[attr-defined]
if aki.key_identifier != ski.key_identifier:
return True
except Exception:
return True
expiry = _cert_not_after(certificate)
if expiry <= datetime.now(tz=timezone.utc):
return True
return False
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())
public_key = private_key.public_key()
now = datetime.now(tz=timezone.utc)
not_after = now + _SERVER_VALIDITY
builder = (
x509.CertificateBuilder()
.subject_name(
x509.Name(
[
x509.NameAttribute(NameOID.COMMON_NAME, common_name),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, _ORG_NAME),
]
)
)
.issuer_name(ca_cert.subject)
.public_key(public_key)
.serial_number(x509.random_serial_number())
.not_valid_before(now - timedelta(minutes=5))
.not_valid_after(not_after)
.add_extension(
x509.SubjectAlternativeName(
[
x509.DNSName("localhost"),
x509.DNSName("127.0.0.1"),
x509.DNSName("::1"),
]
),
critical=False,
)
.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=ca_key, algorithm=hashes.SHA384())
_KEY_FILE.write_bytes(
private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
)
_CERT_FILE.write_bytes(certificate.public_bytes(serialization.Encoding.PEM))
_tighten_permissions(_KEY_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 ensure_certificate(common_name: str = "Borealis Server") -> Tuple[Path, Path, Path]:
"""
Ensure the Engine TLS certificate bundle exists.
Returns (cert_path, key_path, bundle_path).
"""
engine_certificates_root()
engine_code_signing_root()
ca_key, ca_cert, ca_regenerated = _ensure_root_ca()
existing_cert = _load_certificate(_CERT_FILE)
if _server_certificate_needs_regeneration(existing_cert, ca_cert) or ca_regenerated:
existing_cert = _generate_server_certificate(common_name, ca_key, ca_cert)
if existing_cert is None:
existing_cert = _generate_server_certificate(common_name, ca_key, ca_cert)
_write_bundle(existing_cert, ca_cert)
return _CERT_FILE, _KEY_FILE, _BUNDLE_FILE
def certificate_paths() -> Tuple[str, str, str]:
cert_path, key_path, bundle_path = ensure_certificate()
return str(cert_path), str(key_path), str(bundle_path)

View File

@@ -0,0 +1,90 @@
# ======================================================
# Data\Engine\security\signing.py
# Description: Manages Engine code-signing keys under Engine/Certificates/Code-Signing without legacy fallbacks.
#
# API Endpoints (if applicable): None
# ======================================================
"""Engine code-signing helper utilities."""
from __future__ import annotations
import base64
import os
from pathlib import Path
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ed25519
from .certificates import engine_code_signing_root
__all__ = ["ScriptSigner", "load_signer", "base64_from_spki_der"]
_KEY_DIR = engine_code_signing_root()
_SIGNING_KEY_FILE = _KEY_DIR / "borealis-script-ed25519.key"
_SIGNING_PUB_FILE = _KEY_DIR / "borealis-script-ed25519.pub"
def base64_from_spki_der(spki_der: bytes) -> str:
"""Return Base64 representation for SubjectPublicKeyInfo DER bytes."""
return base64.b64encode(spki_der).decode("ascii")
def _tighten_permissions(path: Path) -> None:
try:
if os.name != "nt":
path.chmod(0o600)
except Exception:
pass
class ScriptSigner:
"""Wrap an Ed25519 private key with convenience helpers."""
def __init__(self, private_key: ed25519.Ed25519PrivateKey):
self._private = private_key
self._public = private_key.public_key()
def sign(self, payload: bytes) -> bytes:
return self._private.sign(payload)
def public_spki_der(self) -> bytes:
return self._public.public_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
def public_base64_spki(self) -> str:
return base64_from_spki_der(self.public_spki_der())
def load_signer() -> ScriptSigner:
"""Load (or create) the Engine script-signing key pair."""
private_key = _load_or_create()
return ScriptSigner(private_key)
def _load_or_create() -> ed25519.Ed25519PrivateKey:
_KEY_DIR.mkdir(parents=True, exist_ok=True)
if _SIGNING_KEY_FILE.exists():
with _SIGNING_KEY_FILE.open("rb") as handle:
return serialization.load_pem_private_key(handle.read(), password=None)
private_key = ed25519.Ed25519PrivateKey.generate()
pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption(),
)
_SIGNING_KEY_FILE.write_bytes(pem)
_tighten_permissions(_SIGNING_KEY_FILE)
pub_der = private_key.public_key().public_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
_SIGNING_PUB_FILE.write_bytes(pub_der)
return private_key

View File

@@ -24,7 +24,7 @@ from Modules.auth import jwt_service as jwt_service_module
from Modules.auth.device_auth import DeviceAuthManager from Modules.auth.device_auth import DeviceAuthManager
from Modules.auth.dpop import DPoPValidator from Modules.auth.dpop import DPoPValidator
from Modules.auth.rate_limit import SlidingWindowRateLimiter from Modules.auth.rate_limit import SlidingWindowRateLimiter
from Modules.crypto import signing from ...security import signing
from Modules.enrollment import routes as enrollment_routes from Modules.enrollment import routes as enrollment_routes
from Modules.enrollment.nonce_store import NonceCache from Modules.enrollment.nonce_store import NonceCache
from Modules.tokens import routes as token_routes from Modules.tokens import routes as token_routes