#!/usr/bin/env python3 """Validate and sign a GovOPlaN OCI runtime distribution manifest.""" from __future__ import annotations import argparse import base64 import json from pathlib import Path import sys from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey META_ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(META_ROOT / "tools" / "deployment")) from govoplan_deploy.distribution import ( # noqa: E402 DistributionError, canonical_json, canonical_signed_payload, validate_manifest, ) def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--descriptor", type=Path, required=True) parser.add_argument("--output", type=Path, required=True) parser.add_argument( "--signing-key", action="append", default=[], metavar="KEY_ID=PRIVATE_PEM", required=True, ) return parser def sign_manifest( descriptor: dict[str, object], signing_keys: tuple[tuple[str, Path], ...], ) -> dict[str, object]: payload = dict(descriptor) payload["signatures"] = [ _signature(payload, key_id=key_id, path=path) for key_id, path in signing_keys ] validate_manifest(payload) return payload def _signature( payload: dict[str, object], *, key_id: str, path: Path, ) -> dict[str, str]: try: key = serialization.load_pem_private_key(path.read_bytes(), password=None) except (OSError, ValueError, TypeError) as exc: raise DistributionError(f"cannot load signing key {key_id!r}") from exc if not isinstance(key, Ed25519PrivateKey): raise DistributionError(f"signing key {key_id!r} is not Ed25519") return { "key_id": key_id, "algorithm": "ed25519", "value": base64.b64encode(key.sign(canonical_signed_payload(payload))).decode( "ascii" ), } def _parse_signing_key(value: str) -> tuple[str, Path]: if "=" not in value: raise DistributionError("--signing-key must use KEY_ID=PRIVATE_PEM") key_id, raw_path = value.split("=", 1) if not key_id or not raw_path: raise DistributionError("--signing-key must use KEY_ID=PRIVATE_PEM") return key_id, Path(raw_path).expanduser().resolve() def main() -> int: args = build_parser().parse_args() try: descriptor = json.loads(args.descriptor.read_text(encoding="utf-8")) if not isinstance(descriptor, dict): raise DistributionError("descriptor root must be an object") if "signatures" in descriptor: raise DistributionError("descriptor must not contain signatures") payload = sign_manifest( descriptor, tuple(_parse_signing_key(value) for value in args.signing_key), ) encoded = canonical_json(payload) args.output.parent.mkdir(parents=True, exist_ok=True) temporary = args.output.with_suffix(args.output.suffix + ".tmp") temporary.write_bytes(encoded) temporary.chmod(0o644) temporary.replace(args.output) except (DistributionError, OSError, ValueError, json.JSONDecodeError) as exc: print(f"error: {exc}", file=sys.stderr) return 1 print(f"Runtime distribution manifest written to {args.output}") return 0 if __name__ == "__main__": raise SystemExit(main())