Harden PKI license verification, database seat reconciliation, and binary packaging
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 1m27s

This commit is contained in:
2026-10-02 22:24:21 +02:00
parent 2c08f9fc15
commit f0c4a82fc3
5 changed files with 180 additions and 14 deletions
+16 -3
View File
@@ -1313,8 +1313,10 @@ def enroll_client(req: ClientEnrollRequest, request: Request = None):
conn.close()
raise HTTPException(status_code=500, detail="License configuration not initialized")
expected_secret, max_seats = row[0], row[1]
license_status = row[2] if len(row) > 2 and row[2] else ("active" if max_seats > 0 else "unlicensed")
expected_secret = row[0]
lic_summary = LicenseManager.get_license_summary(SERVER_STATE, db_path=db_path)
max_seats = lic_summary.get("max_seats", 0)
license_status = lic_summary.get("status", "UNLICENSED").lower()
if max_seats <= 0 or license_status in ("unlicensed", "revoked", "expired"):
conn.close()
raise HTTPException(status_code=403, detail="Server is unlicensed. Import an activated .lic file via the WebUI before enrolling edge clients.")
@@ -1893,9 +1895,20 @@ def import_signed_license(req: LicenseImportRequest, auth_user: str = Depends(re
cert_dir = SERVER_STATE.get("config", {}).get("cert_dir", "certs")
ca_file = os.path.join(cert_dir, "ca.crt")
upstream_file = os.path.join(cert_dir, "upstream_ca.crt")
upstream_pem = req.upstream_ca.strip() if req.upstream_ca else None
if not upstream_pem and os.path.exists(upstream_file):
try:
with open(upstream_file, "r", encoding="utf-8") as f:
upstream_pem = f.read().strip()
except Exception:
upstream_pem = None
if upstream_pem:
if not enrollment.verify_certificate_signature(req.ca_cert, upstream_pem):
raise HTTPException(status_code=400, detail="Cryptographic verification failed: Sub-CA certificate was not signed by the upstream Master Authority.")
try:
info = enrollment.extract_license_info_from_cert(req.ca_cert)
info = enrollment.extract_license_info_from_cert(req.ca_cert, upstream_ca_pem=upstream_pem)
except Exception as e:
raise HTTPException(status_code=400, detail=f"Failed to parse X.509 certificate: {e}")
+44 -9
View File
@@ -42,15 +42,23 @@ class LicenseManager:
def get_active_licensed_features(db_path: Optional[str] = None, server_state: Optional[Dict[str, Any]] = None) -> List[str]:
"""
Returns the list of all active enterprise capability identifiers
authorized either via X.509 certificate extensions or the persistent database license config.
authorized either via verified X.509 certificate extensions or the persistent database license config.
"""
features = set()
state = server_state or {}
cert_dir = state.get("config", {}).get("cert_dir", "certs")
upstream_pem = None
upstream_file = os.path.join(cert_dir, "upstream_ca.crt")
if os.path.exists(upstream_file):
try:
with open(upstream_file, "r", encoding="utf-8") as f:
upstream_pem = f.read()
except Exception:
pass
# 1. Check in-memory / active Subordinate CA certificate
ca_pem = state.get("ca_cert_pem")
if not ca_pem:
cert_dir = state.get("config", {}).get("cert_dir", "certs")
ca_file = os.path.join(cert_dir, "ca.crt")
if os.path.exists(ca_file):
try:
@@ -59,9 +67,11 @@ class LicenseManager:
except Exception:
pass
cert_is_verified = False
if ca_pem:
try:
info = enrollment.extract_license_info_from_cert(ca_pem)
info = enrollment.extract_license_info_from_cert(ca_pem, upstream_ca_pem=upstream_pem)
cert_is_verified = info.get("signature_verified", False) or not info.get("is_self_signed", False)
for feat in info.get("features", []):
if feat:
features.add(str(feat).strip())
@@ -105,14 +115,23 @@ class LicenseManager:
@staticmethod
def get_license_summary(server_state: Optional[Dict[str, Any]] = None, db_path: Optional[str] = None) -> Dict[str, Any]:
"""Inspects certs/ca.crt, database quotas, and returns comprehensive licensing status."""
"""Inspects certs/ca.crt, database quotas, verifies cryptographic signatures, and returns comprehensive licensing status."""
state = server_state or {}
config = state.get("config", {})
cert_dir = config.get("cert_dir", "certs")
ca_path = os.path.join(cert_dir, "ca.crt")
upstream_path = os.path.join(cert_dir, "upstream_ca.crt")
target_db = db_path or config.get("db_path", LicenseManager.DEFAULT_DB_FILE)
max_seats = config.get("max_seats", 0)
upstream_pem = None
if os.path.exists(upstream_path):
try:
with open(upstream_path, "r", encoding="utf-8") as f:
upstream_pem = f.read().strip()
except Exception:
pass
db_max_seats = config.get("max_seats", 0)
license_status_db = "unlicensed"
try:
conn = sqlite3.connect(target_db)
@@ -121,7 +140,7 @@ class LicenseManager:
row = c.fetchone()
if row:
if row[0] is not None:
max_seats = row[0]
db_max_seats = row[0]
if len(row) > 1 and row[1]:
license_status_db = row[1]
c.execute("SELECT COUNT(*) FROM clients WHERE status = 'active' AND cert_fingerprint != 'LOCAL-HOST-BUILTIN'")
@@ -136,18 +155,33 @@ class LicenseManager:
csr_path = os.path.join(cert_dir, "ca.csr")
csr_exists = os.path.exists(csr_path)
cert_seats = None
if os.path.exists(ca_path):
try:
with open(ca_path, "r", encoding="utf-8") as f:
ca_pem = f.read()
info = enrollment.extract_license_info_from_cert(ca_pem)
if info.get("seats"):
max_seats = info["seats"]
info = enrollment.extract_license_info_from_cert(ca_pem, upstream_ca_pem=upstream_pem)
if info.get("seats") is not None:
cert_seats = info["seats"]
except Exception:
info = {"valid": False}
else:
info = {"valid": False, "is_self_signed": False, "is_expired": False, "days_remaining": 0}
# Anti-tampering reconciliation: If database max_seats diverged from certified quota, enforce certified quota and update DB
if cert_seats is not None:
max_seats = cert_seats
if db_max_seats != cert_seats and os.path.exists(target_db):
try:
conn = sqlite3.connect(target_db)
conn.execute("UPDATE license_config SET max_seats = ? WHERE id = 1", (cert_seats,))
conn.commit()
conn.close()
except Exception:
pass
else:
max_seats = db_max_seats
license_id = info.get("license_id") or "UNLICENSED"
issuer_url = info.get("issuer_url") or config.get("license_server_url", "https://logar.eibl.tech")
@@ -176,6 +210,7 @@ class LicenseManager:
"issuer_cn": info.get("issuer_cn", "N/A"),
"subject_cn": info.get("subject_cn", "N/A"),
"is_self_signed": info.get("is_self_signed", False),
"signature_verified": info.get("signature_verified", False),
"not_valid_before": info.get("not_valid_before", "N/A"),
"not_valid_after": info.get("not_valid_after", "N/A"),
"days_remaining": info.get("days_remaining", 0),
+28 -1
View File
@@ -408,10 +408,30 @@ def check_and_renew_hub_pki(
return False, False
def extract_license_info_from_cert(cert_pem: str) -> Dict[str, Any]:
def verify_certificate_signature(cert_pem: Any, issuer_ca_pem: Any) -> bool:
"""
Cryptographically verifies that cert_pem was signed by the private key corresponding to issuer_ca_pem.
"""
try:
from cryptography.hazmat.primitives.asymmetric import padding
cert = x509.load_pem_x509_certificate(cert_pem.encode("utf-8")) if isinstance(cert_pem, str) else cert_pem
issuer = x509.load_pem_x509_certificate(issuer_ca_pem.encode("utf-8")) if isinstance(issuer_ca_pem, str) else issuer_ca_pem
issuer.public_key().verify(
cert.signature,
cert.tbs_certificate_bytes,
padding.PKCS1v15(),
cert.signature_hash_algorithm
)
return True
except Exception:
return False
def extract_license_info_from_cert(cert_pem: str, upstream_ca_pem: Optional[str] = None) -> Dict[str, Any]:
"""
Extracts embedded licensing attributes, validity, and metadata from a PEM X.509 certificate.
Supports decoding seat quota and license IDs embedded via LOGAR custom enterprise OIDs.
Cryptographically verifies the signature if upstream_ca_pem is provided.
"""
try:
cert = x509.load_pem_x509_certificate(cert_pem.encode("utf-8"))
@@ -490,6 +510,12 @@ def extract_license_info_from_cert(cert_pem: str) -> Dict[str, Any]:
break
is_self_signed = (cert.subject == cert.issuer)
signature_verified = False
if upstream_ca_pem:
signature_verified = verify_certificate_signature(cert, upstream_ca_pem)
elif is_self_signed:
signature_verified = verify_certificate_signature(cert, cert)
return {
"valid": True,
@@ -501,6 +527,7 @@ def extract_license_info_from_cert(cert_pem: str) -> Dict[str, Any]:
"issuer_cn": issuer_cn,
"is_ca": is_ca,
"is_self_signed": is_self_signed,
"signature_verified": signature_verified,
"fingerprint": cert.fingerprint(hashes.SHA256()).hex().upper(),
"not_valid_before": not_before.isoformat(),
"not_valid_after": not_after.isoformat(),