build: bind checkpoint manifest to acceptance
This commit is contained in:
parent
644c99a9ca
commit
3c26e1bdab
9 changed files with 191 additions and 22 deletions
|
|
@ -21,7 +21,7 @@ The repository now provides:
|
||||||
- a checksummed acceptance-kit generator containing only allowlisted public
|
- a checksummed acceptance-kit generator containing only allowlisted public
|
||||||
synthetic inputs, with provenance binding to the repository commit and
|
synthetic inputs, with provenance binding to the repository commit and
|
||||||
non-executing artifact-verification report, including the Rayforge pin and
|
non-executing artifact-verification report, including the Rayforge pin and
|
||||||
bundle, installer and addon-verification fields;
|
bundle, installer, addon-verification and source checkpoint-manifest fields;
|
||||||
- a fail-closed manual desktop-acceptance report and validator;
|
- a fail-closed manual desktop-acceptance report and validator;
|
||||||
- pinned upstream contract/license manifests and a drift checker;
|
- pinned upstream contract/license manifests and a drift checker;
|
||||||
- a fail-closed API-23 adapter isolating Rayforge's internal driver registry;
|
- a fail-closed API-23 adapter isolating Rayforge's internal driver registry;
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,9 @@ Rayforge commit, requires the manifest and bundle to contain exactly the
|
||||||
versioned repository addon file set with matching digests, and requires the
|
versioned repository addon file set with matching digests, and requires the
|
||||||
pinned Rayforge license both inside the ZIP and as a sidecar. Missing, changed
|
pinned Rayforge license both inside the ZIP and as a sidecar. Missing, changed
|
||||||
or additional addon files fail verification.
|
or additional addon files fail verification.
|
||||||
|
The schema-1 verification output records the exact checkpoint-manifest name
|
||||||
|
and SHA-256 plus the fixed checkpoint and addon identities, so later manual
|
||||||
|
acceptance can retain the complete artifact provenance chain.
|
||||||
It never executes the application or installer, and a passing result retains
|
It never executes the application or installer, and a passing result retains
|
||||||
the manual GUI interaction gate. The installer, manifest and license sidecar
|
the manual GUI interaction gate. The installer, manifest and license sidecar
|
||||||
form one distribution set.
|
form one distribution set.
|
||||||
|
|
@ -90,7 +93,8 @@ report into the kit at creation time:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python scripts/prepare_m1_acceptance_kit.py `
|
python scripts/prepare_m1_acceptance_kit.py `
|
||||||
--artifact-verification .build/artifacts/rayforge-windows-verification.json
|
--artifact-verification .build/artifacts/rayforge-windows-verification.json `
|
||||||
|
--checkpoint-manifest .build/artifacts/rayforge-windows-checkpoint.json
|
||||||
```
|
```
|
||||||
|
|
||||||
The default output is `.build/acceptance-kit/`. The generator uses a fixed
|
The default output is `.build/acceptance-kit/`. The generator uses a fixed
|
||||||
|
|
@ -102,7 +106,8 @@ artifact-verification report by SHA-256. Editing, replacing or referring to an
|
||||||
external provenance report therefore fails validation. The copied report must
|
external provenance report therefore fails validation. The copied report must
|
||||||
also name the pinned Rayforge commit, contain valid bundle and installer
|
also name the pinned Rayforge commit, contain valid bundle and installer
|
||||||
SHA-256 values, and confirm that the exact current addon file count was
|
SHA-256 values, and confirm that the exact current addon file count was
|
||||||
verified.
|
verified. Its own schema, checkpoint identity and addon identity are fixed, and
|
||||||
|
its recorded checkpoint-manifest hash must match the copied schema-2 manifest.
|
||||||
|
|
||||||
1. Pass the non-executing artifact verifier above.
|
1. Pass the non-executing artifact verifier above.
|
||||||
2. Launch the portable build with a fresh application-data directory.
|
2. Launch the portable build with a fresh application-data directory.
|
||||||
|
|
|
||||||
|
|
@ -335,7 +335,8 @@ ignored build area, records their hashes and refuses to overwrite an existing
|
||||||
test run. Its schema-2 provenance chain binds the editable manual report to the
|
test run. Its schema-2 provenance chain binds the editable manual report to the
|
||||||
exact kit manifest, repository commit and non-executing artifact-verification
|
exact kit manifest, repository commit and non-executing artifact-verification
|
||||||
report; substituted or changed inputs, a different Rayforge pin, malformed
|
report; substituted or changed inputs, a different Rayforge pin, malformed
|
||||||
artifact hashes or an unverified addon fail validation.
|
artifact hashes, a changed checkpoint manifest or an unverified addon fail
|
||||||
|
validation.
|
||||||
The pinned upstream contracts and license notices are now machine-verifiable.
|
The pinned upstream contracts and license notices are now machine-verifiable.
|
||||||
Rayforge's documented addon lifecycle is viable, but production driver
|
Rayforge's documented addon lifecycle is viable, but production driver
|
||||||
registration still needs a supported symmetric registry API. The current
|
registration still needs a supported symmetric registry API. The current
|
||||||
|
|
|
||||||
|
|
@ -49,15 +49,28 @@ def create_kit(
|
||||||
output: Path,
|
output: Path,
|
||||||
repository_commit: str | None = None,
|
repository_commit: str | None = None,
|
||||||
artifact_verification: Path | None = None,
|
artifact_verification: Path | None = None,
|
||||||
|
checkpoint_manifest: Path | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
verification_source = None
|
verification_source = None
|
||||||
if artifact_verification is not None:
|
checkpoint_source = None
|
||||||
|
if (artifact_verification is None) != (checkpoint_manifest is None):
|
||||||
|
raise ValueError(
|
||||||
|
"artifact verification and checkpoint manifest must be supplied together"
|
||||||
|
)
|
||||||
|
if artifact_verification is not None and checkpoint_manifest is not None:
|
||||||
verification_source = artifact_verification.resolve()
|
verification_source = artifact_verification.resolve()
|
||||||
|
checkpoint_source = checkpoint_manifest.resolve()
|
||||||
verification = json.loads(
|
verification = json.loads(
|
||||||
verification_source.read_text(encoding="utf-8-sig")
|
verification_source.read_text(encoding="utf-8-sig")
|
||||||
)
|
)
|
||||||
|
checkpoint_bytes = checkpoint_source.read_bytes()
|
||||||
|
checkpoint = json.loads(checkpoint_bytes.decode("utf-8-sig"))
|
||||||
|
checkpoint_record = verification.get("checkpoint_manifest", {})
|
||||||
if (
|
if (
|
||||||
verification.get("status") != "passed"
|
verification.get("schema_version") != 1
|
||||||
|
or verification.get("verification")
|
||||||
|
!= "Rayforge Windows no-device artifact verification"
|
||||||
|
or verification.get("status") != "passed"
|
||||||
or verification.get("hardware_access") is not False
|
or verification.get("hardware_access") is not False
|
||||||
or verification.get("executed_binaries") is not False
|
or verification.get("executed_binaries") is not False
|
||||||
or verification.get("manual_interaction_required") is not True
|
or verification.get("manual_interaction_required") is not True
|
||||||
|
|
@ -65,6 +78,19 @@ def create_kit(
|
||||||
or SHA256.fullmatch(verification.get("bundle_sha256", "")) is None
|
or SHA256.fullmatch(verification.get("bundle_sha256", "")) is None
|
||||||
or SHA256.fullmatch(verification.get("installer_sha256", "")) is None
|
or SHA256.fullmatch(verification.get("installer_sha256", "")) is None
|
||||||
or verification.get("verified_addon_files") != EXPECTED_ADDON_FILES
|
or verification.get("verified_addon_files") != EXPECTED_ADDON_FILES
|
||||||
|
or verification.get("addon") != "the_pirate_ray_jcz"
|
||||||
|
or checkpoint_record.get("path") != checkpoint_source.name
|
||||||
|
or checkpoint_record.get("sha256")
|
||||||
|
!= hashlib.sha256(checkpoint_bytes).hexdigest()
|
||||||
|
or checkpoint.get("schema_version") != 2
|
||||||
|
or checkpoint.get("checkpoint") != "Rayforge Windows no-device bundle"
|
||||||
|
or checkpoint.get("addon") != verification.get("addon")
|
||||||
|
or checkpoint.get("rayforge_commit")
|
||||||
|
!= verification.get("rayforge_commit")
|
||||||
|
or checkpoint.get("bundle", {}).get("sha256")
|
||||||
|
!= verification.get("bundle_sha256")
|
||||||
|
or checkpoint.get("installer", {}).get("sha256")
|
||||||
|
!= verification.get("installer_sha256")
|
||||||
):
|
):
|
||||||
raise ValueError("artifact-verification report did not pass the no-device gate")
|
raise ValueError("artifact-verification report did not pass the no-device gate")
|
||||||
|
|
||||||
|
|
@ -116,6 +142,8 @@ def create_kit(
|
||||||
if verification_source is not None:
|
if verification_source is not None:
|
||||||
verification_target = output / "artifact-verification.json"
|
verification_target = output / "artifact-verification.json"
|
||||||
shutil.copyfile(verification_source, verification_target)
|
shutil.copyfile(verification_source, verification_target)
|
||||||
|
checkpoint_target = output / checkpoint_source.name
|
||||||
|
shutil.copyfile(checkpoint_source, checkpoint_target)
|
||||||
report["artifact_verification"] = {
|
report["artifact_verification"] = {
|
||||||
"status": "passed",
|
"status": "passed",
|
||||||
"report_path": verification_target.name,
|
"report_path": verification_target.name,
|
||||||
|
|
@ -136,13 +164,22 @@ def main() -> int:
|
||||||
type=Path,
|
type=Path,
|
||||||
default=ROOT / ".build" / "acceptance-kit",
|
default=ROOT / ".build" / "acceptance-kit",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--checkpoint-manifest",
|
||||||
|
type=Path,
|
||||||
|
help="schema-2 manifest consumed by the artifact verifier",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--artifact-verification",
|
"--artifact-verification",
|
||||||
type=Path,
|
type=Path,
|
||||||
help="passing JSON output from verify_rayforge_windows_checkpoint.py",
|
help="passing JSON output from verify_rayforge_windows_checkpoint.py",
|
||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
result = create_kit(args.output, artifact_verification=args.artifact_verification)
|
result = create_kit(
|
||||||
|
args.output,
|
||||||
|
artifact_verification=args.artifact_verification,
|
||||||
|
checkpoint_manifest=args.checkpoint_manifest,
|
||||||
|
)
|
||||||
print(f"kit={args.output.resolve()}")
|
print(f"kit={args.output.resolve()}")
|
||||||
print(f"files={len(result['files'])}")
|
print(f"files={len(result['files'])}")
|
||||||
print(f"repository_commit={result['repository_commit']}")
|
print(f"repository_commit={result['repository_commit']}")
|
||||||
|
|
|
||||||
|
|
@ -67,14 +67,24 @@ def _validate_evidence(report_root: Path, records: list[dict]) -> list[str]:
|
||||||
|
|
||||||
|
|
||||||
def _bound_json(report_root: Path, binding: dict, label: str) -> tuple[dict | None, list[str]]:
|
def _bound_json(report_root: Path, binding: dict, label: str) -> tuple[dict | None, list[str]]:
|
||||||
value = binding.get("report_path") or binding.get("manifest_path") or ""
|
value = (
|
||||||
|
binding.get("report_path")
|
||||||
|
or binding.get("manifest_path")
|
||||||
|
or binding.get("path")
|
||||||
|
or ""
|
||||||
|
)
|
||||||
relative = PurePosixPath(value)
|
relative = PurePosixPath(value)
|
||||||
if not value or relative.is_absolute() or ".." in relative.parts:
|
if not value or relative.is_absolute() or ".." in relative.parts:
|
||||||
return None, [f"unsafe {label} path: {value!r}"]
|
return None, [f"unsafe {label} path: {value!r}"]
|
||||||
path = report_root.joinpath(*relative.parts)
|
path = report_root.joinpath(*relative.parts)
|
||||||
if not path.is_file():
|
if not path.is_file():
|
||||||
return None, [f"{label} is missing: {value}"]
|
return None, [f"{label} is missing: {value}"]
|
||||||
digest = binding.get("report_sha256") or binding.get("manifest_sha256") or ""
|
digest = (
|
||||||
|
binding.get("report_sha256")
|
||||||
|
or binding.get("manifest_sha256")
|
||||||
|
or binding.get("sha256")
|
||||||
|
or ""
|
||||||
|
)
|
||||||
actual = hashlib.sha256(path.read_bytes()).hexdigest()
|
actual = hashlib.sha256(path.read_bytes()).hexdigest()
|
||||||
if not SHA256.fullmatch(digest) or digest != actual:
|
if not SHA256.fullmatch(digest) or digest != actual:
|
||||||
return None, [f"{label} SHA-256 mismatch: {value}"]
|
return None, [f"{label} SHA-256 mismatch: {value}"]
|
||||||
|
|
@ -112,6 +122,13 @@ def _validate_artifact_report(report_root: Path, binding: dict) -> list[str]:
|
||||||
return errors
|
return errors
|
||||||
if verification.get("status") != "passed":
|
if verification.get("status") != "passed":
|
||||||
errors.append("artifact verification did not pass")
|
errors.append("artifact verification did not pass")
|
||||||
|
if verification.get("schema_version") != 1:
|
||||||
|
errors.append("unsupported artifact-verification schema")
|
||||||
|
if (
|
||||||
|
verification.get("verification")
|
||||||
|
!= "Rayforge Windows no-device artifact verification"
|
||||||
|
):
|
||||||
|
errors.append("unexpected artifact-verification identity")
|
||||||
if verification.get("hardware_access") is not False:
|
if verification.get("hardware_access") is not False:
|
||||||
errors.append("artifact verification does not preserve the hardware gate")
|
errors.append("artifact verification does not preserve the hardware gate")
|
||||||
if verification.get("executed_binaries") is not False:
|
if verification.get("executed_binaries") is not False:
|
||||||
|
|
@ -120,12 +137,37 @@ def _validate_artifact_report(report_root: Path, binding: dict) -> list[str]:
|
||||||
errors.append("artifact verification dropped the manual interaction gate")
|
errors.append("artifact verification dropped the manual interaction gate")
|
||||||
if verification.get("rayforge_commit") != RAYFORGE_COMMIT:
|
if verification.get("rayforge_commit") != RAYFORGE_COMMIT:
|
||||||
errors.append("artifact verification uses the wrong Rayforge commit")
|
errors.append("artifact verification uses the wrong Rayforge commit")
|
||||||
|
if verification.get("addon") != "the_pirate_ray_jcz":
|
||||||
|
errors.append("artifact verification uses the wrong addon")
|
||||||
if SHA256.fullmatch(verification.get("bundle_sha256", "")) is None:
|
if SHA256.fullmatch(verification.get("bundle_sha256", "")) is None:
|
||||||
errors.append("artifact verification has an invalid bundle SHA-256")
|
errors.append("artifact verification has an invalid bundle SHA-256")
|
||||||
if SHA256.fullmatch(verification.get("installer_sha256", "")) is None:
|
if SHA256.fullmatch(verification.get("installer_sha256", "")) is None:
|
||||||
errors.append("artifact verification has an invalid installer SHA-256")
|
errors.append("artifact verification has an invalid installer SHA-256")
|
||||||
if verification.get("verified_addon_files") != EXPECTED_ADDON_FILES:
|
if verification.get("verified_addon_files") != EXPECTED_ADDON_FILES:
|
||||||
errors.append("artifact verification did not verify the exact addon file set")
|
errors.append("artifact verification did not verify the exact addon file set")
|
||||||
|
checkpoint, checkpoint_errors = _bound_json(
|
||||||
|
report_root,
|
||||||
|
verification.get("checkpoint_manifest", {}),
|
||||||
|
"checkpoint manifest",
|
||||||
|
)
|
||||||
|
errors.extend(checkpoint_errors)
|
||||||
|
if checkpoint is not None:
|
||||||
|
if checkpoint.get("schema_version") != 2:
|
||||||
|
errors.append("unsupported checkpoint-manifest schema")
|
||||||
|
if checkpoint.get("checkpoint") != "Rayforge Windows no-device bundle":
|
||||||
|
errors.append("unexpected checkpoint-manifest identity")
|
||||||
|
if checkpoint.get("addon") != verification.get("addon"):
|
||||||
|
errors.append("checkpoint-manifest addon mismatch")
|
||||||
|
if checkpoint.get("rayforge_commit") != verification.get("rayforge_commit"):
|
||||||
|
errors.append("checkpoint-manifest Rayforge commit mismatch")
|
||||||
|
if checkpoint.get("bundle", {}).get("sha256") != verification.get(
|
||||||
|
"bundle_sha256"
|
||||||
|
):
|
||||||
|
errors.append("checkpoint-manifest bundle SHA-256 mismatch")
|
||||||
|
if checkpoint.get("installer", {}).get("sha256") != verification.get(
|
||||||
|
"installer_sha256"
|
||||||
|
):
|
||||||
|
errors.append("checkpoint-manifest installer SHA-256 mismatch")
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,9 +65,14 @@ def _relative_below(path: PurePosixPath, root: PurePosixPath) -> PurePosixPath |
|
||||||
|
|
||||||
def verify(manifest_path: Path) -> dict:
|
def verify(manifest_path: Path) -> dict:
|
||||||
manifest_path = manifest_path.resolve()
|
manifest_path = manifest_path.resolve()
|
||||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8-sig"))
|
manifest_bytes = manifest_path.read_bytes()
|
||||||
|
manifest = json.loads(manifest_bytes.decode("utf-8-sig"))
|
||||||
if manifest.get("schema_version") != 2:
|
if manifest.get("schema_version") != 2:
|
||||||
raise ValueError("unsupported checkpoint schema")
|
raise ValueError("unsupported checkpoint schema")
|
||||||
|
if manifest.get("checkpoint") != "Rayforge Windows no-device bundle":
|
||||||
|
raise ValueError("unexpected checkpoint identity")
|
||||||
|
if manifest.get("addon") != "the_pirate_ray_jcz":
|
||||||
|
raise ValueError("unexpected checkpoint addon")
|
||||||
if manifest.get("rayforge_commit") != UPSTREAM["commit"]:
|
if manifest.get("rayforge_commit") != UPSTREAM["commit"]:
|
||||||
raise ValueError("Rayforge commit does not match the repository pin")
|
raise ValueError("Rayforge commit does not match the repository pin")
|
||||||
if manifest.get("hardware_access") is not False or manifest.get("transport") is not None:
|
if manifest.get("hardware_access") is not False or manifest.get("transport") is not None:
|
||||||
|
|
@ -143,8 +148,15 @@ def verify(manifest_path: Path) -> dict:
|
||||||
verified_addon_files = len(packaged_addon)
|
verified_addon_files = len(packaged_addon)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
"schema_version": 1,
|
||||||
|
"verification": "Rayforge Windows no-device artifact verification",
|
||||||
"status": "passed",
|
"status": "passed",
|
||||||
"rayforge_commit": manifest["rayforge_commit"],
|
"rayforge_commit": manifest["rayforge_commit"],
|
||||||
|
"addon": manifest["addon"],
|
||||||
|
"checkpoint_manifest": {
|
||||||
|
"path": manifest_path.name,
|
||||||
|
"sha256": _digest(manifest_bytes),
|
||||||
|
},
|
||||||
"hardware_access": False,
|
"hardware_access": False,
|
||||||
"executed_binaries": False,
|
"executed_binaries": False,
|
||||||
"bundle_sha256": manifest["bundle"]["sha256"],
|
"bundle_sha256": manifest["bundle"]["sha256"],
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,20 @@ from scripts.prepare_m1_acceptance_kit import (
|
||||||
|
|
||||||
class M1AcceptanceKitTests(unittest.TestCase):
|
class M1AcceptanceKitTests(unittest.TestCase):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _verification(status="passed"):
|
def _artifact_pair(root: Path, status="passed"):
|
||||||
return {
|
checkpoint = {
|
||||||
|
"schema_version": 2,
|
||||||
|
"checkpoint": "Rayforge Windows no-device bundle",
|
||||||
|
"addon": "the_pirate_ray_jcz",
|
||||||
|
"rayforge_commit": RAYFORGE_COMMIT,
|
||||||
|
"bundle": {"sha256": "1" * 64},
|
||||||
|
"installer": {"sha256": "2" * 64},
|
||||||
|
}
|
||||||
|
checkpoint_path = root / "rayforge-windows-checkpoint.json"
|
||||||
|
checkpoint_path.write_text(json.dumps(checkpoint), encoding="utf-8")
|
||||||
|
verification = {
|
||||||
|
"schema_version": 1,
|
||||||
|
"verification": "Rayforge Windows no-device artifact verification",
|
||||||
"status": status,
|
"status": status,
|
||||||
"hardware_access": False,
|
"hardware_access": False,
|
||||||
"executed_binaries": False,
|
"executed_binaries": False,
|
||||||
|
|
@ -26,7 +38,15 @@ class M1AcceptanceKitTests(unittest.TestCase):
|
||||||
"bundle_sha256": "1" * 64,
|
"bundle_sha256": "1" * 64,
|
||||||
"installer_sha256": "2" * 64,
|
"installer_sha256": "2" * 64,
|
||||||
"verified_addon_files": EXPECTED_ADDON_FILES,
|
"verified_addon_files": EXPECTED_ADDON_FILES,
|
||||||
|
"addon": "the_pirate_ray_jcz",
|
||||||
|
"checkpoint_manifest": {
|
||||||
|
"path": checkpoint_path.name,
|
||||||
|
"sha256": hashlib.sha256(checkpoint_path.read_bytes()).hexdigest(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
verification_path = root / "verification.json"
|
||||||
|
verification_path.write_text(json.dumps(verification), encoding="utf-8")
|
||||||
|
return verification_path, checkpoint_path
|
||||||
|
|
||||||
def test_kit_contains_only_allowlisted_public_inputs(self):
|
def test_kit_contains_only_allowlisted_public_inputs(self):
|
||||||
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
|
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
|
||||||
|
|
@ -84,15 +104,13 @@ class M1AcceptanceKitTests(unittest.TestCase):
|
||||||
def test_passing_artifact_verification_is_bound_into_report(self):
|
def test_passing_artifact_verification_is_bound_into_report(self):
|
||||||
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
|
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
|
||||||
root = Path(directory)
|
root = Path(directory)
|
||||||
verification = root / "verification.json"
|
verification, checkpoint = self._artifact_pair(root)
|
||||||
verification.write_text(
|
|
||||||
json.dumps(self._verification()), encoding="utf-8"
|
|
||||||
)
|
|
||||||
output = root / "kit"
|
output = root / "kit"
|
||||||
create_kit(
|
create_kit(
|
||||||
output,
|
output,
|
||||||
repository_commit="e" * 40,
|
repository_commit="e" * 40,
|
||||||
artifact_verification=verification,
|
artifact_verification=verification,
|
||||||
|
checkpoint_manifest=checkpoint,
|
||||||
)
|
)
|
||||||
copied = output / "artifact-verification.json"
|
copied = output / "artifact-verification.json"
|
||||||
report = json.loads((output / "manual-report.json").read_text(encoding="utf-8"))
|
report = json.loads((output / "manual-report.json").read_text(encoding="utf-8"))
|
||||||
|
|
@ -106,22 +124,20 @@ class M1AcceptanceKitTests(unittest.TestCase):
|
||||||
def test_failed_artifact_verification_is_rejected(self):
|
def test_failed_artifact_verification_is_rejected(self):
|
||||||
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
|
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
|
||||||
root = Path(directory)
|
root = Path(directory)
|
||||||
verification = root / "verification.json"
|
verification, checkpoint = self._artifact_pair(root, status="failed")
|
||||||
verification.write_text(
|
|
||||||
json.dumps(self._verification(status="failed")), encoding="utf-8"
|
|
||||||
)
|
|
||||||
with self.assertRaisesRegex(ValueError, "did not pass"):
|
with self.assertRaisesRegex(ValueError, "did not pass"):
|
||||||
create_kit(
|
create_kit(
|
||||||
root / "kit",
|
root / "kit",
|
||||||
repository_commit="f" * 40,
|
repository_commit="f" * 40,
|
||||||
artifact_verification=verification,
|
artifact_verification=verification,
|
||||||
|
checkpoint_manifest=checkpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_wrong_rayforge_pin_or_missing_hashes_are_rejected(self):
|
def test_wrong_rayforge_pin_or_missing_hashes_are_rejected(self):
|
||||||
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
|
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
|
||||||
root = Path(directory)
|
root = Path(directory)
|
||||||
verification = root / "verification.json"
|
verification, checkpoint = self._artifact_pair(root)
|
||||||
result = self._verification()
|
result = json.loads(verification.read_text(encoding="utf-8"))
|
||||||
result["rayforge_commit"] = "0" * 40
|
result["rayforge_commit"] = "0" * 40
|
||||||
result["bundle_sha256"] = "not-a-hash"
|
result["bundle_sha256"] = "not-a-hash"
|
||||||
verification.write_text(json.dumps(result), encoding="utf-8")
|
verification.write_text(json.dumps(result), encoding="utf-8")
|
||||||
|
|
@ -130,6 +146,20 @@ class M1AcceptanceKitTests(unittest.TestCase):
|
||||||
root / "kit",
|
root / "kit",
|
||||||
repository_commit="f" * 40,
|
repository_commit="f" * 40,
|
||||||
artifact_verification=verification,
|
artifact_verification=verification,
|
||||||
|
checkpoint_manifest=checkpoint,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_changed_checkpoint_manifest_is_rejected(self):
|
||||||
|
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
verification, checkpoint = self._artifact_pair(root)
|
||||||
|
checkpoint.write_text("{}", encoding="utf-8")
|
||||||
|
with self.assertRaisesRegex(ValueError, "did not pass"):
|
||||||
|
create_kit(
|
||||||
|
root / "kit",
|
||||||
|
repository_commit="f" * 40,
|
||||||
|
artifact_verification=verification,
|
||||||
|
checkpoint_manifest=checkpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,19 @@ class M1ManualAcceptanceTests(unittest.TestCase):
|
||||||
"manifest_sha256": hashlib.sha256(manifest_path.read_bytes()).hexdigest(),
|
"manifest_sha256": hashlib.sha256(manifest_path.read_bytes()).hexdigest(),
|
||||||
"repository_commit": "b" * 40,
|
"repository_commit": "b" * 40,
|
||||||
}
|
}
|
||||||
|
checkpoint = {
|
||||||
|
"schema_version": 2,
|
||||||
|
"checkpoint": "Rayforge Windows no-device bundle",
|
||||||
|
"addon": "the_pirate_ray_jcz",
|
||||||
|
"rayforge_commit": RAYFORGE_COMMIT,
|
||||||
|
"bundle": {"sha256": "1" * 64},
|
||||||
|
"installer": {"sha256": "2" * 64},
|
||||||
|
}
|
||||||
|
checkpoint_path = root / "rayforge-windows-checkpoint.json"
|
||||||
|
checkpoint_path.write_text(json.dumps(checkpoint), encoding="utf-8")
|
||||||
verification = {
|
verification = {
|
||||||
|
"schema_version": 1,
|
||||||
|
"verification": "Rayforge Windows no-device artifact verification",
|
||||||
"status": "passed",
|
"status": "passed",
|
||||||
"hardware_access": False,
|
"hardware_access": False,
|
||||||
"executed_binaries": False,
|
"executed_binaries": False,
|
||||||
|
|
@ -51,6 +63,11 @@ class M1ManualAcceptanceTests(unittest.TestCase):
|
||||||
"bundle_sha256": "1" * 64,
|
"bundle_sha256": "1" * 64,
|
||||||
"installer_sha256": "2" * 64,
|
"installer_sha256": "2" * 64,
|
||||||
"verified_addon_files": EXPECTED_ADDON_FILES,
|
"verified_addon_files": EXPECTED_ADDON_FILES,
|
||||||
|
"addon": "the_pirate_ray_jcz",
|
||||||
|
"checkpoint_manifest": {
|
||||||
|
"path": checkpoint_path.name,
|
||||||
|
"sha256": hashlib.sha256(checkpoint_path.read_bytes()).hexdigest(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
verification_path = root / "artifact-verification.json"
|
verification_path = root / "artifact-verification.json"
|
||||||
verification_path.write_text(json.dumps(verification), encoding="utf-8")
|
verification_path.write_text(json.dumps(verification), encoding="utf-8")
|
||||||
|
|
@ -163,6 +180,19 @@ class M1ManualAcceptanceTests(unittest.TestCase):
|
||||||
self.assertIn("artifact verification uses the wrong Rayforge commit", result["errors"])
|
self.assertIn("artifact verification uses the wrong Rayforge commit", result["errors"])
|
||||||
self.assertIn("artifact verification has an invalid installer SHA-256", result["errors"])
|
self.assertIn("artifact verification has an invalid installer SHA-256", result["errors"])
|
||||||
|
|
||||||
|
def test_changed_checkpoint_manifest_is_rejected(self):
|
||||||
|
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
report = self._passing(root)
|
||||||
|
(root / "rayforge-windows-checkpoint.json").write_text(
|
||||||
|
"{}", encoding="utf-8"
|
||||||
|
)
|
||||||
|
result = validate(self._write(root, report))
|
||||||
|
self.assertEqual("failed", result["status"])
|
||||||
|
self.assertTrue(
|
||||||
|
any("checkpoint manifest SHA-256 mismatch" in error for error in result["errors"])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,9 @@ class WindowsCheckpointVerifierTests(unittest.TestCase):
|
||||||
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
|
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
|
||||||
result = verify(self._checkpoint(Path(directory)))
|
result = verify(self._checkpoint(Path(directory)))
|
||||||
self.assertEqual(result["status"], "passed")
|
self.assertEqual(result["status"], "passed")
|
||||||
|
self.assertEqual(1, result["schema_version"])
|
||||||
|
self.assertEqual("the_pirate_ray_jcz", result["addon"])
|
||||||
|
self.assertRegex(result["checkpoint_manifest"]["sha256"], r"^[0-9a-f]{64}$")
|
||||||
self.assertFalse(result["executed_binaries"])
|
self.assertFalse(result["executed_binaries"])
|
||||||
expected_count = sum(
|
expected_count = sum(
|
||||||
path.is_file() and "__pycache__" not in path.parts
|
path.is_file() and "__pycache__" not in path.parts
|
||||||
|
|
@ -132,6 +135,15 @@ class WindowsCheckpointVerifierTests(unittest.TestCase):
|
||||||
with self.assertRaises(FileNotFoundError):
|
with self.assertRaises(FileNotFoundError):
|
||||||
verify(manifest)
|
verify(manifest)
|
||||||
|
|
||||||
|
def test_checkpoint_and_addon_identity_are_required(self):
|
||||||
|
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
|
||||||
|
manifest = self._checkpoint(Path(directory))
|
||||||
|
data = json.loads(manifest.read_text(encoding="utf-8"))
|
||||||
|
data["addon"] = "foreign_addon"
|
||||||
|
manifest.write_text(json.dumps(data), encoding="utf-8")
|
||||||
|
with self.assertRaisesRegex(ValueError, "unexpected checkpoint addon"):
|
||||||
|
verify(manifest)
|
||||||
|
|
||||||
def test_manifest_cannot_omit_or_change_repository_addon_source(self):
|
def test_manifest_cannot_omit_or_change_repository_addon_source(self):
|
||||||
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
|
with tempfile.TemporaryDirectory(dir=ROOT / ".build") as directory:
|
||||||
manifest = self._checkpoint(Path(directory))
|
manifest = self._checkpoint(Path(directory))
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue