feat: strengthen BSP and PSP validation and handoff
This commit is contained in:
1 parent
bcea35a91e
commit
1641fac631
39 files changed
+2695
-90
No files matched your search
@@ -1,17 +1,22 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate a BSP-to-PSP handoff and its immutable artifact hashes."""
|
||||
"""Validate a BSP-to-PSP handoff, output permissions, and artifact hashes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import stat
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REQUIRED_IMAGE_MARGIN_BYTES = 512 * 1024
|
||||
REQUIRED_PARTITION_IMAGES = {"kernel.bin", "rootfs.bin"}
|
||||
DIRECTORY_ACCESS_BITS = 0o555
|
||||
FILE_READ_BITS = 0o444
|
||||
FILE_EXECUTE_BITS = 0o111
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
@@ -30,6 +35,159 @@ def within(path: Path, root: Path) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def validate_output_permissions(
|
||||
payload: object,
|
||||
build: dict[str, object],
|
||||
producer: dict[str, object],
|
||||
imgs_path: Path | None,
|
||||
errors: list[str],
|
||||
repair: bool,
|
||||
) -> dict[str, object] | None:
|
||||
"""Check BSP evidence and live modes; optionally repair only its exact output."""
|
||||
if not isinstance(payload, dict) or payload.get("passed") is not True:
|
||||
errors.append("BSP handoff 缺少通过的输出路径权限证据")
|
||||
return None
|
||||
if payload.get("schema_version") != 1 or payload.get("policy") != "a+rX":
|
||||
errors.append("输出路径权限证据的 schema_version/policy 不匹配")
|
||||
return None
|
||||
|
||||
root_value = payload.get("output_root")
|
||||
version_value = payload.get("imgs_version_path")
|
||||
project_name = producer.get("project")
|
||||
build_version = build.get("version")
|
||||
if (
|
||||
not isinstance(root_value, str)
|
||||
or not Path(root_value).is_absolute()
|
||||
or not isinstance(version_value, str)
|
||||
or not Path(version_value).is_absolute()
|
||||
or not isinstance(project_name, str)
|
||||
or project_name in ("", ".", "..")
|
||||
or Path(project_name).name != project_name
|
||||
or not isinstance(build_version, str)
|
||||
or build_version in ("", ".", "..")
|
||||
or Path(build_version).name != build_version
|
||||
or imgs_path is None
|
||||
):
|
||||
errors.append("输出路径权限证据缺少精确的绝对输出目录、项目或构建版本")
|
||||
return None
|
||||
output_root = Path(root_value)
|
||||
if (
|
||||
len(output_root.parts) < 3
|
||||
or output_root == Path.home()
|
||||
or output_root.is_symlink()
|
||||
):
|
||||
errors.append("输出路径权限证据的根路径过宽或是软链接")
|
||||
return None
|
||||
expected_imgs_path = output_root / project_name / "imgs" / build_version
|
||||
if Path(version_value) != expected_imgs_path or imgs_path != expected_imgs_path:
|
||||
errors.append("输出路径权限证据与 BSP 项目、构建版本或 imgs_version_path 不一致")
|
||||
return None
|
||||
|
||||
evidence_value = build.get("output_permissions_path")
|
||||
project_value = producer.get("project_path")
|
||||
if (
|
||||
not isinstance(project_value, str)
|
||||
or not Path(project_value).is_absolute()
|
||||
or not isinstance(evidence_value, str)
|
||||
or not Path(evidence_value).is_absolute()
|
||||
):
|
||||
errors.append("build.output_permissions_path 必须是 BSP 项目内的绝对路径")
|
||||
return None
|
||||
evidence_path = Path(evidence_value)
|
||||
expected_evidence = (
|
||||
Path(project_value)
|
||||
/ "doc" / "bsp_adaptation" / "validation"
|
||||
/ f"output_permissions_{build_version}.json"
|
||||
)
|
||||
if evidence_path != expected_evidence or evidence_path.is_symlink():
|
||||
errors.append("输出路径权限证据文件路径不是预期 BSP 项目内的普通文件")
|
||||
return None
|
||||
try:
|
||||
external_payload = json.loads(evidence_path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
errors.append(f"输出路径权限证据文件不可读或解析失败: {exc}")
|
||||
return None
|
||||
if external_payload != payload:
|
||||
errors.append("handoff 内嵌权限证据与外部证据文件不一致")
|
||||
return None
|
||||
|
||||
paths: list[tuple[Path, int, str]] = []
|
||||
for path in (
|
||||
output_root,
|
||||
output_root / project_name,
|
||||
output_root / project_name / "imgs",
|
||||
expected_imgs_path,
|
||||
):
|
||||
if path.is_symlink() or not path.is_dir():
|
||||
errors.append(f"构建输出路径节点不是普通目录: {path}")
|
||||
return None
|
||||
paths.append((path, DIRECTORY_ACCESS_BITS, "目录"))
|
||||
|
||||
def walk_error(exc: OSError) -> None:
|
||||
raise exc
|
||||
|
||||
skipped_symlinks = 0
|
||||
try:
|
||||
for parent, directory_names, file_names in os.walk(
|
||||
expected_imgs_path, followlinks=False, onerror=walk_error
|
||||
):
|
||||
parent_path = Path(parent)
|
||||
for name in list(directory_names):
|
||||
path = parent_path / name
|
||||
if path.is_symlink():
|
||||
directory_names.remove(name)
|
||||
skipped_symlinks += 1
|
||||
elif path.is_dir():
|
||||
paths.append((path, DIRECTORY_ACCESS_BITS, "目录"))
|
||||
else:
|
||||
errors.append(f"构建输出包含非普通目录: {path}")
|
||||
return None
|
||||
for name in file_names:
|
||||
path = parent_path / name
|
||||
if path.is_symlink():
|
||||
skipped_symlinks += 1
|
||||
elif path.is_file():
|
||||
bits = FILE_READ_BITS
|
||||
if stat.S_IMODE(path.stat().st_mode) & FILE_EXECUTE_BITS:
|
||||
bits |= FILE_EXECUTE_BITS
|
||||
paths.append((path, bits, "文件"))
|
||||
else:
|
||||
errors.append(f"构建输出包含非普通文件: {path}")
|
||||
return None
|
||||
except OSError as exc:
|
||||
errors.append(f"无法遍历构建输出目录: {exc}")
|
||||
return None
|
||||
|
||||
changed = 0
|
||||
for path, bits, kind in paths:
|
||||
try:
|
||||
if path.is_symlink():
|
||||
errors.append(f"权限处理拒绝软链接: {path}")
|
||||
return None
|
||||
before = stat.S_IMODE(path.stat().st_mode)
|
||||
if before & bits != bits:
|
||||
if not repair:
|
||||
errors.append(f"构建输出{kind}缺少共享读取/进入权限: {path}")
|
||||
continue
|
||||
os.chmod(path, before | bits, follow_symlinks=False)
|
||||
changed += 1
|
||||
after = stat.S_IMODE(path.stat().st_mode)
|
||||
if after & bits != bits:
|
||||
errors.append(f"构建输出{kind}权限修复后仍不满足要求: {path}")
|
||||
except OSError as exc:
|
||||
errors.append(f"无法检查或修复构建输出权限 {path}: {exc}")
|
||||
return {
|
||||
**payload,
|
||||
"current_check": {
|
||||
"passed": not errors,
|
||||
"repaired": changed > 0,
|
||||
"changed_count": changed,
|
||||
"checked_paths": len(paths),
|
||||
"symlinks_skipped": skipped_symlinks,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def validate_image_partition_margin(
|
||||
payload: object,
|
||||
build: dict[str, object],
|
||||
@@ -155,6 +313,7 @@ def validate_handoff(
|
||||
expected_sdk: str | None = None,
|
||||
expected_build_type: str | None = None,
|
||||
expected_system_form: str | None = None,
|
||||
repair_output_permissions: bool = False,
|
||||
) -> dict[str, object]:
|
||||
errors: list[str] = []
|
||||
warnings: list[str] = []
|
||||
@@ -196,6 +355,8 @@ def validate_handoff(
|
||||
for field, (actual, expected) in expected_values.items():
|
||||
if expected is not None and actual != expected:
|
||||
errors.append(f"{field} 不匹配:期望 {expected!r},实际 {actual!r}")
|
||||
if repair_output_permissions and expected_project is None:
|
||||
errors.append("自动修复输出权限时必须提供预期的 BSP 项目名 --project")
|
||||
|
||||
if build.get("status") != "build_passed" or build.get("exit_code") != 0:
|
||||
errors.append("BSP 构建状态不是 build_passed/exit_code=0")
|
||||
@@ -210,6 +371,16 @@ def validate_handoff(
|
||||
if not imgs_path.is_dir() or imgs_path.is_symlink():
|
||||
errors.append(f"imgs_version_path 不存在或不是普通目录: {imgs_path}")
|
||||
|
||||
# Never modify BSP output if the handoff identity is already inconsistent.
|
||||
output_permissions = validate_output_permissions(
|
||||
validation.get("output_permissions"),
|
||||
build,
|
||||
producer,
|
||||
imgs_path,
|
||||
errors,
|
||||
repair=repair_output_permissions and not errors and expected_project == producer.get("project"),
|
||||
)
|
||||
|
||||
verified = 0
|
||||
verified_artifacts: list[dict[str, object]] = []
|
||||
seen_relative_paths: set[str] = set()
|
||||
@@ -297,6 +468,8 @@ def validate_handoff(
|
||||
"verified_artifact_count": verified,
|
||||
"artifacts": verified_artifacts,
|
||||
"image_partition_margin": image_partition_margin,
|
||||
"output_permissions_path": build.get("output_permissions_path"),
|
||||
"output_permissions": output_permissions,
|
||||
"hardware_validation": validation.get("hardware"),
|
||||
"owner_acceptance": validation.get("owner_acceptance"),
|
||||
"release": validation.get("release"),
|
||||
@@ -312,6 +485,11 @@ def main() -> int:
|
||||
parser.add_argument("--sdk")
|
||||
parser.add_argument("--build-type")
|
||||
parser.add_argument("--system-form", choices=("single-os", "dual-os"))
|
||||
parser.add_argument(
|
||||
"--repair-output-permissions",
|
||||
action="store_true",
|
||||
help="仅对已核对项目身份及证据的本次 imgs 路径补齐 a+rX 权限",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
result = validate_handoff(
|
||||
@@ -321,6 +499,7 @@ def main() -> int:
|
||||
args.sdk,
|
||||
args.build_type,
|
||||
args.system_form,
|
||||
repair_output_permissions=args.repair_output_permissions,
|
||||
)
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
result = {"passed": False, "errors": [str(exc)], "warnings": []}
|
||||
|
||||
Reference in new issue
Block a user