feat: ship dual-signed online module marketplace
This commit is contained in:
parent
3153771bf2
commit
b0eeade03d
40 changed files with 5774 additions and 111 deletions
|
|
@ -0,0 +1,37 @@
|
|||
# Included inside the existing guanghu.chat TLS server block.
|
||||
location = /api/hololake/zero-core/lamp {
|
||||
alias /var/lib/guanghu-public-distribution/current/zero-core/lamp.json;
|
||||
default_type application/json;
|
||||
etag on;
|
||||
add_header Cache-Control "public, max-age=30, must-revalidate" always;
|
||||
}
|
||||
location = /api/hololake/zero-core/lamp.sig {
|
||||
alias /var/lib/guanghu-public-distribution/current/zero-core/lamp.sig.json;
|
||||
default_type application/json;
|
||||
}
|
||||
location = /api/hololake/zero-core/artifact {
|
||||
alias /var/lib/guanghu-public-distribution/current/zero-core/artifact.json;
|
||||
default_type application/json;
|
||||
}
|
||||
location = /api/hololake/marketplace/catalog {
|
||||
alias /var/lib/guanghu-public-distribution/current/marketplace/catalog.json;
|
||||
default_type application/json;
|
||||
etag on;
|
||||
add_header Cache-Control "public, max-age=30, must-revalidate" always;
|
||||
}
|
||||
location = /api/hololake/marketplace/catalog.sig {
|
||||
alias /var/lib/guanghu-public-distribution/current/marketplace/catalog.sig.json;
|
||||
default_type application/json;
|
||||
}
|
||||
location ^~ /api/hololake/marketplace/artifacts/ {
|
||||
alias /var/lib/guanghu-public-distribution/current/marketplace/artifacts/;
|
||||
default_type application/octet-stream;
|
||||
add_header X-Content-Type-Options nosniff always;
|
||||
add_header Cache-Control "public, max-age=31536000, immutable" always;
|
||||
try_files $uri =404;
|
||||
}
|
||||
location = /api/hololake/public-distribution/health {
|
||||
alias /var/lib/guanghu-public-distribution/current/health.json;
|
||||
default_type application/json;
|
||||
add_header Cache-Control "no-store" always;
|
||||
}
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
#!/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()
|
||||
Loading…
Reference in a new issue