208 lines
7.6 KiB
Python
208 lines
7.6 KiB
Python
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()
|