fix: bind rollback rehearsal v2 source closure
This commit is contained in:
@@ -64,8 +64,35 @@ REHEARSAL_TOOL_INPUTS = (
|
||||
"yuqei-infra/deploy/release-package.py",
|
||||
"yuqei-infra/deploy/preflight-production.py",
|
||||
"yuqei-infra/deploy/verify-production-compose.py",
|
||||
"yuqei-infra/deploy/plan-first-release-snapshot-restore.py",
|
||||
"yuqei-infra/scripts/build-installable-release.py",
|
||||
"yuqei-infra/scripts/build-infrastructure-image-bundle.py",
|
||||
"yuqei-infra/scripts/build-third-party-materials.py",
|
||||
"yuqei-infra/scripts/linux_release_common.py",
|
||||
"yuqei-infra/release-trust/third-party-materials-baseline.json",
|
||||
)
|
||||
PACKAGE_SCHEMA_V1 = "yuqei.release-package.v1"
|
||||
PACKAGE_SCHEMA_V2 = "yuqei.release-package.v2"
|
||||
V1_BOOTSTRAP_ARTIFACTS = frozenset(
|
||||
{
|
||||
"release_manifest",
|
||||
"checksum_manifest",
|
||||
"image_identity",
|
||||
"installable_manifest",
|
||||
}
|
||||
)
|
||||
V2_BOOTSTRAP_ARTIFACTS = frozenset(
|
||||
{
|
||||
*V1_BOOTSTRAP_ARTIFACTS,
|
||||
"infrastructure_source_set",
|
||||
"infrastructure_image_bundle",
|
||||
"third_party_materials_input",
|
||||
"third_party_materials_manifest",
|
||||
"third_party_materials_verification_receipt",
|
||||
}
|
||||
)
|
||||
OPTIONAL_BOOTSTRAP_ARTIFACTS = frozenset(
|
||||
{"backup_proof", "compatibility_proof"}
|
||||
)
|
||||
EXPECTED_APPLICATION_SERVICES = (
|
||||
"contract-api-migrate",
|
||||
@@ -119,6 +146,15 @@ def _load_release_package_module(
|
||||
"yuqei-infra/deploy/verify-production-compose.py": (
|
||||
"yuqei_release_package_compose_gate"
|
||||
),
|
||||
"yuqei-infra/deploy/plan-first-release-snapshot-restore.py": (
|
||||
"yuqei_release_package_snapshot_restore_planner"
|
||||
),
|
||||
"yuqei-infra/scripts/build-infrastructure-image-bundle.py": (
|
||||
"yuqei_release_package_infrastructure_bundle"
|
||||
),
|
||||
"yuqei-infra/scripts/build-third-party-materials.py": (
|
||||
"yuqei_release_package_third_party_materials"
|
||||
),
|
||||
"yuqei-infra/deploy/release-package.py": name,
|
||||
}
|
||||
previous_modules = {
|
||||
@@ -135,6 +171,9 @@ def _load_release_package_module(
|
||||
"yuqei-infra/scripts/build-installable-release.py",
|
||||
"yuqei-infra/deploy/preflight-production.py",
|
||||
"yuqei-infra/deploy/verify-production-compose.py",
|
||||
"yuqei-infra/deploy/plan-first-release-snapshot-restore.py",
|
||||
"yuqei-infra/scripts/build-infrastructure-image-bundle.py",
|
||||
"yuqei-infra/scripts/build-third-party-materials.py",
|
||||
"yuqei-infra/deploy/release-package.py",
|
||||
):
|
||||
loaded[relative] = _exec_verified_module(
|
||||
@@ -152,6 +191,38 @@ def _load_release_package_module(
|
||||
release_package._COMPOSE_GATE_MODULE = loaded[
|
||||
"yuqei-infra/deploy/verify-production-compose.py"
|
||||
]
|
||||
release_package._SNAPSHOT_RESTORE_PLANNER_MODULE = loaded[
|
||||
"yuqei-infra/deploy/plan-first-release-snapshot-restore.py"
|
||||
]
|
||||
release_package._INFRASTRUCTURE_BUNDLE_MODULE = loaded[
|
||||
"yuqei-infra/scripts/build-infrastructure-image-bundle.py"
|
||||
]
|
||||
release_package._THIRD_PARTY_MATERIALS_MODULE = loaded[
|
||||
"yuqei-infra/scripts/build-third-party-materials.py"
|
||||
]
|
||||
# The snapshot planner normally discovers release-package.py from
|
||||
# its working tree. Keep the circular validation path on the same
|
||||
# already verified module bytes instead of permitting an ambient
|
||||
# file to replace the candidate after bootstrap.
|
||||
snapshot_planner = release_package._SNAPSHOT_RESTORE_PLANNER_MODULE
|
||||
snapshot_planner._load_release_package_validator = (
|
||||
lambda _repo_root: release_package.validate_release_package
|
||||
)
|
||||
baseline_relative = (
|
||||
"yuqei-infra/release-trust/third-party-materials-baseline.json"
|
||||
)
|
||||
baseline_raw, baseline_sha = _read_secure_bytes(
|
||||
REPO_ROOT / baseline_relative,
|
||||
label="verified third-party materials trust baseline",
|
||||
max_bytes=MAX_JSON_BYTES,
|
||||
)
|
||||
if (
|
||||
baseline_raw != verified_sources[baseline_relative]
|
||||
or hashlib.sha256(baseline_raw).hexdigest() != baseline_sha
|
||||
):
|
||||
raise RehearsalProofError(
|
||||
"Third-party materials trust baseline changed after bootstrap."
|
||||
)
|
||||
return release_package
|
||||
except Exception as exc:
|
||||
raise RehearsalProofError(
|
||||
@@ -1522,11 +1593,32 @@ def _bootstrap_verified_tool_sources(
|
||||
current_package_sha256,
|
||||
label="current release-package.json bootstrap",
|
||||
)
|
||||
if package.get("schema_version") != "yuqei.release-package.v1":
|
||||
package_schema = package.get("schema_version")
|
||||
if package_schema not in {PACKAGE_SCHEMA_V1, PACKAGE_SCHEMA_V2}:
|
||||
raise RehearsalProofError("Current release package bootstrap schema is unsupported.")
|
||||
artifacts = package.get("artifacts")
|
||||
if not isinstance(artifacts, dict):
|
||||
raise RehearsalProofError("Current release package bootstrap has no artifact inventory.")
|
||||
artifact_names = set(artifacts)
|
||||
required_artifacts = (
|
||||
V2_BOOTSTRAP_ARTIFACTS
|
||||
if package_schema == PACKAGE_SCHEMA_V2
|
||||
else V1_BOOTSTRAP_ARTIFACTS
|
||||
)
|
||||
if (
|
||||
not required_artifacts.issubset(artifact_names)
|
||||
or not artifact_names.issubset(
|
||||
required_artifacts | OPTIONAL_BOOTSTRAP_ARTIFACTS
|
||||
)
|
||||
):
|
||||
if package_schema == PACKAGE_SCHEMA_V2:
|
||||
raise RehearsalProofError(
|
||||
"Current release package v2 bootstrap artifact closure is incomplete."
|
||||
)
|
||||
raise RehearsalProofError(
|
||||
"Legacy release package v1 bootstrap must remain read-only and must not "
|
||||
"contain v2 or unknown artifacts."
|
||||
)
|
||||
release_record = _require_exact_fields(
|
||||
artifacts.get("release_manifest"),
|
||||
{"path", "sha256"},
|
||||
|
||||
@@ -42,6 +42,62 @@ def _write_json(path: Path, payload: dict[str, Any]) -> str:
|
||||
return hashlib.sha256(raw).hexdigest()
|
||||
|
||||
|
||||
def _bootstrap_package_fixture(
|
||||
tmp_path: Path,
|
||||
*,
|
||||
schema: str,
|
||||
omitted_tool_input: str | None = None,
|
||||
omitted_artifact: str | None = None,
|
||||
extra_artifact: str | None = None,
|
||||
repo_root: Path = REPO_ROOT,
|
||||
) -> tuple[Path, str, dict[str, bytes]]:
|
||||
source_items = []
|
||||
expected_raw: dict[str, bytes] = {}
|
||||
for relative in planner.REHEARSAL_TOOL_INPUTS:
|
||||
source = repo_root / relative
|
||||
raw = source.read_bytes()
|
||||
expected_raw[relative] = raw
|
||||
if relative != omitted_tool_input:
|
||||
source_items.append(
|
||||
{
|
||||
"path": relative,
|
||||
"sha256": hashlib.sha256(raw).hexdigest(),
|
||||
}
|
||||
)
|
||||
source_manifest = {
|
||||
"schema_version": "yuqei.release-source-manifest.v1",
|
||||
"files": {"items": source_items},
|
||||
}
|
||||
release_dir = tmp_path / "release"
|
||||
source_path = release_dir / "release-source-manifest.json"
|
||||
source_sha = _write_json(source_path, source_manifest)
|
||||
required = (
|
||||
planner.V2_BOOTSTRAP_ARTIFACTS
|
||||
if schema == planner.PACKAGE_SCHEMA_V2
|
||||
else planner.V1_BOOTSTRAP_ARTIFACTS
|
||||
)
|
||||
artifacts = {
|
||||
name: {"path": f"artifacts/{name}.json", "sha256": "0" * 64}
|
||||
for name in required
|
||||
if name != omitted_artifact
|
||||
}
|
||||
artifacts["release_manifest"] = {
|
||||
"path": "release-source-manifest.json",
|
||||
"sha256": source_sha,
|
||||
}
|
||||
if extra_artifact is not None:
|
||||
artifacts[extra_artifact] = {
|
||||
"path": f"artifacts/{extra_artifact}.json",
|
||||
"sha256": "1" * 64,
|
||||
}
|
||||
package_path = release_dir / "release-package.json"
|
||||
package_sha = _write_json(
|
||||
package_path,
|
||||
{"schema_version": schema, "artifacts": artifacts},
|
||||
)
|
||||
return package_path, package_sha, expected_raw
|
||||
|
||||
|
||||
def _with_hash(payload: dict[str, Any], field: str) -> dict[str, Any]:
|
||||
return {**payload, field: planner.canonical_sha256(payload)}
|
||||
|
||||
@@ -799,33 +855,10 @@ def test_rollback_tool_sources_are_bound_to_current_package(tmp_path: Path) -> N
|
||||
def test_rollback_tool_sources_are_bootstrapped_before_module_execution(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
source_items = []
|
||||
expected_raw = {}
|
||||
for relative in planner.REHEARSAL_TOOL_INPUTS:
|
||||
source = REPO_ROOT / relative
|
||||
raw = source.read_bytes()
|
||||
expected_raw[relative] = raw
|
||||
source_items.append({
|
||||
"path": relative,
|
||||
"sha256": hashlib.sha256(raw).hexdigest(),
|
||||
})
|
||||
source_manifest = {
|
||||
"schema_version": "yuqei.release-source-manifest.v1",
|
||||
"files": {"items": source_items},
|
||||
}
|
||||
source_path = tmp_path / "release" / "release-source-manifest.json"
|
||||
source_sha = _write_json(source_path, source_manifest)
|
||||
package = {
|
||||
"schema_version": "yuqei.release-package.v1",
|
||||
"artifacts": {
|
||||
"release_manifest": {
|
||||
"path": "release-source-manifest.json",
|
||||
"sha256": source_sha,
|
||||
}
|
||||
},
|
||||
}
|
||||
package_path = tmp_path / "release" / "release-package.json"
|
||||
package_sha = _write_json(package_path, package)
|
||||
package_path, package_sha, expected_raw = _bootstrap_package_fixture(
|
||||
tmp_path,
|
||||
schema=planner.PACKAGE_SCHEMA_V2,
|
||||
)
|
||||
|
||||
verified_sources, verified_hashes = planner._bootstrap_verified_tool_sources(
|
||||
current_package_dir=package_path.parent.resolve(),
|
||||
@@ -841,6 +874,15 @@ def test_rollback_tool_sources_are_bootstrapped_before_module_execution(
|
||||
assert loaded._INSTALLABLE_VALIDATOR_MODULE is not None
|
||||
assert loaded._PREFLIGHT_MODULE is not None
|
||||
assert loaded._COMPOSE_GATE_MODULE is not None
|
||||
assert loaded._INFRASTRUCTURE_BUNDLE_MODULE is not None
|
||||
assert loaded._THIRD_PARTY_MATERIALS_MODULE is not None
|
||||
assert loaded._SNAPSHOT_RESTORE_PLANNER_MODULE is not None
|
||||
assert (
|
||||
loaded._SNAPSHOT_RESTORE_PLANNER_MODULE._load_release_package_validator(
|
||||
REPO_ROOT
|
||||
)
|
||||
is loaded.validate_release_package
|
||||
)
|
||||
|
||||
(package_path.parent / "release-source-manifest.json").write_bytes(b"tampered\n")
|
||||
with pytest.raises(planner.RehearsalProofError, match="externally supplied SHA-256"):
|
||||
@@ -851,6 +893,128 @@ def test_rollback_tool_sources_are_bootstrapped_before_module_execution(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"relative",
|
||||
(
|
||||
"yuqei-infra/scripts/build-infrastructure-image-bundle.py",
|
||||
"yuqei-infra/scripts/build-third-party-materials.py",
|
||||
"yuqei-infra/deploy/plan-first-release-snapshot-restore.py",
|
||||
"yuqei-infra/release-trust/third-party-materials-baseline.json",
|
||||
),
|
||||
)
|
||||
def test_v2_bootstrap_rejects_missing_new_source_closure(
|
||||
tmp_path: Path,
|
||||
relative: str,
|
||||
) -> None:
|
||||
package_path, package_sha, _expected_raw = _bootstrap_package_fixture(
|
||||
tmp_path,
|
||||
schema=planner.PACKAGE_SCHEMA_V2,
|
||||
omitted_tool_input=relative,
|
||||
)
|
||||
with pytest.raises(planner.RehearsalProofError, match="does not bind"):
|
||||
planner._bootstrap_verified_tool_sources(
|
||||
current_package_dir=package_path.parent.resolve(),
|
||||
current_package_sha256=package_sha,
|
||||
repo_root=REPO_ROOT,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"relative",
|
||||
(
|
||||
"yuqei-infra/scripts/build-infrastructure-image-bundle.py",
|
||||
"yuqei-infra/scripts/build-third-party-materials.py",
|
||||
"yuqei-infra/deploy/plan-first-release-snapshot-restore.py",
|
||||
"yuqei-infra/release-trust/third-party-materials-baseline.json",
|
||||
),
|
||||
)
|
||||
def test_v2_bootstrap_rejects_new_source_drift(
|
||||
tmp_path: Path,
|
||||
relative: str,
|
||||
) -> None:
|
||||
candidate_root = tmp_path / "candidate"
|
||||
for tool_input in planner.REHEARSAL_TOOL_INPUTS:
|
||||
target = candidate_root / tool_input
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_bytes((REPO_ROOT / tool_input).read_bytes())
|
||||
package_path, package_sha, _expected_raw = _bootstrap_package_fixture(
|
||||
tmp_path / "package",
|
||||
schema=planner.PACKAGE_SCHEMA_V2,
|
||||
repo_root=candidate_root,
|
||||
)
|
||||
(candidate_root / relative).write_bytes(b"candidate drift\n")
|
||||
with pytest.raises(planner.RehearsalProofError, match="differs from package identity"):
|
||||
planner._bootstrap_verified_tool_sources(
|
||||
current_package_dir=package_path.parent.resolve(),
|
||||
current_package_sha256=package_sha,
|
||||
repo_root=candidate_root.resolve(),
|
||||
)
|
||||
|
||||
|
||||
def test_verified_v2_dependency_modules_are_injected_from_bootstrapped_bytes(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
_package_path, _package_sha, verified_sources = _bootstrap_package_fixture(
|
||||
tmp_path,
|
||||
schema=planner.PACKAGE_SCHEMA_V2,
|
||||
)
|
||||
injected = dict(verified_sources)
|
||||
module_paths = (
|
||||
"yuqei-infra/scripts/build-infrastructure-image-bundle.py",
|
||||
"yuqei-infra/scripts/build-third-party-materials.py",
|
||||
"yuqei-infra/deploy/plan-first-release-snapshot-restore.py",
|
||||
)
|
||||
for index, relative in enumerate(module_paths, start=1):
|
||||
injected[relative] += f"\nVERIFIED_BYTE_SENTINEL = {index}\n".encode()
|
||||
loaded = planner._load_release_package_module(injected)
|
||||
assert loaded._INFRASTRUCTURE_BUNDLE_MODULE.VERIFIED_BYTE_SENTINEL == 1
|
||||
assert loaded._THIRD_PARTY_MATERIALS_MODULE.VERIFIED_BYTE_SENTINEL == 2
|
||||
assert loaded._SNAPSHOT_RESTORE_PLANNER_MODULE.VERIFIED_BYTE_SENTINEL == 3
|
||||
|
||||
|
||||
def test_bootstrap_accepts_complete_legacy_v1_for_read_only_validation(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
package_path, package_sha, expected_raw = _bootstrap_package_fixture(
|
||||
tmp_path,
|
||||
schema=planner.PACKAGE_SCHEMA_V1,
|
||||
)
|
||||
verified_sources, _verified_hashes = planner._bootstrap_verified_tool_sources(
|
||||
current_package_dir=package_path.parent.resolve(),
|
||||
current_package_sha256=package_sha,
|
||||
repo_root=REPO_ROOT,
|
||||
)
|
||||
assert verified_sources == expected_raw
|
||||
|
||||
|
||||
def test_bootstrap_rejects_incomplete_v2_and_v2_artifact_downgrade(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
package_path, package_sha, _expected_raw = _bootstrap_package_fixture(
|
||||
tmp_path / "incomplete-v2",
|
||||
schema=planner.PACKAGE_SCHEMA_V2,
|
||||
omitted_artifact="third_party_materials_verification_receipt",
|
||||
)
|
||||
with pytest.raises(planner.RehearsalProofError, match="v2 bootstrap artifact closure"):
|
||||
planner._bootstrap_verified_tool_sources(
|
||||
current_package_dir=package_path.parent.resolve(),
|
||||
current_package_sha256=package_sha,
|
||||
repo_root=REPO_ROOT,
|
||||
)
|
||||
|
||||
package_path, package_sha, _expected_raw = _bootstrap_package_fixture(
|
||||
tmp_path / "downgraded-v1",
|
||||
schema=planner.PACKAGE_SCHEMA_V1,
|
||||
extra_artifact="infrastructure_image_bundle",
|
||||
)
|
||||
with pytest.raises(planner.RehearsalProofError, match="v1 bootstrap must remain read-only"):
|
||||
planner._bootstrap_verified_tool_sources(
|
||||
current_package_dir=package_path.parent.resolve(),
|
||||
current_package_sha256=package_sha,
|
||||
repo_root=REPO_ROOT,
|
||||
)
|
||||
|
||||
|
||||
def test_strict_env_preflight_and_compose_gate_evidence_fail_closed(
|
||||
tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user