"""Use Catalog's existing publisher-key transition before a signed upgrade.""" from __future__ import annotations import json import os from pathlib import Path from typing import Any, Callable, Mapping, Sequence from serviceification_d3_engine import ( CatalogPublisherTrust, DeployError, canonical_bytes, catalog_config_publisher_trust, db_query_scalar, read_catalog_publisher_trust, ) def release_is_registered(envv: Mapping[str, str], config: Mapping[str, Any]) -> bool: # Hex literals preserve exact opaque IDs without interpolating SQL text. app = str(config["appId"]).encode().hex() digest = str(config["expectedAppReleaseDigest"]).encode().hex() result = db_query_scalar( dict(envv), "SELECT COUNT(*) FROM _System_CatalogAppRelease " f"WHERE AppId = X'{app}' AND AppReleaseDigest = X'{digest}';", database=envv["TIDB_DATABASE"], ).strip() if result not in {"0", "1"}: raise DeployError("cannot determine whether the exact AppRelease is registered") return result == "1" def prepare_upgrade_publisher( *, envv: Mapping[str, str], catalog_config: Mapping[str, Any], catalog_image: str, database_env: Path, evidence: Path, execute: Callable[[Sequence[str]], None], read_publisher: Callable[[Mapping[str, str]], CatalogPublisherTrust] = read_catalog_publisher_trust, registered: Callable[[Mapping[str, str], Mapping[str, Any]], bool] = release_is_registered, ) -> bool: """The reviewed operator selects the signed release; Catalog owns trust. Read the predecessor and use its exact keys/epoch as the existing CAS fence. A completed transition needs no second local recovery journal: a retry reads the already-matching Catalog authority. Revoked publishers stay revoked. Call only for an existing installation; first import owns initial trust. """ # Trust is an import-time check. Refreshing an admitted release must not # revive its historical signing key after a later rotation or revocation. if registered(envv, catalog_config): return False target = catalog_config_publisher_trust(catalog_config) current = read_publisher(envv) if not current.active or current.publisher_id != target.publisher_id: raise DeployError("publisher upgrade requires the existing active publisher identity") if current.digest() == target.digest(): return False operation_id = str(catalog_config["idempotencyKey"]) + "-publisher-trust" config = { "schema": "nexuscrm.catalog-publisher-trust-transition/v1", "operationId": operation_id, "publisherId": current.publisher_id, "expectedOwnershipEpoch": str(current.ownership_epoch), "expectedActiveKeys": [key.config_value() for key in current.keys], "targetActiveKeys": [key.config_value() for key in target.keys], } config_path = evidence / "publisher-trust-config.json" output_path = evidence / "publisher-trust-result.json" config_path.write_bytes(canonical_bytes(config)) config_path.chmod(0o400) execute(( "docker", "run", "--rm", "--network", "host", "--user", f"{os.getuid()}:{os.getgid()}", "--env-file", str(database_env), "--mount", f"type=bind,src={config_path},dst=/etc/nexuscrm/publisher-trust.json,readonly", "--mount", f"type=bind,src={evidence},dst=/output", catalog_image, "publisher-trust", "-config", "/etc/nexuscrm/publisher-trust.json", "-output", "/output/publisher-trust-result.json", )) output = json.loads(output_path.read_text()) expected = { "schema": "nexuscrm.catalog-publisher-trust-transition-result/v1", "operationId": operation_id, "publisherId": target.publisher_id, "ownershipEpoch": str(current.ownership_epoch + 1), "previousTrustDigest": current.digest(), "activeTrustDigest": target.digest(), } if output != expected: raise DeployError("publisher transition result disagrees with the signed upgrade") actual = read_publisher(envv) if actual.ownership_epoch != current.ownership_epoch + 1 or actual.digest() != target.digest(): raise DeployError("publisher transition readback disagrees with Catalog authority") return True