From 187b090244289384ab044e2daf2518cfefc96f36 Mon Sep 17 00:00:00 2001 From: maksyuki Date: Sat, 26 Sep 2026 15:11:09 +0800 Subject: [PATCH] feat: add CRYPTO-P2 targets and reporting script for synthesis, netlist, and STA validation --- Makefile | 21 +++++- docs/ip/crypto-verification.md | 3 + docs/ip/crypto.md | 18 +++++ scripts/README.md | 3 + scripts/crypto_p2.py | 128 +++++++++++++++++++++++++++++++++ 5 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 scripts/crypto_p2.py diff --git a/Makefile b/Makefile index fa1137c6..1091ee62 100644 --- a/Makefile +++ b/Makefile @@ -431,7 +431,7 @@ crypto-p0-baseline: manifest crypto-p0-report: manifest $(PYTHON) $(ROOT_PATH)/scripts/crypto_p0.py report --variant-root $(VARIANT_ROOT) -.PHONY: crypto-p1-constants crypto-p1-rtl crypto-p1-formal crypto-p1-synth crypto-p1-report +.PHONY: crypto-p1-constants crypto-p1-rtl crypto-p1-formal crypto-p1-synth crypto-p1-report crypto-p2-synth crypto-p2-netlist crypto-p2-sta crypto-p2-report crypto-p1-constants: manifest $(PYTHON) $(ROOT_PATH)/scripts/crypto_p1.py constants --variant-root $(VARIANT_ROOT) crypto-p1-rtl: manifest @@ -451,6 +451,25 @@ crypto-p1-report: manifest $(PYTHON) $(ROOT_PATH)/scripts/crypto_p1.py report --variant-root $(VARIANT_ROOT) \ --baseline-root $(CRYPTO_P0_BASELINE_ROOT) --lp-variant-root $(CRYPTO_P1_LP_ROOT) \ $(if $(CRYPTO_P1_CI_ROOT),--ci-variant-root $(CRYPTO_P1_CI_ROOT),) +CRYPTO_P1_ROOT ?= $(VARIANT_ROOT) +CRYPTO_P2_NETLIST_ROOT ?= $(VARIANT_ROOT) +CRYPTO_P2_STA_ROOT ?= $(VARIANT_ROOT) +CRYPTO_P2_PHYSICAL_ROOT ?= $(VARIANT_ROOT) +crypto-p2-synth: manifest + $(MAKE) CONFIG=$(ROOT_PATH)/configs/ci/ihp130.mk BUILD_TIMESTAMP=$(BUILD_TIMESTAMP) \ + SYNTH=YOSYS SYNTH_RECIPE=balanced synth +crypto-p2-netlist: manifest + $(MAKE) CONFIG=$(ROOT_PATH)/configs/ci/ihp130.mk BUILD_TIMESTAMP=$(BUILD_TIMESTAMP) \ + APP=bringup APP_SRCS=$(ROOT_PATH)/tests/c/crypto_firmware.c LINK_TYPE=ld2_all_sram \ + SIMU=IVERILOG SYNTH=YOSYS HAVE_SVA=YES firmware netcomp netsim +crypto-p2-sta: manifest + $(MAKE) CONFIG=$(ROOT_PATH)/configs/ci/ihp130.mk BUILD_TIMESTAMP=$(BUILD_TIMESTAMP) \ + SYNTH=YOSYS STA=OPENSTA synth sta +crypto-p2-report: manifest + $(PYTHON) $(ROOT_PATH)/scripts/crypto_p2.py report --variant-root $(VARIANT_ROOT) \ + --p0-root $(CRYPTO_P0_BASELINE_ROOT) --p1-root $(CRYPTO_P1_ROOT) \ + --netlist-root $(CRYPTO_P2_NETLIST_ROOT) --sta-root $(CRYPTO_P2_STA_ROOT) \ + --physical-root $(CRYPTO_P2_PHYSICAL_ROOT) .PHONY: help config doctor setup setup-regression setup-mpw setup-vexiiriscv setup-clusterip setup-ip setup-pdk setup-app setup-apu-reference setup-apu-kws-reference apu-p5-bundle apu-p5-corpus apu-p7-model apu-p9-coefficients apu-p9-evidence apu-p9-memory-ab setup-hp-linux hp-linux hp-bundle hp-linux-sim hp-smoke-bundle hp-smoke-sim hp-apu-bundle hp-apu-sim \ clean-all purge-cache manifest check-warnings metrics check-metrics package commercial-package \ diff --git a/docs/ip/crypto-verification.md b/docs/ip/crypto-verification.md index 1cea6aaf..b7c08693 100644 --- a/docs/ip/crypto-verification.md +++ b/docs/ip/crypto-verification.md @@ -44,6 +44,9 @@ functioning macro responses and a running clock; they do not establish SRAM analog correctness, arbitrary silicon fault tolerance, DPA/FIA resistance or physical timing. P2 still owns full-chip synthesis, functional netlist simulation, STA, physical views and isolated clean-revision A/B measurement. +The `crypto-p2-report` target is fail-closed on the same clean-revision and +artifact requirements; it cannot convert a dirty worktree or missing netlist, +STA or physical result into a qualification PASS. Run the committed `configs/ci/ihp130.mk` profile through `crypto-p1-rtl`, `crypto-p1-formal` and `crypto-p1-synth`, then the recorded LP/CI firmware runs diff --git a/docs/ip/crypto.md b/docs/ip/crypto.md index 5f3ff196..96153594 100644 --- a/docs/ip/crypto.md +++ b/docs/ip/crypto.md @@ -882,6 +882,24 @@ make CONFIG=configs/ci/ihp130.mk librelane-doctor git diff --check ``` +The Crypto-specific P2 wrappers are: + +```sh +make CONFIG=configs/ci/ihp130.mk SYNTH=YOSYS BUILD_TIMESTAMP= crypto-p2-synth +make CONFIG=configs/ci/ihp130.mk BUILD_TIMESTAMP= crypto-p2-netlist +make CONFIG=configs/ci/ihp130.mk SYNTH=YOSYS STA=OPENSTA BUILD_TIMESTAMP= crypto-p2-sta +make CONFIG=configs/ci/ihp130.mk BUILD_TIMESTAMP= \ + CRYPTO_P0_BASELINE_ROOT=build/ CRYPTO_P1_ROOT=build/ \ + CRYPTO_P2_NETLIST_ROOT=build/ CRYPTO_P2_STA_ROOT=build/ \ + CRYPTO_P2_PHYSICAL_ROOT=build/ crypto-p2-report +``` + +`crypto-p2-netlist` runs the public `tests/c/crypto_firmware.c` image through +the Icarus synthesized SoC netlist, so Crypto is exercised through LP APB/DMA +and no RTL hierarchy or SRAM preload. The P2 report is fail-closed: a dirty +candidate, missing netlist/STA/physical artifact, mismatched profile, stale +P1 evidence or absent P0 report produces `incomplete`, never PASS. + Use a single explicit BUILD_TIMESTAMP for commands that must consume the same variant; behavioral and synthesis configuration differences must be recorded. Python tooling changes additionally require Ruff/Pytest under repository diff --git a/scripts/README.md b/scripts/README.md index 317b7c3f..d95aee6d 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -209,6 +209,9 @@ build/simulation and binds the actual configuration, input snapshot, ELF and log/result hashes. `crypto_p1.py quality --variant-root ` records focused formatter and policy results. Report assembly rejects stale inputs or artifacts; it cannot qualify an earlier binary using current source hashes. +`crypto_p2.py` is the fail-closed P2 aggregator. It requires a clean candidate +revision and records whole-SoC netlist, STA, physical, metrics, warning and +P0/P1 evidence; missing P2 artifacts remain `NOT_RUN`. Update or add tests in [`../tests`](../tests) for script behavior. Run `ruff check .` and `python3 -m pytest -q`; build-flow changes also require the diff --git a/scripts/crypto_p2.py b/scripts/crypto_p2.py new file mode 100644 index 00000000..e340ce8e --- /dev/null +++ b/scripts/crypto_p2.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +"""Fail-closed CRYPTO-P2 synthesis, netlist, STA and physical evidence report.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +from pathlib import Path +import subprocess + +ROOT = Path(__file__).resolve().parents[1] +BASELINE_REVISION = "92830d963da2f1e39bb219c0d377bc4889c07755" + + +def digest(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def dump(path: Path, value: object) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n") + + +def manifest(root: Path) -> dict: + path = root / "meta/manifest.json" + return json.loads(path.read_text()) if path.exists() else {} + + +def clean_revision() -> tuple[bool, str]: + status = subprocess.check_output(["git", "status", "--porcelain"], cwd=ROOT, text=True) + head = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=ROOT, text=True).strip() + return not status, head + + +def artifact(path: Path, expected_status: str | None = None) -> dict: + if not path.exists(): + return {"status": "NOT_RUN", "path": str(path)} + try: + value = json.loads(path.read_text()) + except (OSError, json.JSONDecodeError) as error: + return {"status": "INVALID", "path": str(path), "error": str(error)} + value.setdefault("path", str(path)) + if expected_status is not None and value.get("status") != expected_status: + value["status_check"] = "failed" + return value + + +def report(args: argparse.Namespace) -> int: + variant = args.variant_root.resolve() + p1 = args.p1_root.resolve() + p0 = args.p0_root.resolve() + is_clean, head = clean_revision() + candidate_manifest = manifest(variant) + p1_report = artifact(p1 / "crypto/p1/p1-report.json") + synth = artifact(p1 / "crypto/p1/candidate-synthesis.json") + netlist = artifact(args.netlist_root.resolve() / "sim/iverilog/netl/result-sim-check.json") + sta = artifact(args.sta_root.resolve() / "sta/opensta/result-sta.json") + physical = artifact(args.physical_root.resolve() / "crypto/p2/physical.json") + metrics = artifact(variant / "meta/metrics.json") + warnings = artifact(variant / "meta/warnings.json") + + checks = { + "clean_candidate": is_clean, + "candidate_manifest": candidate_manifest.get("profile") == "ihp130", + "candidate_revision": bool(head) and head != BASELINE_REVISION, + "p0_report": artifact(p0 / "crypto/p0/p0-report.json").get("status") == "passed", + "p1_focused": p1_report.get("focused_gates_status") == "passed", + "block_synthesis": synth.get("status") == "passed", + "whole_soc_netlist": netlist.get("status") == "passed", + "sta": sta.get("status") == "passed", + "physical": physical.get("status") == "passed", + "metrics": metrics.get("status") == "passed", + "warnings": warnings.get("status") in ("passed", "observed"), + } + required = ("clean_candidate", "candidate_manifest", "candidate_revision", "p0_report", + "p1_focused", "block_synthesis", "whole_soc_netlist", "sta", "physical") + passed = all(checks[name] for name in required) + result = { + "schema_version": 1, + "feature": "crypto", + "phase": "CRYPTO-P2", + "status": "passed" if passed else "incomplete", + "verdict": "P2_ACCEPTED" if passed else "NOT_RUN_OR_INCOMPLETE", + "baseline_revision": BASELINE_REVISION, + "candidate_revision": head, + "candidate_dirty": not is_clean, + "profile": candidate_manifest.get("profile", "NOT_RUN"), + "configuration": candidate_manifest.get("configuration", {}), + "checks": checks, + "artifacts": { + "p0_report": str(p0 / "crypto/p0/p0-report.json"), + "p1_report": str(p1 / "crypto/p1/p1-report.json"), + "synthesis": synth, + "netlist": netlist, + "sta": sta, + "physical": physical, + "metrics": metrics, + "warnings": warnings, + }, + "gaps": [name for name, value in checks.items() if not value], + "non_goals": ["Pytest under maintainer constraint", "CRYPTO-P3 commercial variants", + "DPA/FIA/physical security signoff"], + } + dump(variant / "crypto/p2/p2-report.json", result) + return 0 if passed else 2 + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("command", choices=("report",)) + parser.add_argument("--variant-root", type=Path, required=True) + parser.add_argument("--p0-root", type=Path, required=True) + parser.add_argument("--p1-root", type=Path, required=True) + parser.add_argument("--netlist-root", type=Path) + parser.add_argument("--sta-root", type=Path) + parser.add_argument("--physical-root", type=Path) + args = parser.parse_args() + args.netlist_root = args.netlist_root or args.variant_root + args.sta_root = args.sta_root or args.variant_root + args.physical_root = args.physical_root or args.variant_root + if args.command == "report": + return report(args) + raise AssertionError(args.command) + + +if __name__ == "__main__": + raise SystemExit(main())