Harden PKI license verification, database seat reconciliation, and binary packaging
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 1m27s
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 1m27s
This commit is contained in:
+16
-3
@@ -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
@@ -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),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user