hololake-system-architecture/product-source/hololake-native-desktop/server-tools/public-distribution/publish_release.py

188 lines
9.2 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""Verify an origin-authorized HoloLake public release, add enterprise signatures, and atomically publish it."""
from __future__ import annotations
import argparse
import base64
import hashlib
import json
import os
import shutil
import stat
import tempfile
from pathlib import Path
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
ORIGIN_ID = "HLP-SIGNER-ZERO-POINT-ORIGIN-PUBLIC-0001"
ORIGIN_CLASS = "ZERO_POINT_ORIGIN_PUBLIC_SCOPE_SIGNER"
ENTERPRISE_ID = "HLP-SIGNER-ENTERPRISE-ZERO-CORE-DISTRIBUTION-0001"
ENTERPRISE_CLASS = "ENTERPRISE_ZERO_CORE_DISTRIBUTION_SIGNER"
def sha256(raw: bytes) -> str:
return hashlib.sha256(raw).hexdigest()
def load_json(path: Path) -> dict:
return json.loads(path.read_text(encoding="utf-8"))
def canonical_sha(value: object) -> str:
raw = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
return sha256(raw)
def refuse_unsafe_tree(root: Path) -> None:
if not root.is_dir() or root.is_symlink():
raise RuntimeError("stage must be a real directory")
for path in root.rglob("*"):
mode = path.lstat().st_mode
if stat.S_ISLNK(mode) or not (stat.S_ISREG(mode) or stat.S_ISDIR(mode)):
raise RuntimeError(f"unsafe staged path: {path}")
def exact_signed_object(stage: Path, authorization: dict, name: str, origin_key: Ed25519PublicKey) -> bytes:
record = next((item for item in authorization["signedObjects"] if item.get("name") == name), None)
if not record:
raise RuntimeError(f"missing origin authorization: {name}")
raw = (stage / name).read_bytes()
if record.get("sha256") != sha256(raw):
raise RuntimeError(f"origin digest mismatch: {name}")
origin_key.verify(base64.b64decode(record["signatureBase64"], validate=True), raw)
return raw
def signature_bundle(schema: str, plane: str, raw: bytes, origin_signature: str, enterprise_key: Ed25519PrivateKey) -> bytes:
value = {
"schema": schema,
"planeNumber": plane,
"contentSha256": sha256(raw),
"signatures": [
{
"signerId": ORIGIN_ID,
"signerClass": ORIGIN_CLASS,
"algorithm": "Ed25519",
"signatureBase64": origin_signature,
},
{
"signerId": ENTERPRISE_ID,
"signerClass": ENTERPRISE_CLASS,
"algorithm": "Ed25519",
"signatureBase64": base64.b64encode(enterprise_key.sign(raw)).decode("ascii"),
},
],
}
return (json.dumps(value, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
def validate_release(stage: Path, authorization: dict, origin_public: bytes, enterprise_key: Ed25519PrivateKey) -> tuple[bytes, bytes]:
signer = authorization.get("signer", {})
if signer != {
"signerId": ORIGIN_ID,
"signerClass": ORIGIN_CLASS,
"algorithm": "Ed25519",
"publicKeyBase64": base64.b64encode(origin_public).decode("ascii"),
}:
raise RuntimeError("origin signer identity mismatch")
origin_key = Ed25519PublicKey.from_public_bytes(origin_public)
lamp_raw = exact_signed_object(stage, authorization, "zero-core/lamp.json", origin_key)
catalog_raw = exact_signed_object(stage, authorization, "marketplace/catalog.json", origin_key)
lamp = json.loads(lamp_raw)
artifact_raw = (stage / "zero-core/artifact.json").read_bytes()
if lamp.get("schema") != "hololake.public-zero-core-lamp/v1" or lamp.get("planeNumber") != "HLP-DIST-PLANE-0001":
raise RuntimeError("zero-core lamp identity invalid")
if lamp.get("contentRootSha256") != sha256(artifact_raw):
raise RuntimeError("zero-core artifact digest mismatch")
artifact = json.loads(artifact_raw)
if artifact.get("epoch") != lamp.get("epoch") or artifact.get("version") != lamp.get("version"):
raise RuntimeError("zero-core artifact identity mismatch")
catalog = json.loads(catalog_raw)
entries = catalog.get("entries")
if catalog.get("schema") != "hololake.marketplace.catalog/v1" or catalog.get("planeNumber") != "HLP-DIST-PLANE-0003" or not isinstance(entries, list) or not entries:
raise RuntimeError("marketplace catalog identity invalid")
if catalog.get("contentRootSha256") != canonical_sha(entries):
raise RuntimeError("marketplace content root mismatch")
numbers: set[str] = set()
for entry in entries:
number = entry.get("itemNumber", "")
if number in numbers:
raise RuntimeError(f"duplicate marketplace item: {number}")
numbers.add(number)
artifact_path = stage / "marketplace/artifacts" / Path(entry["artifactUrl"]).name
if not artifact_path.is_file() or sha256(artifact_path.read_bytes()) != entry.get("artifactSha256"):
raise RuntimeError(f"marketplace artifact digest mismatch: {number}")
if entry.get("artifactKind") == "PHYSICAL_MODULE":
signature_path = stage / "marketplace/artifacts" / Path(entry["signatureUrl"]).name
if not signature_path.is_file() or not signature_path.read_bytes():
raise RuntimeError(f"physical module signature missing: {number}")
elif entry.get("artifactKind") == "COGNITIVE_SKILL":
if entry.get("executionAuthority") is not False or entry.get("permissions") != [] or entry.get("signatureUrl") is not None:
raise RuntimeError(f"cognitive skill authority boundary invalid: {number}")
else:
raise RuntimeError(f"unknown marketplace kind: {number}")
signed = {item["name"]: item["signatureBase64"] for item in authorization["signedObjects"]}
lamp_bundle = signature_bundle("hololake.public-zero-core-dual-signature/v1", "HLP-DIST-PLANE-0001", lamp_raw, signed["zero-core/lamp.json"], enterprise_key)
catalog_bundle = signature_bundle("hololake.marketplace.catalog-dual-signature/v1", "HLP-DIST-PLANE-0003", catalog_raw, signed["marketplace/catalog.json"], enterprise_key)
return lamp_bundle, catalog_bundle
def publish(stage: Path, destination: Path, lamp_bundle: bytes, catalog_bundle: bytes, release_id: str) -> Path:
releases = destination / "releases"
releases.mkdir(parents=True, exist_ok=True)
final = releases / release_id
if final.exists():
raise RuntimeError(f"release already exists: {release_id}")
temporary = Path(tempfile.mkdtemp(prefix=f".{release_id}.", dir=releases))
try:
# Preserve a raced-in symlink as a symlink so the second tree check rejects
# it; never follow it into a path outside the upload stage.
shutil.copytree(stage, temporary, dirs_exist_ok=True, symlinks=True)
refuse_unsafe_tree(temporary)
(temporary / "zero-core/lamp.sig.json").write_bytes(lamp_bundle)
(temporary / "marketplace/catalog.sig.json").write_bytes(catalog_bundle)
(temporary / "health.json").write_text(json.dumps({"state": "LIVE_DUAL_SIGNED", "releaseId": release_id}, indent=2) + "\n", encoding="utf-8")
for path in temporary.rglob("*"):
os.chmod(path, 0o755 if path.is_dir() else 0o644)
os.replace(temporary, final)
link = destination / f".current.{os.getpid()}"
os.symlink(f"releases/{release_id}", link)
os.replace(link, destination / "current")
except Exception:
shutil.rmtree(temporary, ignore_errors=True)
raise
return final
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--stage", type=Path, required=True)
parser.add_argument("--destination", type=Path, default=Path("/var/lib/guanghu-public-distribution"))
parser.add_argument("--origin-public-key", type=Path, default=Path("/etc/guanghu-public-distribution/origin-ed25519.raw"))
parser.add_argument("--enterprise-private-key", type=Path, default=Path("/etc/guanghu-public-distribution/enterprise-ed25519.pem"))
args = parser.parse_args()
refuse_unsafe_tree(args.stage)
authorization = load_json(args.stage / "origin-authorization.json")
if authorization.get("schema") != "hololake.origin-release-authorization/v1":
raise RuntimeError("origin authorization schema invalid")
release_id = authorization.get("releaseId", "")
if not release_id or "/" in release_id or ".." in release_id:
raise RuntimeError("release id invalid")
origin_public = args.origin_public_key.read_bytes()
if len(origin_public) != 32:
raise RuntimeError("origin public key must be 32 raw bytes")
private = serialization.load_pem_private_key(args.enterprise_private_key.read_bytes(), password=None)
if not isinstance(private, Ed25519PrivateKey):
raise RuntimeError("enterprise release key is not Ed25519")
lamp_bundle, catalog_bundle = validate_release(args.stage, authorization, origin_public, private)
final = publish(args.stage, args.destination, lamp_bundle, catalog_bundle, release_id)
enterprise_public = private.public_key().public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
print(json.dumps({"state": "PUBLISHED", "releaseId": release_id, "path": str(final), "enterprisePublicKeyBase64": base64.b64encode(enterprise_public).decode("ascii")}))
if __name__ == "__main__":
main()