101 lines
4.1 KiB
Python
101 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Verify a Stackchain Dashboard release bundle before promotion."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import sys
|
|
import tarfile
|
|
from pathlib import Path, PurePosixPath
|
|
|
|
from build_release import manifest_for, runtime_files_at_commit
|
|
|
|
|
|
def one(directory: Path, pattern: str) -> Path:
|
|
matches = sorted(directory.glob(pattern))
|
|
if len(matches) != 1:
|
|
raise ValueError(f"expected exactly one {pattern} file, found {len(matches)}")
|
|
return matches[0]
|
|
|
|
|
|
def digest(data: bytes) -> str:
|
|
return hashlib.sha256(data).hexdigest()
|
|
|
|
|
|
def verify(input_dir: Path, commit: str, repository: Path) -> None:
|
|
archive_path = one(input_dir, "*.tar.gz")
|
|
manifest_path = one(input_dir, "*.manifest.json")
|
|
checksum_path = one(input_dir, "*.sha256")
|
|
manifest = json.loads(manifest_path.read_text())
|
|
|
|
if manifest.get("commit") != commit:
|
|
raise ValueError("manifest commit does not match the tested commit")
|
|
artifact = manifest.get("artifact", {})
|
|
archive_data = archive_path.read_bytes()
|
|
archive_digest = digest(archive_data)
|
|
if artifact.get("name") != archive_path.name:
|
|
raise ValueError("manifest artifact name does not match the bundle")
|
|
if artifact.get("size") != len(archive_data) or artifact.get("sha256") != archive_digest:
|
|
raise ValueError("bundle does not match its manifest")
|
|
|
|
checksum_parts = checksum_path.read_text().strip().split()
|
|
if checksum_parts != [archive_digest, archive_path.name]:
|
|
raise ValueError("checksum file does not match the bundle")
|
|
|
|
expected_files = manifest.get("files")
|
|
if not isinstance(expected_files, dict):
|
|
raise ValueError("manifest files must be an object")
|
|
source_files = runtime_files_at_commit(repository, commit)
|
|
source_manifest = manifest_for(source_files, commit)["files"]
|
|
if expected_files != source_manifest:
|
|
raise ValueError("bundle manifest does not match the declared Git commit")
|
|
source_by_name = {item.name: item for item in source_files}
|
|
with tarfile.open(archive_path, "r:gz") as archive:
|
|
members = archive.getmembers()
|
|
for member in members:
|
|
path = PurePosixPath(member.name)
|
|
if path.is_absolute() or ".." in path.parts or not member.isfile():
|
|
raise ValueError("bundle contains an unsafe member")
|
|
names = {member.name for member in members}
|
|
if names != set(expected_files) | {"release-manifest.json"}:
|
|
raise ValueError("bundle contents do not match the manifest")
|
|
embedded_file = archive.extractfile("release-manifest.json")
|
|
if embedded_file is None:
|
|
raise ValueError("bundle manifest is missing")
|
|
embedded = json.load(embedded_file)
|
|
if embedded.get("commit") != commit or embedded.get("files") != expected_files:
|
|
raise ValueError("embedded manifest does not match the public manifest")
|
|
for name, metadata in expected_files.items():
|
|
bundled = archive.extractfile(name)
|
|
if bundled is None:
|
|
raise ValueError(f"bundle member is missing: {name}")
|
|
data = bundled.read()
|
|
if metadata != {"sha256": digest(data), "size": len(data)}:
|
|
raise ValueError(f"bundle member failed verification: {name}")
|
|
if data != source_by_name[name].data:
|
|
raise ValueError(f"bundle member differs from the declared Git commit: {name}")
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--input-dir", type=Path, required=True)
|
|
parser.add_argument("--commit", required=True)
|
|
parser.add_argument("--repository", type=Path, default=Path.cwd())
|
|
return parser.parse_args()
|
|
|
|
|
|
def main() -> None:
|
|
args = parse_args()
|
|
try:
|
|
verify(args.input_dir, args.commit, args.repository)
|
|
except (OSError, ValueError, json.JSONDecodeError, tarfile.TarError) as error:
|
|
print(f"release verification failed: {error}", file=sys.stderr)
|
|
raise SystemExit(1) from error
|
|
print(f"verified release bundle for {args.commit}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|