112 lines
4.1 KiB
Python
112 lines
4.1 KiB
Python
"""Closed public NGINX runtime inventory, independent from TLS and service state."""
|
|
|
|
import re
|
|
import stat
|
|
from pathlib import Path
|
|
|
|
from .policy import DeliveryError, digest, fields, integer, read_json, sha256
|
|
|
|
VERSION = "1.30.5"
|
|
MAX_BYTES = 64 * 1024**2
|
|
|
|
|
|
def validate(value: dict) -> dict:
|
|
fields(
|
|
value,
|
|
{
|
|
"schema_version",
|
|
"format",
|
|
"version",
|
|
"architecture",
|
|
"package_sha256",
|
|
"files",
|
|
"directories",
|
|
"payload_bytes",
|
|
},
|
|
"proxy runtime",
|
|
)
|
|
if (
|
|
type(value["schema_version"]) is not int
|
|
or value["schema_version"] != 1
|
|
or value["format"] != "kf.nginx.runtime.v1"
|
|
or value["version"] != VERSION
|
|
or value["architecture"] != "x86_64"
|
|
or value["directories"] != ["bin", "lib"]
|
|
or not isinstance(value["files"], list)
|
|
or not 1 <= len(value["files"]) <= 32
|
|
):
|
|
raise DeliveryError("unsupported proxy runtime profile")
|
|
sha256(value["package_sha256"])
|
|
names = set()
|
|
total = 0
|
|
for entry in value["files"]:
|
|
fields(entry, {"path", "bytes", "sha256", "mode"}, "proxy runtime file")
|
|
name = entry["path"]
|
|
if not isinstance(name, str) or (
|
|
name != "bin/nginx" and not re.fullmatch(r"lib/[A-Za-z0-9_.+-]+\.so(?:\.[0-9]+)*", name)
|
|
):
|
|
raise DeliveryError("non-runtime or private proxy member")
|
|
if name in names or entry["mode"] != ("0555" if name == "bin/nginx" else "0444"):
|
|
raise DeliveryError("duplicate proxy member or unsafe mode")
|
|
names.add(name)
|
|
total += integer(entry["bytes"], 1, 32 * 1024**2, "proxy file bytes")
|
|
sha256(entry["sha256"])
|
|
if (
|
|
not {"bin/nginx", "lib/libc.so.6", "lib/ld-linux-x86-64.so.2"} <= names
|
|
or total > MAX_BYTES
|
|
or type(value["payload_bytes"]) is not int
|
|
or value["payload_bytes"] != total
|
|
):
|
|
raise DeliveryError("incomplete or unbounded proxy runtime")
|
|
return value
|
|
|
|
|
|
def verify(root: Path, expected: str) -> dict:
|
|
if root.is_symlink() or not root.is_dir():
|
|
raise DeliveryError("ordinary proxy runtime directory required")
|
|
catalog = root / "manifest.json"
|
|
if catalog.is_symlink() or digest(catalog) != sha256(expected):
|
|
raise DeliveryError("proxy runtime out-of-band fingerprint differs")
|
|
value = validate(read_json(catalog, 16384))
|
|
if {p.name for p in root.iterdir()} != {"manifest.json", "bin", "lib"}:
|
|
raise DeliveryError("unexpected proxy runtime tree member")
|
|
for directory in (root / "bin", root / "lib"):
|
|
if (
|
|
directory.is_symlink()
|
|
or not directory.is_dir()
|
|
or stat.S_IMODE(directory.stat().st_mode) != 0o555
|
|
):
|
|
raise DeliveryError("readonly ordinary proxy runtime directories required")
|
|
names = set()
|
|
for directory in (root / "bin", root / "lib"):
|
|
for p in directory.iterdir():
|
|
names.add(p.relative_to(root).as_posix())
|
|
if names != {entry["path"] for entry in value["files"]}:
|
|
raise DeliveryError("proxy runtime inventory differs")
|
|
for entry in value["files"]:
|
|
p = root / entry["path"]
|
|
info = p.lstat()
|
|
if (
|
|
not stat.S_ISREG(info.st_mode)
|
|
or info.st_nlink != 1
|
|
or info.st_size != entry["bytes"]
|
|
or stat.S_IMODE(info.st_mode) != int(entry["mode"], 8)
|
|
or digest(p) != entry["sha256"]
|
|
):
|
|
raise DeliveryError("proxy runtime bytes or permission differ")
|
|
return value
|
|
|
|
|
|
def copy_to_payload(root: Path, expected: str, destination: Path) -> None:
|
|
from .build import copy
|
|
|
|
value = verify(root, expected)
|
|
if destination.exists() or destination.is_symlink():
|
|
raise DeliveryError("proxy runtime payload must be new")
|
|
destination.mkdir()
|
|
for directory in value["directories"]:
|
|
(destination / directory).mkdir()
|
|
for entry in value["files"]:
|
|
copy(root / entry["path"], destination / entry["path"])
|
|
copy(root / "manifest.json", destination / "manifest.json")
|