Add signed runtime distribution pipeline
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user