Add signed runtime distribution pipeline
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
from datetime import UTC, datetime, timedelta
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||
|
||||
|
||||
META_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(META_ROOT / "tools" / "deployment"))
|
||||
|
||||
from govoplan_deploy.bundle import bundle_paths # noqa: E402
|
||||
from govoplan_deploy.cli import main # noqa: E402
|
||||
from govoplan_deploy.distribution import ( # noqa: E402
|
||||
canonical_json,
|
||||
canonical_signed_payload,
|
||||
)
|
||||
from govoplan_deploy.model import load_spec # noqa: E402
|
||||
from govoplan_deploy.planning import static_checks # noqa: E402
|
||||
|
||||
|
||||
class DeploymentReleaseAdoptionTests(unittest.TestCase):
|
||||
def test_adopts_verified_manifest_and_makes_release_checks_pass(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="govoplan-release-adopt-") as value:
|
||||
root = Path(value)
|
||||
self.assertEqual(
|
||||
0,
|
||||
main(
|
||||
[
|
||||
"init",
|
||||
"--directory",
|
||||
str(root),
|
||||
"--non-interactive",
|
||||
"--module-set",
|
||||
"core",
|
||||
]
|
||||
),
|
||||
)
|
||||
manifest, keyring = self._signed_distribution()
|
||||
manifest_path = root / "source-manifest.json"
|
||||
keyring_path = root / "source-keyring.json"
|
||||
encoded_manifest = canonical_json(manifest)
|
||||
manifest_path.write_bytes(encoded_manifest)
|
||||
keyring_path.write_bytes(canonical_json(keyring))
|
||||
|
||||
result = main(
|
||||
[
|
||||
"verify-release",
|
||||
"--directory",
|
||||
str(root),
|
||||
"--manifest",
|
||||
str(manifest_path),
|
||||
"--manifest-sha256",
|
||||
hashlib.sha256(encoded_manifest).hexdigest(),
|
||||
"--trusted-keyring",
|
||||
str(keyring_path),
|
||||
"--adopt",
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEqual(0, result)
|
||||
paths = bundle_paths(root)
|
||||
spec = load_spec(paths.spec)
|
||||
self.assertEqual("1.2.3", spec.release.version)
|
||||
self.assertEqual("release-1", spec.release.manifest_signature_key_id)
|
||||
self.assertTrue(spec.release.api_image.endswith("a" * 64))
|
||||
release_checks = {
|
||||
item.id: item for item in static_checks(spec, paths)
|
||||
if item.id.startswith("release.") or item.id == "modules.image_composition"
|
||||
}
|
||||
self.assertEqual("ok", release_checks["release.manifest"].level)
|
||||
self.assertEqual(
|
||||
"ok", release_checks["release.signature_verification"].level
|
||||
)
|
||||
self.assertEqual("ok", release_checks["modules.image_composition"].level)
|
||||
|
||||
def test_rejects_manifest_whose_independent_digest_does_not_match(self) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="govoplan-release-adopt-") as value:
|
||||
root = Path(value)
|
||||
main(
|
||||
[
|
||||
"init",
|
||||
"--directory",
|
||||
str(root),
|
||||
"--non-interactive",
|
||||
"--module-set",
|
||||
"core",
|
||||
]
|
||||
)
|
||||
manifest, keyring = self._signed_distribution()
|
||||
manifest_path = root / "source-manifest.json"
|
||||
keyring_path = root / "source-keyring.json"
|
||||
manifest_path.write_bytes(canonical_json(manifest))
|
||||
keyring_path.write_bytes(canonical_json(keyring))
|
||||
|
||||
self.assertEqual(
|
||||
1,
|
||||
main(
|
||||
[
|
||||
"verify-release",
|
||||
"--directory",
|
||||
str(root),
|
||||
"--manifest",
|
||||
str(manifest_path),
|
||||
"--manifest-sha256",
|
||||
"0" * 64,
|
||||
"--trusted-keyring",
|
||||
str(keyring_path),
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _signed_distribution() -> tuple[dict[str, object], dict[str, object]]:
|
||||
now = datetime.now(UTC)
|
||||
private = Ed25519PrivateKey.generate()
|
||||
public = private.public_key().public_bytes(
|
||||
serialization.Encoding.PEM,
|
||||
serialization.PublicFormat.SubjectPublicKeyInfo,
|
||||
).decode("ascii")
|
||||
artifact = {
|
||||
"url": "https://downloads.example.test/artifact.json",
|
||||
"sha256": "f" * 64,
|
||||
}
|
||||
payload: dict[str, object] = {
|
||||
"schema_version": "1",
|
||||
"channel": "stable",
|
||||
"sequence": 1,
|
||||
"version": "1.2.3",
|
||||
"issued_at": (now - timedelta(minutes=1)).isoformat(),
|
||||
"expires_at": (now + timedelta(days=30)).isoformat(),
|
||||
"revoked": False,
|
||||
"deployer": {
|
||||
"url": "https://downloads.example.test/govoplan-deploy.pyz",
|
||||
"sha256": "e" * 64,
|
||||
},
|
||||
"images": {
|
||||
"api": {
|
||||
"index": "registry.example/govoplan/api@sha256:" + "a" * 64,
|
||||
"platforms": {
|
||||
"linux/amd64": "registry.example/govoplan/api@sha256:" + "1" * 64,
|
||||
"linux/arm64": "registry.example/govoplan/api@sha256:" + "2" * 64,
|
||||
},
|
||||
"sbom": dict(artifact),
|
||||
"provenance": dict(artifact),
|
||||
},
|
||||
"web": {
|
||||
"index": "registry.example/govoplan/web@sha256:" + "b" * 64,
|
||||
"platforms": {
|
||||
"linux/amd64": "registry.example/govoplan/web@sha256:" + "3" * 64,
|
||||
"linux/arm64": "registry.example/govoplan/web@sha256:" + "4" * 64,
|
||||
},
|
||||
"sbom": dict(artifact),
|
||||
"provenance": dict(artifact),
|
||||
},
|
||||
},
|
||||
"dependencies": {
|
||||
"postgres": "docker.io/library/postgres@sha256:" + "5" * 64,
|
||||
"redis": "docker.io/library/redis@sha256:" + "6" * 64,
|
||||
"load_balancer": "docker.io/library/haproxy@sha256:" + "7" * 64,
|
||||
},
|
||||
"composition": {
|
||||
"sha256": "c" * 64,
|
||||
"module_ids": [],
|
||||
"packages": [
|
||||
{
|
||||
"name": "govoplan-core",
|
||||
"version": "1.2.3",
|
||||
"wheel_sha256": "8" * 64,
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
payload["signatures"] = [
|
||||
{
|
||||
"key_id": "release-1",
|
||||
"algorithm": "ed25519",
|
||||
"value": base64.b64encode(
|
||||
private.sign(canonical_signed_payload(payload))
|
||||
).decode("ascii"),
|
||||
}
|
||||
]
|
||||
keyring = {
|
||||
"schema_version": "1",
|
||||
"purpose": "govoplan-runtime-distribution",
|
||||
"keys": [
|
||||
{
|
||||
"key_id": "release-1",
|
||||
"algorithm": "ed25519",
|
||||
"status": "active",
|
||||
"public_key_pem": public,
|
||||
"not_before": (now - timedelta(days=1)).isoformat(),
|
||||
"expires_at": (now + timedelta(days=365)).isoformat(),
|
||||
}
|
||||
],
|
||||
}
|
||||
return payload, keyring
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user