mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-11-07 05:32:20 +01:00
fix(security): refresh client trust and publish validated SearXNG images
Repair SearXNG system and certifi stores, validate native amd64/arm64 runtime images through Compose, and publish the exact verified artifacts through a trusted CI job. Adopt released Python, certifi, grpcio, uv and Docker CLI trust fixes, with strict controls for all fourteen public incident chains. Part of #6784 and #6790. Co-authored-by: Léo <leograndcontact@gmail.com>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
"""Remove known incident certificates without downloading packages or roots.
|
||||
|
||||
Inventory provenance: https://emudhra.we-have-a-problem.com/roots.json and
|
||||
https://emudhra.we-have-a-problem.com/impacted-intermediates.json; the additional
|
||||
cross-signed issuer is from the incident's public chains.zip.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import ssl
|
||||
import subprocess
|
||||
|
||||
|
||||
BLOCKED = json.loads(Path(__file__).with_name("blocked-certificates.json").read_text())
|
||||
PEM = re.compile(r"-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----", re.S)
|
||||
|
||||
|
||||
def certificates(text):
|
||||
blocks = PEM.findall(text)
|
||||
if not blocks or "-----BEGIN" in PEM.sub("", text):
|
||||
raise ValueError("empty or malformed certificate input")
|
||||
for block in blocks:
|
||||
# Validate the X.509 object as well as its base64 encoding.
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||
context.load_verify_locations(cadata=block)
|
||||
yield block, hashlib.sha256(ssl.PEM_cert_to_DER_cert(block)).hexdigest()
|
||||
|
||||
|
||||
def filter_bundle(path, *, allow_empty=False):
|
||||
original = list(certificates(path.read_text()))
|
||||
retained = [block for block, digest in original if digest not in BLOCKED]
|
||||
removed = len(original) - len(retained)
|
||||
if not retained and not allow_empty:
|
||||
raise ValueError(f"refusing to empty trust bundle: {path}")
|
||||
if removed:
|
||||
if retained:
|
||||
path.write_text("\n".join(retained) + "\n")
|
||||
else:
|
||||
path.unlink()
|
||||
return removed
|
||||
|
||||
|
||||
def check_bundle(path):
|
||||
blocks = list(certificates(path.read_text()))
|
||||
if any(digest in BLOCKED for _, digest in blocks):
|
||||
raise ValueError(f"incident certificate remains: {path}")
|
||||
return len(blocks)
|
||||
|
||||
|
||||
def main():
|
||||
import certifi
|
||||
|
||||
sources = list(Path("/usr/share/ca-certificates").rglob("*.crt"))
|
||||
sources += list(Path("/usr/local/share/ca-certificates").rglob("*.crt"))
|
||||
if not sources:
|
||||
raise ValueError("no system CA sources found")
|
||||
removed = sum(filter_bundle(path, allow_empty=True) for path in sources)
|
||||
removed += filter_bundle(Path(certifi.where()))
|
||||
# The upstream entrypoint also regenerates trust at root startup. Removing
|
||||
# source certificates ensures that it cannot restore the incident roots.
|
||||
subprocess.run(["update-ca-certificates", "--fresh"], check=True)
|
||||
paths = [Path("/etc/ssl/certs/ca-certificates.crt"), Path(certifi.where())]
|
||||
paths += [p for p in Path("/etc/ssl/certs").iterdir()
|
||||
if re.fullmatch(r"[0-9a-fA-F]{8}\.[0-9]+", p.name)]
|
||||
if len(paths) <= 2:
|
||||
raise ValueError("no hashed system CA entries generated")
|
||||
for path in paths:
|
||||
check_bundle(path)
|
||||
print(f"Removed {removed} incident certificates; checked {len(paths)} trust files")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user