188 lines
9.2 KiB
Python
188 lines
9.2 KiB
Python
#!/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()
|