diff --git a/yuqei-infra/scripts/apply-rc06-publication-batch.py b/yuqei-infra/scripts/apply-rc06-publication-batch.py index eeea0d58..d57c3840 100644 --- a/yuqei-infra/scripts/apply-rc06-publication-batch.py +++ b/yuqei-infra/scripts/apply-rc06-publication-batch.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 """Protocol client for applying one frozen RC-06 batch through a real service. -The service contract is deliberately fail-closed. A successful return requires -the exact 60 assets and 14 Prompt modules to be committed, read back online, and -covered by the existing independently signed RC-06 apply receipt. This client -never accepts an unsigned HTTP response as publication evidence. +The service contract is deliberately fail-closed. The exact 60 assets and 14 +Prompt modules must be durably committed and read back online before the +offline apply authority can countersign them. This client never treats an +unsigned HTTP response as publication evidence. The legal-source request/decision/keyring checked here is an externally signed assertion. This program delegates identity, qualification, freshness, status @@ -13,8 +13,7 @@ it does not perform legal review or infer professional status. This file does not implement the cross-Contract/AI transaction service. RC-06 remains blocked until a deployed endpoint can truthfully produce the signed -single-service transaction, rollback and online read-back evidence required by -this client; ordinary sequential publish endpoints are intentionally rejected. +cross-database saga and online read-back evidence required by this client. """ from __future__ import annotations @@ -58,15 +57,17 @@ batch = _load_builder() SCENE_LEGAL_REVIEW_BUILDER_PATH = SCRIPT_DIR / "build-scene-source-legal-review.py" -APPLY_REQUEST_SCHEMA_VERSION = "yuqei.rc06-online-apply-request.v1" -APPLY_RESPONSE_SCHEMA_VERSION = "yuqei.rc06-online-apply-response.v1" -APPLY_STATUS_SCHEMA_VERSION = "yuqei.rc06-online-apply-status.v1" -ROLLBACK_REQUEST_SCHEMA_VERSION = "yuqei.rc06-online-rollback-request.v1" -ROLLBACK_RESPONSE_SCHEMA_VERSION = "yuqei.rc06-online-rollback-response.v1" -RUNTIME_RECEIPT_SCHEMA_VERSION = "yuqei.rc06-runtime-snapshot-receipt.v1" -RUNTIME_PAYLOAD_SCHEMA_VERSION = "yuqei.rc06-runtime-snapshot-payload.v1" -RUNTIME_SIGNATURE_DOMAIN = b"YUQEI_RC06_RUNTIME_SNAPSHOT_RECEIPT_V1\0" -EXECUTION_EVIDENCE_SCHEMA_VERSION = "yuqei.rc06-online-apply-evidence.v1" +APPLY_REQUEST_SCHEMA_VERSION = "yuqei.rc06-online-apply-request.v2" +APPLY_RESPONSE_SCHEMA_VERSION = "yuqei.rc06-online-apply-response.v2" +APPLY_STATUS_SCHEMA_VERSION = "yuqei.rc06-online-apply-status.v2" +ROLLBACK_REQUEST_SCHEMA_VERSION = "yuqei.rc06-online-rollback-request.v2" +ROLLBACK_RESPONSE_SCHEMA_VERSION = "yuqei.rc06-online-rollback-response.v2" +FINALIZE_REQUEST_SCHEMA_VERSION = "yuqei.rc06-online-finalize-request.v1" +FINALIZE_RESPONSE_SCHEMA_VERSION = "yuqei.rc06-online-finalize-response.v1" +FINALIZE_STATUS_SCHEMA_VERSION = "yuqei.rc06-online-finalize-status.v1" +EXECUTION_EVIDENCE_SCHEMA_VERSION = "yuqei.rc06-online-apply-evidence.v2" +FINAL_EVIDENCE_SCHEMA_VERSION = "yuqei.rc06-finalized-apply-evidence.v1" +LEDGER_SCHEMA_VERSION = "yuqei.rc06-local-apply-ledger.v2" AUTH_TOKEN_ENV = "RC06_APPLY_BEARER_TOKEN" MAX_HTTP_BODY_BYTES = 16 * 1024 * 1024 @@ -116,85 +117,30 @@ def validate_scene_legal_review( request: Any, decision: Any, keyring: Any, + keyring_raw_sha256: str, *, now: datetime, ) -> tuple[dict[str, Any], str]: - """Delegate the 30-scene/43-decision proof to its canonical validator.""" + """Delegate legal verification and frozen-keyring trust to the builder.""" - if not isinstance(request, dict) or not isinstance(decision, dict) or not isinstance(keyring, dict): - raise ApplyPublicationError("scene legal review request, decision and keyring are required") - if request.get("candidate_commit") != manifest["candidate"]["commit"]: - raise ApplyPublicationError("scene legal review candidate does not match the publication batch") - scope = request.get("scope") - if not isinstance(scope, dict) or scope.get("scene_count") != 30 or scope.get( - "reference_occurrence_count" - ) != 43: - raise ApplyPublicationError("scene legal review must cover 30 scenes and 43 references") - request_sha256 = request.get("request_sha256") - if not isinstance(request_sha256, str) or not batch.HEX64_RE.fullmatch(request_sha256): - raise ApplyPublicationError("scene legal review request_sha256 is invalid") - if decision.get("request_sha256") != request_sha256: - raise ApplyPublicationError("scene legal review decision is not bound to its request") - validator = _load_scene_legal_review_module() try: - verified_request = validator.verify_request(request, REPO_ROOT) - summary = validator.verify_signed_decision( - verified_request, + return batch.validate_scene_legal_review_evidence( + REPO_ROOT, + manifest, + approval, + request, decision, keyring, - checked_at=now.astimezone(UTC).isoformat(timespec="seconds"), + keyring_raw_sha256, + now=now, + validator=_load_scene_legal_review_module(), ) - except Exception as exc: + except batch.PublicationBatchError as exc: raise ApplyPublicationError(f"scene legal review validation failed: {exc}") from exc - if ( - summary.get("status") != "verified_qualified_human_review" - or summary.get("request_sha256") != request_sha256 - or summary.get("reviewed_occurrence_count") != 43 - or summary.get("all_current_effectiveness_verified") is not True - or summary.get("all_transaction_applicability_verified") is not True - or summary.get("authorizes_publication_by_itself") is not False - ): - raise ApplyPublicationError("scene legal review verification summary is incomplete") - approval_expires_at = batch._parse_time( - approval["payload"]["expires_at"], "approval expires_at" - ) - legal_expires_at = batch._parse_time(summary.get("expires_at"), "legal review expires_at") - if legal_expires_at > approval_expires_at: - raise ApplyPublicationError("scene legal review validity exceeds the approval window") - binding = { - "request_sha256": request_sha256, - "decision_sha256": batch._sha256_json(decision), - "keyring_sha256": batch._sha256_json(keyring), - } - return summary, batch._sha256_json(binding) def expected_runtime_snapshot_results(manifest: Mapping[str, Any]) -> list[dict[str, Any]]: - """Project candidate-rendered executable hashes into online read-back rows.""" - - dependencies = manifest["runtime_prompt_dependencies"] - if dependencies.get("publication_item_count") != 74 or dependencies.get("count") != 14: - raise ApplyPublicationError( - "manifest must distinguish 74 publication items from 14 runtime snapshots" - ) - results = [ - { - "kind": item["kind"], - "runtime_key": item["runtime_key"], - "version": item["version"], - "expected_content_sha256": item["content_sha256"], - "online_content_sha256": item["content_sha256"], - "status": "published_and_resolved", - "publication_dependency": item["publication_dependency"], - "publication_module_dependencies": item[ - "publication_module_dependencies" - ], - } - for item in dependencies["items"] - ] - if len(results) != 14 or len({item["runtime_key"] for item in results}) != 14: - raise ApplyPublicationError("runtime snapshot inventory must contain 14 identities") - return results + return batch.expected_runtime_snapshot_results(manifest) def validate_runtime_snapshot_receipt( @@ -202,90 +148,33 @@ def validate_runtime_snapshot_receipt( manifest_raw_sha256: str, approval: Mapping[str, Any], approval_raw_sha256: str, - legal_review_raw_sha256: str, - online_service_public_key: Ed25519PublicKey, + legal_review_binding_sha256: str, + legal_review_expires_at: str, + apply_service_public_key: Ed25519PublicKey, + online_service_public_key: Any, + online_transaction_receipt: Mapping[str, Any], receipt: Any, *, environment_id: str, - transaction_id: str, + tenant_id: str, ) -> dict[str, Any]: - """Verify signed online evidence for 1 drafting plus 13 review identities.""" - - envelope = _require_exact( - receipt, - "runtime snapshot receipt", - ("schema_version", "receipt_id", "payload", "signature"), - ) - if envelope["schema_version"] != RUNTIME_RECEIPT_SCHEMA_VERSION: - raise ApplyPublicationError("runtime snapshot receipt schema is unsupported") - payload = _require_exact( - envelope["payload"], - "runtime snapshot payload", - ( - "schema_version", - "key_id", - "batch_manifest_raw_sha256", - "batch_manifest_payload_sha256", - "approval_receipt_sha256", - "legal_source_review_binding_sha256", - "environment_id", - "transaction_id", - "captured_at", - "generated_by", - "transaction_mode", - "publication_item_count", - "runtime_snapshot_dependencies", - "runtime_snapshot_dependency_count", - "status", - ), - ) - if ( - payload["schema_version"] != RUNTIME_PAYLOAD_SCHEMA_VERSION - or payload["key_id"] != batch.TRUSTED_ONLINE_APPLY_SERVICE_KEY_ID - or payload["status"] != "runtime_snapshots_verified" - or payload["batch_manifest_raw_sha256"] != manifest_raw_sha256 - or payload["batch_manifest_payload_sha256"] != manifest["manifest_payload_sha256"] - or payload["approval_receipt_sha256"] != approval_raw_sha256 - or payload["legal_source_review_binding_sha256"] != legal_review_raw_sha256 - or payload["environment_id"] != environment_id - or payload["transaction_id"] != transaction_id - or payload["transaction_mode"] != "single_service_atomic" - or payload["publication_item_count"] != 74 - or payload["runtime_snapshot_dependency_count"] != 14 - or payload["runtime_snapshot_dependencies"] - != expected_runtime_snapshot_results(manifest) - or payload["generated_by"] - != {"type": "system", "component": batch.TRUSTED_ONLINE_APPLY_SERVICE_COMPONENT} - ): - raise ApplyPublicationError("runtime snapshot receipt identity or 14-item evidence drift") - captured_at = batch._parse_time(payload["captured_at"], "runtime snapshot captured_at") - approval_payload = approval["payload"] - if captured_at < batch._parse_time( - approval_payload["approved_at"], "approval approved_at" - ) or captured_at >= batch._parse_time( - approval_payload["expires_at"], "approval expires_at" - ): - raise ApplyPublicationError("runtime snapshots were captured outside the approval window") - if _sha256(_public_raw(online_service_public_key)) != batch.TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SHA256: - raise ApplyPublicationError("runtime snapshot signer does not match the frozen online key pin") try: - signature = base64.b64decode( - _require_text(envelope["signature"], "runtime snapshot signature", maximum=1024), - validate=True, + return batch.validate_runtime_snapshot_receipt( + manifest, + manifest_raw_sha256, + approval_raw_sha256, + approval, + apply_service_public_key, + online_service_public_key, + online_transaction_receipt, + receipt, + legal_review_binding_sha256=legal_review_binding_sha256, + legal_review_expires_at=legal_review_expires_at, + environment_id=environment_id, + tenant_id=tenant_id, ) - online_service_public_key.verify( - signature, - RUNTIME_SIGNATURE_DOMAIN + batch._canonical(payload), - ) - except (TypeError, ValueError, InvalidSignature) as exc: - raise ApplyPublicationError("runtime snapshot service signature verification failed") from exc - expected_id = "rc06-runtime-snapshot-" + _sha256( - batch._canonical(payload) + signature - )[:24] - if envelope["receipt_id"] != expected_id: - raise ApplyPublicationError("runtime snapshot receipt_id does not match signed material") - batch._assert_no_secret_material(envelope, "runtime snapshot receipt") - return envelope + except batch.PublicationBatchError as exc: + raise ApplyPublicationError(str(exc)) from exc class JsonHttpClient: @@ -373,8 +262,15 @@ class ApplyLedger: self.connection = sqlite3.connect(self.path, timeout=30, isolation_level=None) self.connection.execute("PRAGMA journal_mode=WAL") self.connection.execute("PRAGMA synchronous=FULL") + legacy_operations_present = self.connection.execute( + "SELECT 1 FROM sqlite_master WHERE type='table' AND name='rc06_apply_operations'" + ).fetchone() is not None self.connection.executescript( """ + CREATE TABLE IF NOT EXISTS rc06_apply_ledger_metadata ( + singleton INTEGER PRIMARY KEY CHECK(singleton = 1), + schema_version TEXT NOT NULL + ); CREATE TABLE IF NOT EXISTS rc06_apply_operations ( idempotency_key TEXT PRIMARY KEY, request_sha256 TEXT NOT NULL, @@ -393,6 +289,20 @@ class ApplyLedger: ); """ ) + metadata = self.connection.execute( + "SELECT schema_version FROM rc06_apply_ledger_metadata WHERE singleton = 1" + ).fetchone() + if metadata is None: + if legacy_operations_present: + raise ApplyPublicationError( + "legacy apply ledger cannot be silently upgraded; use a new ledger path" + ) + self.connection.execute( + "INSERT INTO rc06_apply_ledger_metadata(singleton, schema_version) VALUES (1, ?)", + (LEDGER_SCHEMA_VERSION,), + ) + elif metadata[0] != LEDGER_SCHEMA_VERSION: + raise ApplyPublicationError("apply ledger schema is unsupported") def reserve(self, key: str, request_sha256: str, now: datetime) -> dict[str, Any] | None: self.connection.execute("BEGIN IMMEDIATE") @@ -404,11 +314,14 @@ class ApplyLedger: ).fetchone() if row is not None and row[0] != request_sha256: raise ApplyPublicationError("idempotency key is bound to another request") - if row is not None and row[1] == "succeeded": + if row is not None and row[1] in { + "online_committed_pending_countersign", + "succeeded", + }: receipt = json.loads(row[2]) self.connection.execute("COMMIT") return receipt - if row is not None and row[1] in {"dispatching", "verifying"}: + if row is not None: raise ApplyPublicationError("an apply with this idempotency key needs reconciliation") timestamp = now.astimezone(UTC).isoformat(timespec="seconds") self.connection.execute( @@ -424,6 +337,20 @@ class ApplyLedger: self.connection.execute("ROLLBACK") raise + def load(self, key: str, request_sha256: str) -> tuple[str, dict[str, Any]]: + row = self.connection.execute( + "SELECT request_sha256, state, apply_receipt_json FROM rc06_apply_operations " + "WHERE idempotency_key = ?", + (key,), + ).fetchone() + if row is None or row[0] != request_sha256 or row[2] is None: + raise ApplyPublicationError("online execution evidence is absent from the ledger") + try: + evidence = json.loads(row[2]) + except (TypeError, json.JSONDecodeError) as exc: + raise ApplyPublicationError("stored online execution evidence is invalid") from exc + return str(row[1]), evidence + def state( self, key: str, @@ -467,34 +394,36 @@ class ApplyLedger: def _idempotency_key( manifest_raw_sha256: str, approval_raw_sha256: str, - legal_review_raw_sha256: str, + legal_review_binding_sha256: str, environment_id: str, + tenant_id: str, ) -> str: - identity = { - "operation": batch.APPLY_OPERATION, - "manifest_raw_sha256": manifest_raw_sha256, - "approval_raw_sha256": approval_raw_sha256, - "legal_review_raw_sha256": legal_review_raw_sha256, - "environment_id": environment_id, - } - return _sha256(batch._canonical(identity)) + return batch.publication_idempotency_key( + manifest_raw_sha256, + approval_raw_sha256, + legal_review_binding_sha256, + environment_id, + tenant_id, + ) def _transaction_id(idempotency_key: str) -> str: - return f"rc06-{idempotency_key}" + return batch.publication_transaction_id(idempotency_key) def _headers( key: str, manifest_raw_sha256: str, approval_raw_sha256: str, - legal_review_raw_sha256: str, + legal_review_binding_sha256: str, + tenant_id: str, ) -> dict[str, str]: return { "Idempotency-Key": key, "X-RC06-Manifest-SHA256": manifest_raw_sha256, "X-RC06-Approval-SHA256": approval_raw_sha256, - "X-RC06-Legal-Review-SHA256": legal_review_raw_sha256, + "X-RC06-Legal-Review-SHA256": legal_review_binding_sha256, + "X-RC06-Tenant-ID": tenant_id, } @@ -503,7 +432,11 @@ def _validate_apply_response( *, key: str, request_sha256: str, - legal_review_raw_sha256: str, + legal_review_binding_sha256: str, + legal_review_expires_at: str, + transaction_id: str, + environment_id: str, + tenant_id: str, ) -> dict[str, Any]: row = _require_exact( response, @@ -513,10 +446,15 @@ def _validate_apply_response( "idempotency_key", "request_sha256", "legal_source_review_binding_sha256", + "legal_source_review_expires_at", + "transaction_id", + "environment_id", + "tenant_id", "transaction_mode", + "cross_database_atomicity", "publication_item_count", "runtime_snapshot_dependency_count", - "apply_receipt", + "online_transaction_receipt", "runtime_snapshot_receipt", ), ) @@ -524,8 +462,13 @@ def _validate_apply_response( row["schema_version"] != APPLY_RESPONSE_SCHEMA_VERSION or row["idempotency_key"] != key or row["request_sha256"] != request_sha256 - or row["legal_source_review_binding_sha256"] != legal_review_raw_sha256 - or row["transaction_mode"] != "single_service_atomic" + or row["legal_source_review_binding_sha256"] != legal_review_binding_sha256 + or row["legal_source_review_expires_at"] != legal_review_expires_at + or row["transaction_id"] != transaction_id + or row["environment_id"] != environment_id + or row["tenant_id"] != tenant_id + or row["transaction_mode"] != batch.TRANSACTION_MODE + or row["cross_database_atomicity"] is not False or row["publication_item_count"] != 74 or row["runtime_snapshot_dependency_count"] != 14 ): @@ -538,8 +481,10 @@ def _validate_online_status( *, key: str, transaction_id: str, - legal_review_raw_sha256: str, - apply_receipt: Mapping[str, Any], + legal_review_binding_sha256: str, + environment_id: str, + tenant_id: str, + online_transaction_receipt: Mapping[str, Any], runtime_snapshot_receipt: Mapping[str, Any], expected_publication_results: list[dict[str, Any]], expected_runtime_results: list[dict[str, Any]], @@ -553,12 +498,17 @@ def _validate_online_status( "transaction_id", "state", "legal_source_review_binding_sha256", + "environment_id", + "tenant_id", + "transaction_mode", + "cross_database_atomicity", "publication_item_results", "publication_item_count", "runtime_snapshot_dependencies", "runtime_snapshot_dependency_count", - "apply_receipt_sha256", + "online_transaction_receipt_sha256", "runtime_snapshot_receipt_sha256", + "participant_evidence", ), ) if ( @@ -566,28 +516,55 @@ def _validate_online_status( or row["idempotency_key"] != key or row["transaction_id"] != transaction_id or row["state"] != "committed_and_verified" - or row["legal_source_review_binding_sha256"] != legal_review_raw_sha256 + or row["legal_source_review_binding_sha256"] != legal_review_binding_sha256 + or row["environment_id"] != environment_id + or row["tenant_id"] != tenant_id + or row["transaction_mode"] != batch.TRANSACTION_MODE + or row["cross_database_atomicity"] is not False or row["publication_item_results"] != expected_publication_results or row["publication_item_count"] != 74 or row["runtime_snapshot_dependencies"] != expected_runtime_results or row["runtime_snapshot_dependency_count"] != 14 - or row["apply_receipt_sha256"] != batch._sha256_json(apply_receipt) + or row["online_transaction_receipt_sha256"] + != batch._sha256_json(online_transaction_receipt) or row["runtime_snapshot_receipt_sha256"] != batch._sha256_json(runtime_snapshot_receipt) + or row["participant_evidence"] + != online_transaction_receipt["payload"]["participant_evidence"] ): raise ApplyPublicationError("online read-after-write hash verification failed") return row +def _remote_status_state( + status: Any, + *, + key: str, + transaction_id: str, +) -> str | None: + if ( + not isinstance(status, dict) + or status.get("schema_version") != APPLY_STATUS_SCHEMA_VERSION + or status.get("idempotency_key") != key + or status.get("transaction_id") != transaction_id + or not isinstance(status.get("state"), str) + ): + return None + return status["state"] + + def _compensate( client: JsonHttpClient, *, + manifest: Mapping[str, Any], key: str, transaction_id: str, headers: Mapping[str, str], manifest_raw_sha256: str, approval_raw_sha256: str, - legal_review_raw_sha256: str, + legal_review_binding_sha256: str, + environment_id: str, + tenant_id: str, ) -> dict[str, Any]: request = { "schema_version": ROLLBACK_REQUEST_SCHEMA_VERSION, @@ -595,7 +572,11 @@ def _compensate( "transaction_id": transaction_id, "batch_manifest_raw_sha256": manifest_raw_sha256, "approval_receipt_sha256": approval_raw_sha256, - "legal_source_review_binding_sha256": legal_review_raw_sha256, + "legal_source_review_binding_sha256": legal_review_binding_sha256, + "environment_id": environment_id, + "tenant_id": tenant_id, + "transaction_mode": batch.TRANSACTION_MODE, + "cross_database_atomicity": False, } response = client.request( "POST", @@ -611,7 +592,11 @@ def _compensate( "idempotency_key", "transaction_id", "state", + "participant_states_before", + "participant_states_after", + "compensation_item_results", "restored_item_count", + "removed_item_count", "rollback_evidence_sha256", ), ) @@ -620,14 +605,205 @@ def _compensate( or row["idempotency_key"] != key or row["transaction_id"] != transaction_id or row["state"] != "rolled_back" - or row["restored_item_count"] != 0 - or not batch.HEX64_RE.fullmatch(str(row["rollback_evidence_sha256"])) + or not isinstance(row["restored_item_count"], int) + or isinstance(row["restored_item_count"], bool) + or row["restored_item_count"] < 0 + or not isinstance(row["removed_item_count"], int) + or isinstance(row["removed_item_count"], bool) + or row["removed_item_count"] < 0 ): raise ApplyPublicationError("online rollback evidence is invalid") + expected_participants = ["ai-platform", "contract-platform"] + before = row["participant_states_before"] + after = row["participant_states_after"] + if not isinstance(before, list) or not isinstance(after, list): + raise ApplyPublicationError("online rollback participant evidence is invalid") + for label, states, allowed in ( + ("before", before, {"not_started", "prepared", "committed", "failed"}), + ("after", after, {"unchanged", "compensated_and_verified"}), + ): + if [item.get("participant") for item in states if isinstance(item, dict)] != expected_participants: + raise ApplyPublicationError(f"online rollback {label} participant identities drift") + for index, value in enumerate(states): + state_row = _require_exact( + value, + f"online rollback {label} participant {index}", + ("participant", "state", "write_count", "durable_state_sha256"), + ) + if ( + state_row["state"] not in allowed + or not isinstance(state_row["write_count"], int) + or isinstance(state_row["write_count"], bool) + or state_row["write_count"] < 0 + or state_row["write_count"] > 74 + ): + raise ApplyPublicationError( + f"online rollback {label} participant state drift" + ) + supplied_state_sha = batch._require_sha( + state_row["durable_state_sha256"], + f"online rollback {label} participant state SHA-256", + ) + if supplied_state_sha != batch._sha256_json( + { + key_name: child + for key_name, child in state_row.items() + if key_name != "durable_state_sha256" + } + ): + raise ApplyPublicationError( + f"online rollback {label} participant durable state hash drift" + ) + item_results = row["compensation_item_results"] + if not isinstance(item_results, list): + raise ApplyPublicationError("online rollback item results are invalid") + identities: set[tuple[str, str]] = set() + expected_identities = { + (item["kind"], item["item_id"]) + for item in batch._expected_apply_item_results(manifest) + } + restored_count = 0 + removed_count = 0 + for index, value in enumerate(item_results): + item = _require_exact( + value, + f"online rollback item {index}", + ( + "kind", + "item_id", + "pre_apply_content_sha256", + "post_compensation_content_sha256", + "action", + "status", + ), + ) + identity = (item["kind"], item["item_id"]) + if ( + identity in identities + or identity not in expected_identities + or item["status"] != "verified" + ): + raise ApplyPublicationError("online rollback item identity/status drift") + identities.add(identity) + if item["action"] == "restored_previous": + restored_count += 1 + batch._require_sha( + item["pre_apply_content_sha256"], + "online rollback restored content SHA-256", + ) + if item["post_compensation_content_sha256"] != item["pre_apply_content_sha256"]: + raise ApplyPublicationError("online rollback restored content hash drift") + elif item["action"] == "removed_new": + removed_count += 1 + if ( + item["pre_apply_content_sha256"] is not None + or item["post_compensation_content_sha256"] is not None + ): + raise ApplyPublicationError("online rollback removal evidence drift") + else: + raise ApplyPublicationError("online rollback action is unsupported") + if ( + restored_count != row["restored_item_count"] + or removed_count != row["removed_item_count"] + ): + raise ApplyPublicationError("online rollback item counts drift") + rollback_sha = batch._require_sha( + row["rollback_evidence_sha256"], "online rollback evidence SHA-256" + ) + if rollback_sha != batch._sha256_json( + { + key_name: child + for key_name, child in row.items() + if key_name != "rollback_evidence_sha256" + } + ): + raise ApplyPublicationError("online rollback evidence hash drift") return row -def apply_publication_batch( +def _validate_execution_evidence( + evidence: Any, + *, + manifest: Mapping[str, Any], + manifest_raw_sha256: str, + approval: Mapping[str, Any], + approval_raw_sha256: str, + apply_service_public_key: Ed25519PublicKey, + online_service_public_key: Any, + legal_review_binding_sha256: str, + legal_review_expires_at: str, + environment_id: str, + tenant_id: str, + idempotency_key: str, + transaction_id: str, +) -> dict[str, Any]: + row = _require_exact( + evidence, + "online execution evidence", + ( + "schema_version", + "idempotency_key", + "transaction_id", + "request_sha256", + "legal_source_review_binding_sha256", + "legal_source_review_expires_at", + "environment_id", + "tenant_id", + "transaction_mode", + "cross_database_atomicity", + "publication_item_count", + "runtime_snapshot_dependency_count", + "online_transaction_receipt", + "runtime_snapshot_receipt", + ), + ) + if ( + row["schema_version"] != EXECUTION_EVIDENCE_SCHEMA_VERSION + or row["idempotency_key"] != idempotency_key + or row["transaction_id"] != transaction_id + or not batch.HEX64_RE.fullmatch(str(row["request_sha256"])) + or row["legal_source_review_binding_sha256"] + != legal_review_binding_sha256 + or row["legal_source_review_expires_at"] != legal_review_expires_at + or row["environment_id"] != environment_id + or row["tenant_id"] != tenant_id + or row["transaction_mode"] != batch.TRANSACTION_MODE + or row["cross_database_atomicity"] is not False + or row["publication_item_count"] != 74 + or row["runtime_snapshot_dependency_count"] != 14 + ): + raise ApplyPublicationError("online execution evidence identity drift") + online_receipt = batch.validate_online_transaction_receipt( + manifest, + manifest_raw_sha256, + approval_raw_sha256, + approval, + apply_service_public_key, + online_service_public_key, + row["online_transaction_receipt"], + legal_review_binding_sha256=legal_review_binding_sha256, + legal_review_expires_at=legal_review_expires_at, + environment_id=environment_id, + tenant_id=tenant_id, + ) + batch.validate_runtime_snapshot_receipt( + manifest, + manifest_raw_sha256, + approval_raw_sha256, + approval, + apply_service_public_key, + online_service_public_key, + online_receipt, + row["runtime_snapshot_receipt"], + legal_review_binding_sha256=legal_review_binding_sha256, + legal_review_expires_at=legal_review_expires_at, + environment_id=environment_id, + tenant_id=tenant_id, + ) + return row + + +def execute_online_publication_batch( *, manifest: Mapping[str, Any], manifest_raw_sha256: str, @@ -636,16 +812,19 @@ def apply_publication_batch( approval_keyring: Mapping[str, Any], approval_root_public_key: Ed25519PublicKey, apply_service_public_key: Ed25519PublicKey, - online_service_public_key: Ed25519PublicKey, + online_service_public_key: Any, legal_review_request: Mapping[str, Any], legal_review_decision: Mapping[str, Any], legal_review_keyring: Mapping[str, Any], + legal_review_keyring_raw_sha256: str, + authority_challenge: Mapping[str, Any], environment_id: str, + tenant_id: str, client: JsonHttpClient, ledger: ApplyLedger, now: datetime, ) -> dict[str, Any]: - """Perform one authenticated, idempotent and recoverable online apply.""" + """Execute the online saga and persist evidence pending offline countersign.""" batch.validate_manifest(manifest) if manifest["runtime_prompt_dependencies"]["items"] != batch._load_executable_prompt_dependencies( @@ -657,6 +836,7 @@ def apply_publication_batch( manifest_raw_sha256 = batch._require_sha(manifest_raw_sha256, "manifest raw SHA-256") approval_raw_sha256 = batch._require_sha(approval_raw_sha256, "approval raw SHA-256") environment_id = _require_text(environment_id, "environment_id", maximum=256) + tenant_id = _require_text(tenant_id, "tenant_id", maximum=64) batch.validate_approval_receipt( manifest, manifest_raw_sha256, @@ -664,18 +844,20 @@ def apply_publication_batch( approval_keyring, approval_root_public_key, ) - _, legal_review_binding_sha256 = validate_scene_legal_review( + legal_review_summary, legal_review_binding_sha256 = validate_scene_legal_review( manifest, approval, legal_review_request, legal_review_decision, legal_review_keyring, + legal_review_keyring_raw_sha256, now=now, ) + legal_review_expires_at = legal_review_summary["expires_at"] keys = { - _public_raw(approval_root_public_key), - _public_raw(apply_service_public_key), - _public_raw(online_service_public_key), + batch._public_key_spki(approval_root_public_key), + batch._public_key_spki(apply_service_public_key), + batch._public_key_spki(online_service_public_key), } if len(keys) != 3: raise ApplyPublicationError("approval, apply and online service keys must be distinct") @@ -684,22 +866,40 @@ def apply_publication_batch( approval_raw_sha256, legal_review_binding_sha256, environment_id, + tenant_id, ) transaction_id = _transaction_id(key) + batch.validate_apply_challenge( + manifest, + manifest_raw_sha256, + approval_raw_sha256, + apply_service_public_key, + authority_challenge, + legal_review_binding_sha256=legal_review_binding_sha256, + legal_review_expires_at=legal_review_expires_at, + environment_id=environment_id, + tenant_id=tenant_id, + ) request_payload = { "schema_version": APPLY_REQUEST_SCHEMA_VERSION, "idempotency_key": key, "transaction_id": transaction_id, "environment_id": environment_id, + "tenant_id": tenant_id, + "transaction_mode": batch.TRANSACTION_MODE, + "cross_database_atomicity": False, "batch_manifest_raw_sha256": manifest_raw_sha256, "batch_manifest_payload_sha256": manifest["manifest_payload_sha256"], "approval_receipt_sha256": approval_raw_sha256, "legal_source_review_binding_sha256": legal_review_binding_sha256, + "legal_source_review_expires_at": legal_review_expires_at, + "authority_challenge": authority_challenge, "manifest": manifest, "approval_receipt": approval, "legal_source_review_request": legal_review_request, "legal_source_review_decision": legal_review_decision, "legal_source_review_keyring": legal_review_keyring, + "legal_source_review_keyring_raw_sha256": legal_review_keyring_raw_sha256, "expected_publication_item_results": batch._expected_apply_item_results(manifest), "expected_publication_item_count": 74, "expected_runtime_snapshot_dependencies": expected_runtime_snapshot_results(manifest), @@ -708,53 +908,32 @@ def apply_publication_batch( request_sha256 = batch._sha256_json(request_payload) replay = ledger.reserve(key, request_sha256, now) if replay is not None: - replay = _require_exact( + if replay.get("schema_version") == FINAL_EVIDENCE_SCHEMA_VERSION: + replay = replay.get("online_execution") + return _validate_execution_evidence( replay, - "stored online apply evidence", - ( - "schema_version", - "publication_item_count", - "runtime_snapshot_dependency_count", - "apply_receipt", - "runtime_snapshot_receipt", - ), - ) - if ( - replay["schema_version"] != EXECUTION_EVIDENCE_SCHEMA_VERSION - or replay["publication_item_count"] != 74 - or replay["runtime_snapshot_dependency_count"] != 14 - ): - raise ApplyPublicationError("stored online apply evidence count drift") - apply_receipt = replay["apply_receipt"] - batch.validate_apply_receipt( - manifest, - manifest_raw_sha256, - approval, - approval_keyring, - approval_root_public_key, - apply_service_public_key, - online_service_public_key, - approval_raw_sha256, - apply_receipt, - ) - validate_runtime_snapshot_receipt( - manifest, - manifest_raw_sha256, - approval, - approval_raw_sha256, - legal_review_binding_sha256, - online_service_public_key, - replay["runtime_snapshot_receipt"], + manifest=manifest, + manifest_raw_sha256=manifest_raw_sha256, + approval=approval, + approval_raw_sha256=approval_raw_sha256, + apply_service_public_key=apply_service_public_key, + online_service_public_key=online_service_public_key, + legal_review_binding_sha256=legal_review_binding_sha256, + legal_review_expires_at=legal_review_expires_at, environment_id=environment_id, + tenant_id=tenant_id, + idempotency_key=key, transaction_id=transaction_id, ) - if apply_receipt["payload"]["online_transaction_receipt"]["payload"]["transaction_id"] != transaction_id: - raise ApplyPublicationError("stored apply receipt idempotency binding drift") - return replay headers = _headers( - key, manifest_raw_sha256, approval_raw_sha256, legal_review_binding_sha256 + key, + manifest_raw_sha256, + approval_raw_sha256, + legal_review_binding_sha256, + tenant_id, ) dispatch_started = False + response: Any = None try: dispatch_started = True response = client.request( @@ -767,20 +946,26 @@ def apply_publication_batch( response, key=key, request_sha256=request_sha256, - legal_review_raw_sha256=legal_review_binding_sha256, + legal_review_binding_sha256=legal_review_binding_sha256, + legal_review_expires_at=legal_review_expires_at, + transaction_id=transaction_id, + environment_id=environment_id, + tenant_id=tenant_id, ) - apply_receipt = response["apply_receipt"] + online_receipt = response["online_transaction_receipt"] runtime_receipt = response["runtime_snapshot_receipt"] - batch.validate_apply_receipt( + batch.validate_online_transaction_receipt( manifest, manifest_raw_sha256, + approval_raw_sha256, approval, - approval_keyring, - approval_root_public_key, apply_service_public_key, online_service_public_key, - approval_raw_sha256, - apply_receipt, + online_receipt, + legal_review_binding_sha256=legal_review_binding_sha256, + legal_review_expires_at=legal_review_expires_at, + environment_id=environment_id, + tenant_id=tenant_id, ) validate_runtime_snapshot_receipt( manifest, @@ -788,12 +973,15 @@ def apply_publication_batch( approval, approval_raw_sha256, legal_review_binding_sha256, + legal_review_expires_at, + apply_service_public_key, online_service_public_key, + online_receipt, runtime_receipt, environment_id=environment_id, - transaction_id=transaction_id, + tenant_id=tenant_id, ) - online_payload = apply_receipt["payload"]["online_transaction_receipt"]["payload"] + online_payload = online_receipt["payload"] if online_payload["transaction_id"] != transaction_id: raise ApplyPublicationError("signed apply receipt is not bound to the idempotency identity") ledger.state(key, "verifying", response, now) @@ -807,20 +995,37 @@ def apply_publication_batch( status, key=key, transaction_id=transaction_id, - legal_review_raw_sha256=legal_review_binding_sha256, - apply_receipt=apply_receipt, + legal_review_binding_sha256=legal_review_binding_sha256, + environment_id=environment_id, + tenant_id=tenant_id, + online_transaction_receipt=online_receipt, runtime_snapshot_receipt=runtime_receipt, expected_publication_results=batch._expected_apply_item_results(manifest), expected_runtime_results=expected_runtime_snapshot_results(manifest), ) evidence = { "schema_version": EXECUTION_EVIDENCE_SCHEMA_VERSION, + "idempotency_key": key, + "transaction_id": transaction_id, + "request_sha256": request_sha256, + "legal_source_review_binding_sha256": legal_review_binding_sha256, + "legal_source_review_expires_at": legal_review_expires_at, + "environment_id": environment_id, + "tenant_id": tenant_id, + "transaction_mode": batch.TRANSACTION_MODE, + "cross_database_atomicity": False, "publication_item_count": 74, "runtime_snapshot_dependency_count": 14, - "apply_receipt": apply_receipt, + "online_transaction_receipt": online_receipt, "runtime_snapshot_receipt": runtime_receipt, } - ledger.state(key, "succeeded", status, now, receipt=evidence) + ledger.state( + key, + "online_committed_pending_countersign", + status, + now, + receipt=evidence, + ) return evidence except Exception as exc: if not isinstance(exc, ApplyPublicationError): @@ -829,15 +1034,74 @@ def apply_publication_batch( else: failure = exc if dispatch_started: + try: + reconciliation = client.request( + "GET", + f"/internal/rc06/publication-transactions/{transaction_id}/status", + payload=None, + headers=headers, + ) + remote_state = _remote_status_state( + reconciliation, + key=key, + transaction_id=transaction_id, + ) + except Exception as reconciliation_exc: + ledger.state( + key, + "commit_status_unknown", + { + "apply_error": str(failure), + "reconciliation_error": str(reconciliation_exc), + }, + now, + error=f"{failure}; commit status is unknown", + ) + raise ApplyPublicationError( + "online apply evidence failed and commit status is unknown; no rollback was attempted" + ) from reconciliation_exc + if remote_state in { + "contract_committed", + "evidence_pending", + "committed_and_verified", + "online_committed_pending_countersign", + }: + ledger.state( + key, + "committed_evidence_invalid_no_go", + reconciliation, + now, + error=str(failure), + ) + raise ApplyPublicationError( + "online saga is terminally committed but local evidence verification failed; rollback is forbidden" + ) from failure + if remote_state == "rolled_back": + ledger.state(key, "rolled_back", reconciliation, now, error=str(failure)) + raise failure + if remote_state not in {"prepared", "ai_committed", "failed_before_commit"}: + ledger.state( + key, + "commit_status_unknown", + reconciliation, + now, + error=str(failure), + ) + raise ApplyPublicationError( + "online saga state is not safely compensatable; no rollback was attempted" + ) from failure try: rollback = _compensate( client, + manifest=manifest, key=key, transaction_id=transaction_id, headers=headers, manifest_raw_sha256=manifest_raw_sha256, approval_raw_sha256=approval_raw_sha256, - legal_review_raw_sha256=legal_review_binding_sha256, + legal_review_binding_sha256=legal_review_binding_sha256, + environment_id=environment_id, + tenant_id=tenant_id, ) ledger.state(key, "rolled_back", rollback, now, error=str(failure)) except Exception as rollback_exc: @@ -856,6 +1120,227 @@ def apply_publication_batch( raise failure +def finalize_publication_batch( + *, + manifest: Mapping[str, Any], + manifest_raw_sha256: str, + approval: Mapping[str, Any], + approval_raw_sha256: str, + approval_keyring: Mapping[str, Any], + approval_root_public_key: Ed25519PublicKey, + apply_service_public_key: Ed25519PublicKey, + online_service_public_key: Any, + legal_review_binding_sha256: str, + legal_review_expires_at: str, + environment_id: str, + tenant_id: str, + online_execution: Mapping[str, Any], + apply_receipt: Mapping[str, Any], + client: JsonHttpClient, + ledger: ApplyLedger, + now: datetime, +) -> dict[str, Any]: + """Verify the offline countersign and move the local ledger to final success.""" + + key = batch.publication_idempotency_key( + manifest_raw_sha256, + approval_raw_sha256, + legal_review_binding_sha256, + environment_id, + tenant_id, + ) + transaction_id = batch.publication_transaction_id(key) + execution = _validate_execution_evidence( + online_execution, + manifest=manifest, + manifest_raw_sha256=manifest_raw_sha256, + approval=approval, + approval_raw_sha256=approval_raw_sha256, + apply_service_public_key=apply_service_public_key, + online_service_public_key=online_service_public_key, + legal_review_binding_sha256=legal_review_binding_sha256, + legal_review_expires_at=legal_review_expires_at, + environment_id=environment_id, + tenant_id=tenant_id, + idempotency_key=key, + transaction_id=transaction_id, + ) + state, stored = ledger.load(key, execution["request_sha256"]) + if state not in { + "online_committed_pending_countersign", + "finalize_failed_no_go", + "succeeded", + }: + raise ApplyPublicationError("online saga is not eligible for offline finalization") + if state == "succeeded" and stored.get("schema_version") == FINAL_EVIDENCE_SCHEMA_VERSION: + return stored + if stored != execution: + raise ApplyPublicationError("supplied online evidence differs from the durable ledger") + batch.validate_apply_receipt( + manifest, + manifest_raw_sha256, + approval, + approval_keyring, + approval_root_public_key, + apply_service_public_key, + online_service_public_key, + approval_raw_sha256, + apply_receipt, + legal_review_binding_sha256=legal_review_binding_sha256, + legal_review_expires_at=legal_review_expires_at, + environment_id=environment_id, + tenant_id=tenant_id, + ) + if ( + apply_receipt["payload"]["online_transaction_receipt"] + != execution["online_transaction_receipt"] + or apply_receipt["payload"]["runtime_snapshot_receipt"] + != execution["runtime_snapshot_receipt"] + ): + raise ApplyPublicationError("offline countersign is not bound to this receipt pair") + apply_receipt_sha256 = batch._sha256_json(apply_receipt) + finalize_request = { + "schema_version": FINALIZE_REQUEST_SCHEMA_VERSION, + "idempotency_key": key, + "transaction_id": transaction_id, + "environment_id": environment_id, + "tenant_id": tenant_id, + "batch_manifest_raw_sha256": manifest_raw_sha256, + "approval_receipt_sha256": approval_raw_sha256, + "legal_source_review_binding_sha256": legal_review_binding_sha256, + "online_transaction_receipt_sha256": batch._sha256_json( + execution["online_transaction_receipt"] + ), + "runtime_snapshot_receipt_sha256": batch._sha256_json( + execution["runtime_snapshot_receipt"] + ), + "apply_receipt_sha256": apply_receipt_sha256, + "apply_receipt": apply_receipt, + } + headers = _headers( + key, + manifest_raw_sha256, + approval_raw_sha256, + legal_review_binding_sha256, + tenant_id, + ) + try: + response = client.request( + "POST", + f"/internal/rc06/publication-transactions/{transaction_id}/finalize", + payload=finalize_request, + headers={ + **headers, + "X-RC06-Request-SHA256": batch._sha256_json(finalize_request), + }, + ) + response_row = _require_exact( + response, + "online finalize response", + ( + "schema_version", + "idempotency_key", + "transaction_id", + "state", + "no_go", + "apply_receipt_sha256", + "finalized_at", + ), + ) + if ( + response_row["schema_version"] != FINALIZE_RESPONSE_SCHEMA_VERSION + or response_row["idempotency_key"] != key + or response_row["transaction_id"] != transaction_id + or response_row["state"] != "finalized" + or response_row["no_go"] is not False + or response_row["apply_receipt_sha256"] != apply_receipt_sha256 + ): + raise ApplyPublicationError("online finalize response identity drift") + finalized_at = batch._parse_time( + response_row["finalized_at"], "online finalized_at" + ) + if finalized_at < batch._parse_time( + apply_receipt["payload"]["countersigned_at"], "apply countersigned_at" + ): + raise ApplyPublicationError("online finalize predates the offline countersign") + status = client.request( + "GET", + f"/internal/rc06/publication-transactions/{transaction_id}/status", + payload=None, + headers=headers, + ) + status_row = _require_exact( + status, + "online finalize status", + ( + "schema_version", + "idempotency_key", + "transaction_id", + "state", + "no_go", + "transaction_mode", + "cross_database_atomicity", + "publication_item_count", + "runtime_snapshot_dependency_count", + "online_transaction_receipt_sha256", + "runtime_snapshot_receipt_sha256", + "apply_receipt_sha256", + "finalized_at", + ), + ) + if ( + status_row["schema_version"] != FINALIZE_STATUS_SCHEMA_VERSION + or status_row["idempotency_key"] != key + or status_row["transaction_id"] != transaction_id + or status_row["state"] != "finalized" + or status_row["no_go"] is not False + or status_row["transaction_mode"] != batch.TRANSACTION_MODE + or status_row["cross_database_atomicity"] is not False + or status_row["publication_item_count"] != 74 + or status_row["runtime_snapshot_dependency_count"] != 14 + or status_row["online_transaction_receipt_sha256"] + != finalize_request["online_transaction_receipt_sha256"] + or status_row["runtime_snapshot_receipt_sha256"] + != finalize_request["runtime_snapshot_receipt_sha256"] + or status_row["apply_receipt_sha256"] != apply_receipt_sha256 + or status_row["finalized_at"] != response_row["finalized_at"] + ): + raise ApplyPublicationError("online finalized status verification failed") + except Exception as exc: + failure = ( + exc + if isinstance(exc, ApplyPublicationError) + else ApplyPublicationError("online finalize request failed") + ) + ledger.state( + key, + "finalize_failed_no_go", + {"error": str(failure)}, + now, + receipt=execution, + error=str(failure), + ) + raise failure + final_evidence = { + "schema_version": FINAL_EVIDENCE_SCHEMA_VERSION, + "transaction_mode": batch.TRANSACTION_MODE, + "cross_database_atomicity": False, + "publication_item_count": 74, + "runtime_snapshot_dependency_count": 14, + "online_execution": execution, + "apply_receipt": apply_receipt, + "online_finalize_response": response_row, + "online_finalize_status": status_row, + } + ledger.state(key, "succeeded", final_evidence, now, receipt=final_evidence) + return final_evidence + + +# Backward-compatible import name with deliberately changed two-stage semantics: +# it executes online only and never fabricates an offline authority receipt. +apply_publication_batch = execute_online_publication_batch + + def _read_public_key(path: Path, expected_sha256: str, label: str) -> Ed25519PublicKey: return batch._read_pinned_raw_public_key( path, @@ -864,6 +1349,14 @@ def _read_public_key(path: Path, expected_sha256: str, label: str) -> Ed25519Pub ) +def _read_online_public_key(path: Path) -> Any: + return batch._read_pinned_spki_public_key( + path, + expected_sha256=batch.TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SPKI_SHA256, + label="online service public key", + ) + + def _outside_repo(path: Path, label: str) -> Path: resolved = path.resolve() try: @@ -873,8 +1366,13 @@ def _outside_repo(path: Path, label: str) -> Path: raise ApplyPublicationError(f"{label} must be stored outside the source worktree") -def _write_receipt(path: Path, receipt: Mapping[str, Any]) -> str: - raw = batch._canonical(receipt) + b"\n" +def _write_receipt( + path: Path, + receipt: Mapping[str, Any], + *, + canonical_only: bool = False, +) -> str: + raw = batch._canonical(receipt) + (b"" if canonical_only else b"\n") path.parent.mkdir(parents=True, exist_ok=True) if path.exists(): existing = path.read_bytes() @@ -898,8 +1396,7 @@ def _write_receipt(path: Path, receipt: Mapping[str, Any]) -> str: return _sha256(raw) -def main(argv: list[str] | None = None) -> int: - parser = argparse.ArgumentParser(description=__doc__) +def _add_common_arguments(parser: argparse.ArgumentParser) -> None: parser.add_argument("--manifest", type=Path, required=True) parser.add_argument("--manifest-sha256", required=True) parser.add_argument("--approval", type=Path, required=True) @@ -916,15 +1413,35 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("--legal-review-keyring", type=Path, required=True) parser.add_argument("--legal-review-keyring-sha256", required=True) parser.add_argument("--environment-id", required=True) - parser.add_argument("--service-url", required=True) + parser.add_argument("--tenant-id", required=True) parser.add_argument("--ledger", type=Path, required=True) parser.add_argument("--output", type=Path, required=True) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + commands = parser.add_subparsers(dest="command", required=True) + execute = commands.add_parser( + "execute-online", help="execute and verify the online durable saga" + ) + _add_common_arguments(execute) + execute.add_argument("--authority-challenge", type=Path, required=True) + execute.add_argument("--authority-challenge-sha256", required=True) + execute.add_argument("--service-url", required=True) + execute.add_argument("--online-receipt-output", type=Path, required=True) + execute.add_argument("--runtime-receipt-output", type=Path, required=True) + finalize = commands.add_parser( + "finalize", help="verify the offline countersign and finalize local evidence" + ) + _add_common_arguments(finalize) + finalize.add_argument("--online-execution", type=Path, required=True) + finalize.add_argument("--online-execution-sha256", required=True) + finalize.add_argument("--apply-receipt", type=Path, required=True) + finalize.add_argument("--apply-receipt-sha256", required=True) + finalize.add_argument("--service-url", required=True) args = parser.parse_args(argv) ledger: ApplyLedger | None = None try: - token = os.environ.get(AUTH_TOKEN_ENV, "") - if not token: - raise ApplyPublicationError(f"{AUTH_TOKEN_ENV} is not set") manifest, manifest_raw = batch.secure_read_json( args.manifest, "publication manifest", args.manifest_sha256 ) @@ -946,7 +1463,7 @@ def main(argv: list[str] | None = None) -> int: "scene legal review decision", args.legal_review_decision_sha256, ) - legal_keyring, _ = batch.secure_read_json( + legal_keyring, legal_keyring_raw = batch.secure_read_json( args.legal_review_keyring, "scene legal review keyring", args.legal_review_keyring_sha256, @@ -961,36 +1478,105 @@ def main(argv: list[str] | None = None) -> int: batch.TRUSTED_APPLY_SERVICE_PUBLIC_KEY_SHA256, "apply service public key", ) - online_key = _read_public_key( - args.online_service_public_key, - batch.TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SHA256, - "online service public key", - ) + online_key = _read_online_public_key(args.online_service_public_key) ledger_path = _outside_repo(args.ledger, "apply ledger") - output_path = _outside_repo(args.output, "apply receipt") + output_path = _outside_repo(args.output, "apply evidence") ledger = ApplyLedger(ledger_path) - receipt = apply_publication_batch( - manifest=manifest, - manifest_raw_sha256=manifest_raw, - approval=approval, - approval_raw_sha256=approval_raw, - approval_keyring=keyring, - approval_root_public_key=root_key, - apply_service_public_key=apply_key, - online_service_public_key=online_key, - legal_review_request=legal_request, - legal_review_decision=legal_decision, - legal_review_keyring=legal_keyring, - environment_id=args.environment_id, - client=JsonHttpClient(args.service_url, token), - ledger=ledger, - now=datetime.now(UTC), + current = datetime.now(UTC) + legal_summary, legal_binding = validate_scene_legal_review( + manifest, + approval, + legal_request, + legal_decision, + legal_keyring, + legal_keyring_raw, + now=current, ) - receipt_sha = _write_receipt(output_path, receipt) + if args.command == "execute-online": + token = os.environ.get(AUTH_TOKEN_ENV, "") + if not token: + raise ApplyPublicationError(f"{AUTH_TOKEN_ENV} is not set") + challenge, _ = batch.secure_read_json( + args.authority_challenge, + "apply authority challenge", + args.authority_challenge_sha256, + ) + evidence = execute_online_publication_batch( + manifest=manifest, + manifest_raw_sha256=manifest_raw, + approval=approval, + approval_raw_sha256=approval_raw, + approval_keyring=keyring, + approval_root_public_key=root_key, + apply_service_public_key=apply_key, + online_service_public_key=online_key, + legal_review_request=legal_request, + legal_review_decision=legal_decision, + legal_review_keyring=legal_keyring, + legal_review_keyring_raw_sha256=legal_keyring_raw, + authority_challenge=challenge, + environment_id=args.environment_id, + tenant_id=args.tenant_id, + client=JsonHttpClient(args.service_url, token), + ledger=ledger, + now=current, + ) + online_output = _outside_repo( + args.online_receipt_output, "online receipt" + ) + runtime_output = _outside_repo( + args.runtime_receipt_output, "runtime receipt" + ) + _write_receipt( + online_output, + evidence["online_transaction_receipt"], + canonical_only=True, + ) + _write_receipt( + runtime_output, + evidence["runtime_snapshot_receipt"], + canonical_only=True, + ) + status = "online_committed_pending_countersign" + else: + token = os.environ.get(AUTH_TOKEN_ENV, "") + if not token: + raise ApplyPublicationError(f"{AUTH_TOKEN_ENV} is not set") + online_execution, _ = batch.secure_read_json( + args.online_execution, + "online execution evidence", + args.online_execution_sha256, + ) + apply_receipt, _ = batch.secure_read_json( + args.apply_receipt, + "offline apply receipt", + args.apply_receipt_sha256, + ) + evidence = finalize_publication_batch( + manifest=manifest, + manifest_raw_sha256=manifest_raw, + approval=approval, + approval_raw_sha256=approval_raw, + approval_keyring=keyring, + approval_root_public_key=root_key, + apply_service_public_key=apply_key, + online_service_public_key=online_key, + legal_review_binding_sha256=legal_binding, + legal_review_expires_at=legal_summary["expires_at"], + environment_id=args.environment_id, + tenant_id=args.tenant_id, + online_execution=online_execution, + apply_receipt=apply_receipt, + client=JsonHttpClient(args.service_url, token), + ledger=ledger, + now=current, + ) + status = "finalized_with_offline_countersign" + receipt_sha = _write_receipt(output_path, evidence) print( json.dumps( { - "status": "applied_and_verified", + "status": status, "apply_evidence": str(output_path), "apply_evidence_raw_sha256": receipt_sha, "publication_item_count": 74, diff --git a/yuqei-infra/scripts/build-rc06-publication-batch.py b/yuqei-infra/scripts/build-rc06-publication-batch.py index cc1dec5c..30ec7d72 100644 --- a/yuqei-infra/scripts/build-rc06-publication-batch.py +++ b/yuqei-infra/scripts/build-rc06-publication-batch.py @@ -12,6 +12,7 @@ import argparse import base64 import csv import hashlib +import importlib.util import json import os import re @@ -20,12 +21,13 @@ import subprocess import sys import tempfile import types -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from pathlib import Path, PurePath from typing import Any, Iterable, Mapping from cryptography.exceptions import InvalidSignature -from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import ec, padding, rsa, utils from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey @@ -36,18 +38,22 @@ APPROVAL_SCHEMA_VERSION = "yuqei.rc06-publication-batch-approval.v5" APPROVAL_PAYLOAD_SCHEMA_VERSION = "yuqei.rc06-publication-batch-approval-payload.v4" APPROVAL_REVIEW_SUMMARY_SCHEMA_VERSION = "yuqei.rc06-single-reviewer-summary.v2" APPROVAL_KEYRING_SCHEMA_VERSION = "yuqei.rc06-publication-approval-keyring.v4" -APPLY_SCHEMA_VERSION = "yuqei.rc06-publication-system-item-apply-receipt.v5" -APPLY_PAYLOAD_SCHEMA_VERSION = "yuqei.rc06-publication-system-item-apply-payload.v3" -ONLINE_TRANSACTION_RECEIPT_SCHEMA_VERSION = "yuqei.rc06-online-transaction-receipt.v2" -ONLINE_TRANSACTION_PAYLOAD_SCHEMA_VERSION = "yuqei.rc06-online-transaction-payload.v2" -APPLY_CHALLENGE_SCHEMA_VERSION = "yuqei.rc06-apply-challenge.v1" -APPLY_CHALLENGE_PAYLOAD_SCHEMA_VERSION = "yuqei.rc06-apply-challenge-payload.v1" +APPLY_SCHEMA_VERSION = "yuqei.rc06-publication-system-item-apply-receipt.v6" +APPLY_PAYLOAD_SCHEMA_VERSION = "yuqei.rc06-publication-system-item-apply-payload.v4" +ONLINE_TRANSACTION_RECEIPT_SCHEMA_VERSION = "yuqei.rc06-online-transaction-receipt.v4" +ONLINE_TRANSACTION_PAYLOAD_SCHEMA_VERSION = "yuqei.rc06-online-transaction-payload.v4" +RUNTIME_RECEIPT_SCHEMA_VERSION = "yuqei.rc06-runtime-snapshot-receipt.v2" +RUNTIME_PAYLOAD_SCHEMA_VERSION = "yuqei.rc06-runtime-snapshot-payload.v2" +APPLY_CHALLENGE_SCHEMA_VERSION = "yuqei.rc06-apply-challenge.v2" +APPLY_CHALLENGE_PAYLOAD_SCHEMA_VERSION = "yuqei.rc06-apply-challenge-payload.v2" APPROVAL_SIGNATURE_DOMAIN = b"YUQEI_RC06_PUBLICATION_BATCH_APPROVAL_V1\0" APPROVAL_KEYRING_SIGNATURE_DOMAIN = b"YUQEI_RC06_APPROVAL_KEYRING_V1\0" -APPLY_SIGNATURE_DOMAIN = b"YUQEI_RC06_SYSTEM_APPLY_RECEIPT_V1\0" -ONLINE_TRANSACTION_SIGNATURE_DOMAIN = b"YUQEI_RC06_ONLINE_TRANSACTION_RECEIPT_V1\0" -APPLY_CHALLENGE_SIGNATURE_DOMAIN = b"YUQEI_RC06_APPLY_CHALLENGE_V1\0" +APPLY_SIGNATURE_DOMAIN = b"YUQEI_RC06_SYSTEM_APPLY_RECEIPT_V2\0" +ONLINE_TRANSACTION_SIGNATURE_DOMAIN = b"YUQEI_RC06_ONLINE_TRANSACTION_RECEIPT_V2\0" +RUNTIME_SIGNATURE_DOMAIN = b"YUQEI_RC06_RUNTIME_SNAPSHOT_RECEIPT_V2\0" +APPLY_CHALLENGE_SIGNATURE_DOMAIN = b"YUQEI_RC06_APPLY_CHALLENGE_V2\0" APPLY_OPERATION = "apply_rc06_publication_batch" +TRANSACTION_MODE = "durable_saga" MAX_APPLY_CHALLENGE_TTL = timedelta(minutes=10) TRUSTED_APPROVAL_ROOT_KEY_ID = "yuqei-rc06-approval-root-v1" # Provision the long-lived root/apply pins and exact candidate keyring epoch @@ -60,13 +66,32 @@ TRUSTED_APPLY_SERVICE_PUBLIC_KEY_SHA256 = "" # These values must be provisioned from the real publication service before # source freeze. Empty defaults deliberately make every formal apply receipt # fail closed; a caller-provided environment name or key cannot override them. -TRUSTED_ONLINE_APPLY_SERVICE_KEY_ID = "yuqei-rc06-online-publication-service-v1" -TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SHA256 = "" +TRUSTED_ONLINE_APPLY_SERVICE_PROVIDER = "" +TRUSTED_ONLINE_APPLY_SERVICE_KEY_REFERENCE = "" +TRUSTED_ONLINE_APPLY_SERVICE_KEY_VERSION = "" +TRUSTED_ONLINE_APPLY_SERVICE_ALGORITHM = "" +TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SPKI_SHA256 = "" TRUSTED_ONLINE_APPLY_SERVICE_COMPONENT = "rc06-publication-service" TRUSTED_ONLINE_APPLY_ENVIRONMENT_IDS: tuple[str, ...] = () +# Pin the exact canonical legal-review keyring bytes before candidate freeze. +# Empty deliberately rejects caller-supplied/self-signed reviewer keyrings. +TRUSTED_SCENE_LEGAL_REVIEW_KEYRING_SHA256 = "" RC05_SUMMARY_SCHEMA_VERSION = "multiscene-evaluation-run.v1" -RC05_LEDGER_SCHEMA_VERSION = "multiscene-call-ledger.v1" -RC05_CAMPAIGN_SCHEMA_VERSION = "multiscene-call-campaign.v1" +RC05_PLAN_SCHEMA_VERSION = "multiscene-evaluation-plan.v2" +RC05_CANDIDATE_SCHEMA_VERSION = "multiscene-candidate-binding.v1" +RC05_RELEASE_CONTROLS_SCHEMA_VERSION = "multiscene-release-controls.v1" +RC05_LEDGER_SCHEMA_VERSION = "multiscene-call-ledger.v2" +RC05_LEDGER_SNAPSHOT_SCHEMA_VERSION = "multiscene-call-ledger-snapshot.v2" +RC05_CAMPAIGN_SCHEMA_VERSION = "multiscene-call-campaign.v2" +RC05_CONTROL_STATE_SCHEMA_VERSION = "multiscene-release-control-state.v1" +EXPECTED_RC05_CANARY_CASE_IDS = ( + "base--TPL-AGENCY-DISTRIBUTION-001--normal", + "base--TPL-DEMAND-LETTER-001--normal", + "base--TPL-PURCHASE-GOODS-001--normal", + "scene--TPL-AMENDMENT-GENERAL-001--opposite-adversarial", + "specialist-trade_procurement-first", + "intake-tpl-purchase-goods-001", +) # Each tuple entry is missing evidence, not a request for another human reviewer. # Validation removes the first two entries as the single-reviewer approval and @@ -1156,11 +1181,165 @@ def validate_image_identities(payload: Any, commit: str, tag: str) -> dict[str, } -def validate_rc05_plan(payload: Any) -> dict[str, Any]: - if not isinstance(payload, dict): - raise PublicationBatchError("RC-05 plan must be an object") - if payload.get("schema_version") != "multiscene-evaluation-plan.v1": - raise PublicationBatchError("unsupported RC-05 plan schema") +def _validate_rc05_candidate_binding(value: Any) -> dict[str, Any]: + candidate = _require_keys( + value, + "RC-05 candidate binding", + ("schema_version", "commit", "tag", "source_manifest", "image_identities"), + ) + if ( + candidate["schema_version"] != RC05_CANDIDATE_SCHEMA_VERSION + or not COMMIT_RE.fullmatch(str(candidate["commit"])) + or not TAG_RE.fullmatch(str(candidate["tag"])) + or ".." in candidate["tag"] + or "//" in candidate["tag"] + ): + raise PublicationBatchError("RC-05 candidate binding identity is invalid") + source = _require_keys( + candidate["source_manifest"], + "RC-05 candidate source manifest binding", + ("schema_version", "raw_sha256", "payload_sha256"), + ) + if source["schema_version"] != "yuqei.release-source-manifest.v1": + raise PublicationBatchError("RC-05 candidate source manifest schema is invalid") + _require_sha(source["raw_sha256"], "RC-05 candidate source manifest raw SHA-256") + _require_sha( + source["payload_sha256"], "RC-05 candidate source manifest payload SHA-256" + ) + image_binding = _require_keys( + candidate["image_identities"], + "RC-05 candidate image binding", + ("schema_version", "raw_sha256", "identity_set_sha256", "images"), + ) + if image_binding["schema_version"] not in { + IMAGE_SCHEMA_VERSION, + "yuqei.installable-release-manifest.v1", + }: + raise PublicationBatchError("RC-05 candidate image binding schema is invalid") + _require_sha(image_binding["raw_sha256"], "RC-05 candidate image raw SHA-256") + identity_set_sha = _require_sha( + image_binding["identity_set_sha256"], "RC-05 candidate image set SHA-256" + ) + images = image_binding["images"] + if not isinstance(images, list) or len(images) != len(REQUIRED_IMAGE_ROLES): + raise PublicationBatchError("RC-05 candidate must bind all four release images") + normalized: list[dict[str, Any]] = [] + roles: set[str] = set() + manifest_digests: set[str] = set() + for index, value_row in enumerate(images): + row = _require_keys( + value_row, + f"RC-05 candidate image {index}", + ("role", "repository", "manifest_digest", "config_digest"), + ) + role = row["role"] + repository = _require_string( + row["repository"], f"RC-05 candidate image {index} repository", max_length=256 + ) + if role not in REQUIRED_IMAGE_ROLES or role in roles: + raise PublicationBatchError("RC-05 candidate image role inventory is invalid") + manifest_digest = row["manifest_digest"] + config_digest = row["config_digest"] + if ( + not isinstance(manifest_digest, str) + or not manifest_digest.startswith("sha256:") + or not isinstance(config_digest, str) + or not config_digest.startswith("sha256:") + ): + raise PublicationBatchError("RC-05 candidate image digest is invalid") + _require_sha( + manifest_digest.removeprefix("sha256:"), + f"RC-05 candidate image {role} manifest digest", + ) + _require_sha( + config_digest.removeprefix("sha256:"), + f"RC-05 candidate image {role} config digest", + ) + if manifest_digest in manifest_digests: + raise PublicationBatchError("RC-05 candidate image manifest digests must be unique") + roles.add(role) + manifest_digests.add(manifest_digest) + normalized.append( + { + "role": role, + "repository": repository, + "manifest_digest": manifest_digest, + "config_digest": config_digest, + } + ) + normalized.sort(key=lambda item: REQUIRED_IMAGE_ROLES.index(item["role"])) + if images != normalized or roles != set(REQUIRED_IMAGE_ROLES): + raise PublicationBatchError("RC-05 candidate images are not canonically ordered") + if _sha256_json(normalized) != identity_set_sha: + raise PublicationBatchError("RC-05 candidate image identity-set digest drift") + return dict(candidate) + + +def _validate_rc05_release_controls( + value: Any, *, cases: list[dict[str, Any]] +) -> dict[str, Any]: + controls = _require_keys( + value, + "RC-05 release controls", + ("schema_version", "canary_case_ids", "failure_classes"), + ) + canaries = controls["canary_case_ids"] + failure_classes = controls["failure_classes"] + case_ids = [case["case_id"] for case in cases] + if ( + controls["schema_version"] != RC05_RELEASE_CONTROLS_SCHEMA_VERSION + or canaries != list(EXPECTED_RC05_CANARY_CASE_IDS) + or not isinstance(failure_classes, dict) + or set(failure_classes) != set(case_ids) + ): + raise PublicationBatchError("RC-05 release controls identity or coverage drift") + for case in cases: + failure_class = _require_string( + case.get("failure_class"), + f"RC-05 {case['case_id']} failure class", + max_length=128, + ) + if ( + not STABLE_ID_RE.fullmatch(failure_class) + or failure_classes[case["case_id"]] != failure_class + ): + raise PublicationBatchError("RC-05 failure-class circuit binding drift") + return dict(controls) + + +def validate_rc05_plan( + payload: Any, *, expected_candidate: Mapping[str, Any] | None = None +) -> dict[str, Any]: + plan = _require_keys( + payload, + "RC-05 plan", + ( + "schema_version", + "prepared_at", + "model", + "reasoning_effort", + "timeout_seconds", + "case_count", + "drafting_case_count", + "supplemental_case_count", + "operation_limits", + "total_provider_call_budget", + "candidate", + "release_controls", + "source_pins", + "cases", + "limitations", + ), + ) + if plan["schema_version"] != RC05_PLAN_SCHEMA_VERSION: + raise PublicationBatchError( + "legacy or unsupported RC-05 plan schema is read-only diagnostic material" + ) + _parse_time(plan["prepared_at"], "RC-05 plan prepared_at") + candidate_binding = _validate_rc05_candidate_binding(plan["candidate"]) + if expected_candidate is not None and candidate_binding != dict(expected_candidate): + raise PublicationBatchError("RC-05 plan candidate binding differs from the frozen candidate") + payload = plan if payload.get("model") != "gpt-5.6-sol" or payload.get("reasoning_effort") != "medium": raise PublicationBatchError("RC-05 plan model identity is not the frozen release configuration") if payload.get("timeout_seconds") != 300 or payload.get("case_count") != 144: @@ -1261,6 +1440,9 @@ def validate_rc05_plan(payload: Any) -> dict[str, Any]: or total_case_budget != 280 ): raise PublicationBatchError("RC-05 case operations or provider budgets do not reconcile") + release_controls = _validate_rc05_release_controls( + payload["release_controls"], cases=cases + ) source_pins = payload.get("source_pins") if not isinstance(source_pins, dict) or not source_pins: raise PublicationBatchError("RC-05 plan source pins are missing") @@ -1286,6 +1468,8 @@ def validate_rc05_plan(payload: Any) -> dict[str, Any]: "total_provider_call_budget": payload["total_provider_call_budget"], "case_inventory_sha256": _sha256_json(cases), "source_pins_sha256": _sha256_json(source_pins), + "candidate": candidate_binding, + "release_controls": release_controls, } @@ -1325,10 +1509,12 @@ def validate_rc05_plan_identity(value: Any) -> dict[str, Any]: "total_provider_call_budget", "case_inventory_sha256", "source_pins_sha256", + "candidate", + "release_controls", ), ) if ( - identity["schema_version"] != "multiscene-evaluation-plan.v1" + identity["schema_version"] != RC05_PLAN_SCHEMA_VERSION or identity["model"] != "gpt-5.6-sol" or identity["reasoning_effort"] != "medium" or identity["timeout_seconds"] != 300 @@ -1341,6 +1527,26 @@ def validate_rc05_plan_identity(value: Any) -> dict[str, Any]: raise PublicationBatchError("RC-05 plan identity drift") _require_sha(identity["case_inventory_sha256"], "RC-05 case inventory SHA-256") _require_sha(identity["source_pins_sha256"], "RC-05 source pins SHA-256") + _validate_rc05_candidate_binding(identity["candidate"]) + controls = _require_keys( + identity["release_controls"], + "RC-05 plan identity release controls", + ("schema_version", "canary_case_ids", "failure_classes"), + ) + if ( + controls["schema_version"] != RC05_RELEASE_CONTROLS_SCHEMA_VERSION + or controls["canary_case_ids"] != list(EXPECTED_RC05_CANARY_CASE_IDS) + or not isinstance(controls["failure_classes"], dict) + or len(controls["failure_classes"]) != 144 + or any( + not isinstance(key, str) + or not STABLE_ID_RE.fullmatch(key) + or not isinstance(value, str) + or not STABLE_ID_RE.fullmatch(value) + for key, value in controls["failure_classes"].items() + ) + ): + raise PublicationBatchError("RC-05 plan identity release controls drift") return dict(identity) @@ -1441,7 +1647,7 @@ def validate_rc05_terminal_evidence( "RC-05 ledger identity", ( "schema_version", "plan_sha256", "operation_limits", "case_limits", - "source_pins", "real_provider", "authority", + "source_pins", "real_provider", "candidate_binding", "release_controls", "authority", "integrity_not_authentication", "historical_usage", ), ) @@ -1450,6 +1656,8 @@ def validate_rc05_terminal_evidence( or ledger["plan_sha256"] != plan_sha or ledger["operation_limits"] != plan["operation_limits"] or ledger["source_pins"] != dict(sorted(plan["source_pins"].items())) + or ledger["candidate_binding"] != plan["candidate"] + or ledger["release_controls"] != plan["release_controls"] or ledger["real_provider"] is not True or ledger["integrity_not_authentication"] is not True ): @@ -1575,7 +1783,7 @@ def validate_rc05_terminal_evidence( ) if ( campaign["schema_version"] != RC05_CAMPAIGN_SCHEMA_VERSION - or campaign["campaign_id"] != "rc05-real-usage-v1" + or campaign["campaign_id"] != "rc05-real-usage-v2" or campaign["historical_receipt_sha256"] != historical_receipt_sha or campaign["historical_attempts_sha256"] != historical_attempts_sha or campaign["historical_provider_call_count"] != historical_count @@ -1742,7 +1950,7 @@ def validate_rc05_terminal_evidence( if not isinstance(snapshot, dict): raise PublicationBatchError("RC-05 terminal ledger snapshot is missing") if ( - snapshot.get("schema_version") != "multiscene-call-ledger-snapshot.v1" + snapshot.get("schema_version") != RC05_LEDGER_SNAPSHOT_SCHEMA_VERSION or snapshot.get("ledger_identity_sha256") != ledger_identity_sha256 or snapshot.get("real_provider") is not True or snapshot.get("authority") != authority @@ -1750,6 +1958,28 @@ def validate_rc05_terminal_evidence( or snapshot.get("terminal") != snapshot.get("attempted") ): raise PublicationBatchError("RC-05 terminal ledger snapshot identity or terminal counts drift") + control_state = _require_keys( + snapshot.get("release_controls"), + "RC-05 terminal release-control state", + ( + "schema_version", + "phase", + "canary_case_ids", + "blocked_failure_classes", + "canary_transition_sha256", + ), + ) + if ( + control_state["schema_version"] != RC05_CONTROL_STATE_SCHEMA_VERSION + or control_state["phase"] != "full_authorized" + or control_state["canary_case_ids"] != list(EXPECTED_RC05_CANARY_CASE_IDS) + or control_state["blocked_failure_classes"] != [] + ): + raise PublicationBatchError("RC-05 terminal release controls are not green") + _require_sha( + control_state["canary_transition_sha256"], + "RC-05 canary transition SHA-256", + ) case_snapshot = snapshot.get("cases") operation_snapshot = snapshot.get("operations") if not isinstance(case_snapshot, dict) or set(case_snapshot) != set(plan_cases): @@ -2889,6 +3119,124 @@ def _public_key_raw(public_key: Ed25519PublicKey) -> bytes: ) +def _public_key_spki(public_key: Any) -> bytes: + try: + return public_key.public_bytes( + serialization.Encoding.DER, + serialization.PublicFormat.SubjectPublicKeyInfo, + ) + except (AttributeError, TypeError, ValueError) as exc: + raise PublicationBatchError("online signing public key is invalid") from exc + + +def _read_pinned_spki_public_key(path: Path, *, expected_sha256: str, label: str) -> Any: + if not HEX64_RE.fullmatch(expected_sha256): + raise PublicationBatchError(f"{label} SPKI pin is not provisioned in the frozen source") + raw = secure_read_bytes(path, label, maximum_bytes=64 * 1024) + try: + public_key = serialization.load_pem_public_key(raw) + except (TypeError, ValueError) as exc: + raise PublicationBatchError(f"{label} is not a valid PEM public key") from exc + if not isinstance(public_key, (rsa.RSAPublicKey, ec.EllipticCurvePublicKey)): + raise PublicationBatchError(f"{label} must be an RSA or EC KMS signing key") + if _sha256_bytes(_public_key_spki(public_key)) != expected_sha256: + raise PublicationBatchError(f"{label} does not match the frozen SPKI trust anchor") + return public_key + + +def _verify_online_kms_signature( + public_key: Any, + signature_block: Any, + *, + domain: bytes, + payload: Mapping[str, Any], + label: str, +) -> dict[str, Any]: + """Verify one KMS digest signature against exact frozen signer metadata.""" + + signature = _require_keys( + signature_block, + f"{label} signature", + ( + "provider", + "key_reference", + "key_version", + "algorithm", + "value_base64", + "request_id", + ), + ) + required_trust = ( + TRUSTED_ONLINE_APPLY_SERVICE_PROVIDER, + TRUSTED_ONLINE_APPLY_SERVICE_KEY_REFERENCE, + TRUSTED_ONLINE_APPLY_SERVICE_KEY_VERSION, + TRUSTED_ONLINE_APPLY_SERVICE_ALGORITHM, + TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SPKI_SHA256, + ) + if ( + not all(isinstance(value, str) and value for value in required_trust) + or not HEX64_RE.fullmatch( + TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SPKI_SHA256 + ) + ): + raise PublicationBatchError("online KMS signer trust is not fully provisioned") + if ( + signature["provider"] != TRUSTED_ONLINE_APPLY_SERVICE_PROVIDER + or signature["key_reference"] + != TRUSTED_ONLINE_APPLY_SERVICE_KEY_REFERENCE + or signature["key_version"] != TRUSTED_ONLINE_APPLY_SERVICE_KEY_VERSION + or signature["algorithm"] != TRUSTED_ONLINE_APPLY_SERVICE_ALGORITHM + ): + raise PublicationBatchError(f"{label} KMS signer identity drift") + _require_string(signature["request_id"], f"{label} KMS request_id", max_length=512) + if ( + _sha256_bytes(_public_key_spki(public_key)) + != TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SPKI_SHA256 + ): + raise PublicationBatchError(f"{label} signer does not match the frozen SPKI pin") + try: + signature_bytes = base64.b64decode( + _require_string( + signature["value_base64"], f"{label} signature value", max_length=16384 + ), + validate=True, + ) + digest = hashlib.sha256(domain + _canonical(payload)).digest() + algorithm = signature["algorithm"] + if algorithm == "RSA_PSS_SHA_256" and isinstance(public_key, rsa.RSAPublicKey): + public_key.verify( + signature_bytes, + digest, + padding.PSS( + mgf=padding.MGF1(hashes.SHA256()), + salt_length=hashes.SHA256().digest_size, + ), + utils.Prehashed(hashes.SHA256()), + ) + elif algorithm == "RSA_PKCS1_SHA_256" and isinstance( + public_key, rsa.RSAPublicKey + ): + public_key.verify( + signature_bytes, + digest, + padding.PKCS1v15(), + utils.Prehashed(hashes.SHA256()), + ) + elif algorithm == "ECDSA_SHA_256" and isinstance( + public_key, ec.EllipticCurvePublicKey + ): + public_key.verify( + signature_bytes, + digest, + ec.ECDSA(utils.Prehashed(hashes.SHA256())), + ) + else: + raise PublicationBatchError(f"{label} KMS algorithm/key type is unsupported") + except (TypeError, ValueError, InvalidSignature) as exc: + raise PublicationBatchError(f"{label} KMS signature verification failed") from exc + return signature + + def _read_pinned_raw_public_key(path: Path, *, expected_sha256: str, label: str) -> Ed25519PublicKey: if not HEX64_RE.fullmatch(expected_sha256): raise PublicationBatchError(f"{label} pin is not provisioned in the frozen source") @@ -3171,12 +3519,362 @@ def _expected_apply_item_results(manifest: Mapping[str, Any]) -> list[dict[str, return results +def expected_runtime_snapshot_results(manifest: Mapping[str, Any]) -> list[dict[str, Any]]: + """Project the fourteen executable Prompt identities into read-back evidence.""" + + dependencies = manifest["runtime_prompt_dependencies"] + if dependencies.get("publication_item_count") != 74 or dependencies.get("count") != 14: + raise PublicationBatchError( + "manifest must distinguish 74 publication items from 14 runtime snapshots" + ) + results = [ + { + "kind": item["kind"], + "runtime_key": item["runtime_key"], + "version": item["version"], + "expected_content_sha256": item["content_sha256"], + "online_content_sha256": item["content_sha256"], + "status": "published_and_resolved", + "publication_dependency": item["publication_dependency"], + "publication_module_dependencies": item["publication_module_dependencies"], + } + for item in dependencies["items"] + ] + if len(results) != EXPECTED_RUNTIME_PROMPT_COUNT or len( + {item["runtime_key"] for item in results} + ) != EXPECTED_RUNTIME_PROMPT_COUNT: + raise PublicationBatchError("runtime snapshot inventory must contain 14 identities") + return results + + +def publication_idempotency_key( + manifest_raw_sha256: str, + approval_raw_sha256: str, + legal_review_binding_sha256: str, + environment_id: str, + tenant_id: str, +) -> str: + identity = { + "operation": APPLY_OPERATION, + "manifest_raw_sha256": _require_sha( + manifest_raw_sha256, "publication manifest raw SHA-256" + ), + "approval_raw_sha256": _require_sha( + approval_raw_sha256, "publication approval raw SHA-256" + ), + "legal_review_binding_sha256": _require_sha( + legal_review_binding_sha256, "legal review binding SHA-256" + ), + "environment_id": _require_string( + environment_id, "publication environment_id", max_length=256 + ), + "tenant_id": _require_string(tenant_id, "publication tenant_id", max_length=64), + } + return _sha256_json(identity) + + +def publication_transaction_id(idempotency_key: str) -> str: + """Return a stable transaction identifier that fits the 64-character DB contract.""" + + normalized = _require_sha(idempotency_key, "publication idempotency key") + return "rc06-" + normalized[:59] + + +def validate_scene_legal_review_keyring( + keyring: Any, + keyring_raw_sha256: str, + *, + expected_sha256: str | None = None, +) -> dict[str, Any]: + """Require the exact frozen canonical legal-review trust anchor. + + The legal decision verifier checks the selected key's identity and validity. + This additional check prevents a caller from supplying an arbitrary self-signed + keyring to that verifier. + """ + + trust_pin = ( + TRUSTED_SCENE_LEGAL_REVIEW_KEYRING_SHA256 + if expected_sha256 is None + else expected_sha256 + ) + if not HEX64_RE.fullmatch(trust_pin): + raise PublicationBatchError("scene legal review keyring trust is not provisioned") + normalized_raw_sha = _require_sha( + keyring_raw_sha256, "scene legal review keyring raw SHA-256" + ) + if ( + normalized_raw_sha != trust_pin + or _sha256_json(keyring) != trust_pin + ): + raise PublicationBatchError( + "scene legal review keyring does not match the frozen canonical pin" + ) + row = _require_keys( + keyring, + "scene legal review keyring", + ("schema_version", "keys"), + ) + if row["schema_version"] != "yuqei.scene-source-legal-review-keyring.v1": + raise PublicationBatchError("scene legal review keyring schema is unsupported") + keys = row["keys"] + if not isinstance(keys, list) or not keys: + raise PublicationBatchError("scene legal review keyring must contain trusted keys") + seen: set[str] = set() + for index, value in enumerate(keys): + key = _require_keys( + value, + f"scene legal review key {index}", + ( + "key_id", + "status", + "revoked_at", + "valid_from", + "valid_until", + "reviewer_name", + "reviewer_organization", + "reviewer_qualification", + "credential_reference", + "public_key_raw_base64", + ), + ) + key_id = _require_string(key["key_id"], "scene legal review key_id", max_length=128) + if key_id in seen: + raise PublicationBatchError("scene legal review keyring contains duplicate key ids") + seen.add(key_id) + if key["status"] != "trusted" or key["revoked_at"] is not None: + raise PublicationBatchError("scene legal review keyring contains an inactive key") + valid_from = _parse_time(key["valid_from"], "scene legal review key valid_from") + valid_until = _parse_time(key["valid_until"], "scene legal review key valid_until") + if valid_until <= valid_from: + raise PublicationBatchError("scene legal review key validity window is invalid") + for field in ( + "reviewer_name", + "reviewer_organization", + "reviewer_qualification", + "credential_reference", + ): + _require_string(key[field], f"scene legal review key {field}", max_length=512) + try: + public_raw = base64.b64decode( + _require_string( + key["public_key_raw_base64"], + "scene legal review public key", + max_length=128, + ), + validate=True, + ) + Ed25519PublicKey.from_public_bytes(public_raw) + except (TypeError, ValueError) as exc: + raise PublicationBatchError("scene legal review public key is invalid") from exc + return row + + +def _load_scene_legal_review_module() -> Any: + path = Path(__file__).resolve().with_name("build-scene-source-legal-review.py") + if not path.is_file(): + raise PublicationBatchError("scene legal review validator is absent") + spec = importlib.util.spec_from_file_location("rc06_scene_source_legal_review", path) + if spec is None or spec.loader is None: + raise PublicationBatchError("cannot load scene legal review validator") + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +def validate_scene_legal_review_evidence( + repo: Path, + manifest: Mapping[str, Any], + approval: Mapping[str, Any], + request: Any, + decision: Any, + keyring: Any, + keyring_raw_sha256: str, + *, + now: datetime, + validator: Any | None = None, +) -> tuple[dict[str, Any], str]: + """Validate the qualified-human 30-scene/43-occurrence proof and trust pin.""" + + if not isinstance(request, dict) or not isinstance(decision, dict): + raise PublicationBatchError("scene legal review request and decision are required") + validate_scene_legal_review_keyring(keyring, keyring_raw_sha256) + if request.get("candidate_commit") != manifest["candidate"]["commit"]: + raise PublicationBatchError("scene legal review candidate does not match the batch") + scope = request.get("scope") + if not isinstance(scope, dict) or scope.get("scene_count") != 30 or scope.get( + "reference_occurrence_count" + ) != 43: + raise PublicationBatchError("scene legal review must cover 30 scenes and 43 references") + request_sha256 = request.get("request_sha256") + if not isinstance(request_sha256, str) or not HEX64_RE.fullmatch(request_sha256): + raise PublicationBatchError("scene legal review request_sha256 is invalid") + if decision.get("request_sha256") != request_sha256: + raise PublicationBatchError("scene legal review decision is not bound to its request") + verifier = validator or _load_scene_legal_review_module() + try: + verified_request = verifier.verify_request(request, repo) + summary = verifier.verify_signed_decision( + verified_request, + decision, + keyring, + checked_at=now.astimezone(UTC).isoformat(timespec="seconds"), + ) + except Exception as exc: + raise PublicationBatchError(f"scene legal review validation failed: {exc}") from exc + if ( + summary.get("status") != "verified_qualified_human_review" + or summary.get("request_sha256") != request_sha256 + or summary.get("reviewed_occurrence_count") != 43 + or summary.get("all_current_effectiveness_verified") is not True + or summary.get("all_transaction_applicability_verified") is not True + or summary.get("authorizes_publication_by_itself") is not False + ): + raise PublicationBatchError("scene legal review verification summary is incomplete") + approval_expires_at = _parse_time(approval["payload"]["expires_at"], "approval expires_at") + legal_expires_at = _parse_time(summary.get("expires_at"), "legal review expires_at") + if legal_expires_at > approval_expires_at: + raise PublicationBatchError("scene legal review validity exceeds the approval window") + binding = { + "request_sha256": request_sha256, + "decision_sha256": _sha256_json(decision), + "keyring_raw_sha256": keyring_raw_sha256, + } + return summary, _sha256_json(binding) + + +def validate_scene_legal_review_evidence_online( + manifest: Mapping[str, Any], + approval: Mapping[str, Any], + request: Any, + decision: Any, + keyring: Any, + keyring_raw_sha256: str, + *, + now: datetime, + validator: Any | None = None, +) -> tuple[dict[str, Any], str]: + """Verify frozen legal evidence without Git/worktree access. + + The offline authority must still call ``validate_scene_legal_review_evidence`` + so the request is rebuilt from the immutable Git candidate. This narrower + verifier is for the production facade, whose image has no ``.git``: it + verifies the request's embedded hash/30-by-43 structure, the frozen keyring, + and the qualified-reviewer signature and validity window. + """ + + validate_scene_legal_review_keyring(keyring, keyring_raw_sha256) + row = _require_keys( + request, + "scene legal review request", + ( + "schema_version", + "status", + "candidate_commit", + "created_at", + "scope", + "source_files", + "occurrences", + "guardrails", + "request_sha256", + ), + ) + request_sha256 = _require_sha( + row["request_sha256"], "scene legal review request_sha256" + ) + if _sha256_json({key: value for key, value in row.items() if key != "request_sha256"}) != request_sha256: + raise PublicationBatchError("scene legal review request embedded hash drift") + if ( + row["schema_version"] != "yuqei.scene-source-legal-review-request.v1" + or row["status"] != "pending_qualified_human_review" + or row["candidate_commit"] != manifest["candidate"]["commit"] + ): + raise PublicationBatchError("scene legal review request identity drift") + _parse_time(row["created_at"], "scene legal review request created_at") + scope = _require_keys( + row["scope"], + "scene legal review scope", + ( + "scene_count", + "reference_occurrence_count", + "distinct_source_record_count", + "distinct_url_count", + ), + ) + if scope["scene_count"] != 30 or scope["reference_occurrence_count"] != 43: + raise PublicationBatchError("scene legal review must cover 30 scenes and 43 references") + occurrences = row["occurrences"] + if not isinstance(occurrences, list) or len(occurrences) != 43: + raise PublicationBatchError("scene legal review occurrence inventory is incomplete") + occurrence_ids: list[str] = [] + for index, occurrence in enumerate(occurrences): + if not isinstance(occurrence, dict): + raise PublicationBatchError(f"scene legal review occurrence {index} is invalid") + occurrence_id = _require_string( + occurrence.get("occurrence_id"), + f"scene legal review occurrence {index} id", + max_length=512, + ) + _require_sha( + occurrence.get("source_record_sha256"), + f"scene legal review occurrence {index} source record SHA-256", + ) + occurrence_ids.append(occurrence_id) + if occurrence_ids != sorted(occurrence_ids) or len(set(occurrence_ids)) != 43: + raise PublicationBatchError("scene legal review occurrence identities drift") + if row["guardrails"] != { + "not_legal_approval": True, + "authorizes_publication": False, + "ai_must_not_supply_human_decisions": True, + "all_occurrences_require_qualified_human_review": True, + }: + raise PublicationBatchError("scene legal review guardrails drift") + if not isinstance(row["source_files"], list) or not row["source_files"]: + raise PublicationBatchError("scene legal review source file inventory is empty") + verifier = validator or _load_scene_legal_review_module() + try: + summary = verifier.verify_signed_decision( + row, + decision, + keyring, + checked_at=now.astimezone(UTC).isoformat(timespec="seconds"), + ) + except Exception as exc: + raise PublicationBatchError(f"scene legal review validation failed: {exc}") from exc + if ( + summary.get("status") != "verified_qualified_human_review" + or summary.get("request_sha256") != request_sha256 + or summary.get("reviewed_occurrence_count") != 43 + or summary.get("all_current_effectiveness_verified") is not True + or summary.get("all_transaction_applicability_verified") is not True + or summary.get("authorizes_publication_by_itself") is not False + ): + raise PublicationBatchError("scene legal review verification summary is incomplete") + legal_expires_at = _parse_time(summary.get("expires_at"), "legal review expires_at") + if legal_expires_at > _parse_time( + approval["payload"]["expires_at"], "approval expires_at" + ): + raise PublicationBatchError("scene legal review validity exceeds the approval window") + binding = { + "request_sha256": request_sha256, + "decision_sha256": _sha256_json(decision), + "keyring_raw_sha256": keyring_raw_sha256, + } + return summary, _sha256_json(binding) + + def validate_apply_challenge( manifest: Mapping[str, Any], manifest_raw_sha256: str, approval_raw_sha256: str, apply_service_public_key: Ed25519PublicKey, challenge: Any, + *, + legal_review_binding_sha256: str, + legal_review_expires_at: str, + environment_id: str, + tenant_id: str, ) -> dict[str, Any]: if not HEX64_RE.fullmatch(TRUSTED_APPLY_SERVICE_PUBLIC_KEY_SHA256): raise PublicationBatchError("apply service public key pin is not provisioned") @@ -3201,8 +3899,13 @@ def validate_apply_challenge( "batch_manifest_raw_sha256", "batch_manifest_payload_sha256", "approval_receipt_sha256", + "legal_source_review_binding_sha256", + "legal_source_review_expires_at", "operation", "environment_id", + "tenant_id", + "idempotency_key", + "transaction_id", "actor", "issued_at", "expires_at", @@ -3216,6 +3919,12 @@ def validate_apply_challenge( != manifest["manifest_payload_sha256"] or payload["approval_receipt_sha256"] != _require_sha(approval_raw_sha256, "approval raw SHA-256") + or payload["legal_source_review_binding_sha256"] + != _require_sha( + legal_review_binding_sha256, + "expected legal review binding SHA-256", + ) + or payload["legal_source_review_expires_at"] != legal_review_expires_at or payload["operation"] != APPLY_OPERATION ): raise PublicationBatchError("apply challenge candidate or operation binding drift") @@ -3223,11 +3932,36 @@ def validate_apply_challenge( manifest_raw_sha256, "apply challenge manifest raw SHA-256" ): raise PublicationBatchError("apply challenge manifest raw binding drift") - environment_id = _require_string( + normalized_environment_id = _require_string( payload["environment_id"], "apply challenge environment_id", max_length=256 ) - if environment_id != payload["environment_id"]: + normalized_expected_environment = _require_string( + environment_id, "expected apply challenge environment_id", max_length=256 + ) + normalized_tenant_id = _require_string( + payload["tenant_id"], "apply challenge tenant_id", max_length=64 + ) + normalized_expected_tenant = _require_string( + tenant_id, "expected apply challenge tenant_id", max_length=64 + ) + if ( + normalized_environment_id != payload["environment_id"] + or normalized_environment_id != normalized_expected_environment + or normalized_tenant_id != payload["tenant_id"] + or normalized_tenant_id != normalized_expected_tenant + ): raise PublicationBatchError("apply challenge environment_id must be canonical") + expected_idempotency_key = publication_idempotency_key( + manifest_raw_sha256, + approval_raw_sha256, + legal_review_binding_sha256, + normalized_environment_id, + normalized_tenant_id, + ) + if payload["idempotency_key"] != expected_idempotency_key: + raise PublicationBatchError("apply challenge idempotency binding drift") + if payload["transaction_id"] != publication_transaction_id(expected_idempotency_key): + raise PublicationBatchError("apply challenge transaction binding drift") actor = _require_keys(payload["actor"], "apply challenge actor", ("type", "component")) if actor != { "type": "system", @@ -3236,9 +3970,14 @@ def validate_apply_challenge( raise PublicationBatchError("apply challenge actor is not the trusted online service") issued_at = _parse_time(payload["issued_at"], "apply challenge issued_at") expires_at = _parse_time(payload["expires_at"], "apply challenge expires_at") + legal_expires_at = _parse_time( + payload["legal_source_review_expires_at"], + "apply challenge legal review expires_at", + ) if ( expires_at <= issued_at or expires_at - issued_at > MAX_APPLY_CHALLENGE_TTL + or expires_at > legal_expires_at ): raise PublicationBatchError("apply challenge TTL is invalid") try: @@ -3278,8 +4017,13 @@ def validate_online_transaction_receipt( approval_raw_sha256: str, approval: Mapping[str, Any], apply_service_public_key: Ed25519PublicKey, - online_service_public_key: Ed25519PublicKey, + online_service_public_key: Any, receipt: Any, + *, + legal_review_binding_sha256: str, + legal_review_expires_at: str, + environment_id: str, + tenant_id: str, ) -> dict[str, Any]: """Verify server-originated database transaction evidence. @@ -3288,10 +4032,6 @@ def validate_online_transaction_receipt( evidence merely by supplying another key or environment identifier. """ - if not HEX64_RE.fullmatch(TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SHA256): - raise PublicationBatchError( - "online publication service public key pin is not provisioned" - ) if ( not TRUSTED_ONLINE_APPLY_ENVIRONMENT_IDS or len(set(TRUSTED_ONLINE_APPLY_ENVIRONMENT_IDS)) @@ -3304,10 +4044,17 @@ def validate_online_transaction_receipt( raise PublicationBatchError( "online publication environment allowlist is not provisioned" ) - online_public_raw = _public_key_raw(online_service_public_key) - if _sha256_bytes(online_public_raw) != TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SHA256: + if not HEX64_RE.fullmatch(TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SPKI_SHA256): raise PublicationBatchError( - "online publication service public key does not match the frozen pin" + "online publication service SPKI pin is not provisioned" + ) + online_public_spki = _public_key_spki(online_service_public_key) + if ( + _sha256_bytes(online_public_spki) + != TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SPKI_SHA256 + ): + raise PublicationBatchError( + "online publication service public key does not match the frozen SPKI pin" ) envelope = _require_keys( receipt, @@ -3321,12 +4068,20 @@ def validate_online_transaction_receipt( "online transaction receipt payload", ( "schema_version", - "key_id", + "candidate_commit", + "candidate_tag", + "batch_manifest_raw_sha256", "batch_manifest_payload_sha256", "approval_receipt_sha256", + "legal_source_review_binding_sha256", "environment_id", + "tenant_id", + "idempotency_key", "transaction_id", - "database_commit_id", + "transaction_mode", + "cross_database_atomicity", + "coordinator_deployment", + "participant_evidence", "committed_at", "authority_challenge", "generated_by", @@ -3337,10 +4092,17 @@ def validate_online_transaction_receipt( ) if ( payload["schema_version"] != ONLINE_TRANSACTION_PAYLOAD_SCHEMA_VERSION - or payload["key_id"] != TRUSTED_ONLINE_APPLY_SERVICE_KEY_ID + or payload["candidate_commit"] != manifest["candidate"]["commit"] + or payload["candidate_tag"] != manifest["candidate"]["tag"] or payload["status"] != "committed_and_verified" + or payload["transaction_mode"] != TRANSACTION_MODE + or payload["cross_database_atomicity"] is not False ): raise PublicationBatchError("online transaction receipt identity is invalid") + if payload["batch_manifest_raw_sha256"] != _require_sha( + manifest_raw_sha256, "online transaction manifest raw SHA-256" + ): + raise PublicationBatchError("online transaction raw manifest binding drift") if payload["batch_manifest_payload_sha256"] != manifest["manifest_payload_sha256"]: raise PublicationBatchError("online transaction receipt is not bound to this batch") if payload["approval_receipt_sha256"] != _require_sha( @@ -3349,30 +4111,55 @@ def validate_online_transaction_receipt( raise PublicationBatchError( "online transaction receipt is not bound to the approval receipt" ) - environment_id = _require_string( + if payload["legal_source_review_binding_sha256"] != _require_sha( + legal_review_binding_sha256, "expected legal review binding SHA-256" + ): + raise PublicationBatchError( + "online transaction receipt is not bound to the legal review" + ) + normalized_environment_id = _require_string( payload["environment_id"], "online transaction environment_id", max_length=256 ) - if environment_id != payload["environment_id"]: + normalized_expected_environment = _require_string( + environment_id, "expected online transaction environment_id", max_length=256 + ) + normalized_tenant_id = _require_string( + payload["tenant_id"], "online transaction tenant_id", max_length=64 + ) + normalized_expected_tenant = _require_string( + tenant_id, "expected online transaction tenant_id", max_length=64 + ) + if ( + normalized_environment_id != payload["environment_id"] + or normalized_environment_id != normalized_expected_environment + or normalized_tenant_id != payload["tenant_id"] + or normalized_tenant_id != normalized_expected_tenant + ): raise PublicationBatchError( - "online transaction environment_id must be canonical" + "online transaction environment or tenant binding drift" ) - if environment_id not in TRUSTED_ONLINE_APPLY_ENVIRONMENT_IDS: + if normalized_environment_id not in TRUSTED_ONLINE_APPLY_ENVIRONMENT_IDS: raise PublicationBatchError( "online transaction environment is not in the frozen allowlist" ) - transaction_id = _require_string( + expected_idempotency_key = publication_idempotency_key( + manifest_raw_sha256, + approval_raw_sha256, + legal_review_binding_sha256, + normalized_environment_id, + normalized_tenant_id, + ) + if payload["idempotency_key"] != expected_idempotency_key: + raise PublicationBatchError("online transaction idempotency binding drift") + normalized_transaction_id = _require_string( payload["transaction_id"], "online transaction_id", max_length=256 ) - database_commit_id = _require_string( - payload["database_commit_id"], "online database_commit_id", max_length=256 - ) if ( - transaction_id != payload["transaction_id"] - or database_commit_id != payload["database_commit_id"] + normalized_transaction_id != payload["transaction_id"] + or normalized_transaction_id + != publication_transaction_id(expected_idempotency_key) ): - raise PublicationBatchError( - "online transaction identifiers must be canonical" - ) + raise PublicationBatchError("online transaction identifier binding drift") committed_at = _parse_time(payload["committed_at"], "online committed_at") approval_payload = approval["payload"] if committed_at < _parse_time( @@ -3389,6 +4176,10 @@ def validate_online_transaction_receipt( approval_raw_sha256, apply_service_public_key, payload["authority_challenge"], + legal_review_binding_sha256=legal_review_binding_sha256, + legal_review_expires_at=legal_review_expires_at, + environment_id=normalized_environment_id, + tenant_id=normalized_tenant_id, ) challenge_payload = challenge["payload"] challenge_issued_at = _parse_time( @@ -3404,7 +4195,10 @@ def validate_online_transaction_receipt( approval_payload["expires_at"], "approval expires_at" ) if ( - challenge_payload["environment_id"] != environment_id + challenge_payload["environment_id"] != normalized_environment_id + or challenge_payload["tenant_id"] != normalized_tenant_id + or challenge_payload["idempotency_key"] != expected_idempotency_key + or challenge_payload["transaction_id"] != normalized_transaction_id or challenge_issued_at < approval_approved_at or challenge_expires_at > approval_expires_at or committed_at < challenge_issued_at @@ -3423,30 +4217,117 @@ def validate_online_transaction_receipt( raise PublicationBatchError( "online transaction receipt was not generated by the trusted service" ) - if payload["item_results"] != _expected_apply_item_results(manifest): + expected_item_results = _expected_apply_item_results(manifest) + if payload["item_results"] != expected_item_results: raise PublicationBatchError("online transaction 74-item evidence drift") if payload["write_count"] != EXPECTED_ASSET_COUNT + EXPECTED_MODULE_COUNT: raise PublicationBatchError("online transaction write count must be exactly 74") - try: - signature = base64.b64decode( - _require_string( - envelope["signature"], - "online transaction receipt signature", - max_length=1024, - ), - validate=True, - ) - online_service_public_key.verify( - signature, - ONLINE_TRANSACTION_SIGNATURE_DOMAIN + _canonical(payload), - ) - except (TypeError, ValueError, InvalidSignature) as exc: + image_digests = { + item["role"]: item["manifest_digest"] + for item in manifest["candidate"]["images"] + } + coordinator_deployment = _require_keys( + payload["coordinator_deployment"], + "online transaction coordinator deployment", + ("component", "candidate_commit", "release_image_digest"), + ) + if coordinator_deployment != { + "component": TRUSTED_ONLINE_APPLY_SERVICE_COMPONENT, + "candidate_commit": manifest["candidate"]["commit"], + "release_image_digest": image_digests["contract-api"], + }: + raise PublicationBatchError("online transaction coordinator deployment drift") + participant_evidence = payload["participant_evidence"] + if not isinstance(participant_evidence, list) or len(participant_evidence) != 2: raise PublicationBatchError( - "online transaction service signature verification failed" - ) from exc + "online transaction must contain exactly two participant commit proofs" + ) + expected_participants = { + "ai-platform": ( + EXPECTED_MODULE_COUNT, + EXPECTED_RUNTIME_PROMPT_COUNT, + image_digests["ai-platform-api"], + _sha256_json( + [item for item in expected_item_results if item["kind"] == "prompt_module"] + ), + ), + "contract-platform": ( + EXPECTED_ASSET_COUNT, + 0, + image_digests["contract-api"], + _sha256_json([item for item in expected_item_results if item["kind"] == "asset"]), + ), + } + if [item.get("participant") for item in participant_evidence] != sorted( + expected_participants + ): + raise PublicationBatchError("online participant evidence ordering or identity drift") + for index, value in enumerate(participant_evidence): + participant = _require_keys( + value, + f"online participant evidence {index}", + ( + "participant", + "state", + "transaction_id", + "candidate_commit", + "release_image_digest", + "committed_at", + "write_count", + "runtime_snapshot_count", + "committed_item_results_sha256", + "durable_commit_evidence_sha256", + ), + ) + ( + expected_write_count, + expected_runtime_count, + expected_image_digest, + expected_results_sha, + ) = expected_participants[ + participant["participant"] + ] + participant_committed_at = _parse_time( + participant["committed_at"], + f"online participant {participant['participant']} committed_at", + ) + if ( + participant["state"] != "committed" + or participant["transaction_id"] != normalized_transaction_id + or participant["candidate_commit"] != manifest["candidate"]["commit"] + or participant["release_image_digest"] != expected_image_digest + or participant_committed_at < challenge_issued_at + or participant_committed_at > committed_at + or participant_committed_at >= challenge_expires_at + or participant_committed_at >= approval_expires_at + or participant["write_count"] != expected_write_count + or participant["runtime_snapshot_count"] != expected_runtime_count + or participant["committed_item_results_sha256"] != expected_results_sha + ): + raise PublicationBatchError("online participant commit evidence drift") + supplied_durable_sha = _require_sha( + participant["durable_commit_evidence_sha256"], + "online participant durable commit evidence SHA-256", + ) + durable_projection = { + key: child + for key, child in participant.items() + if key != "durable_commit_evidence_sha256" + } + if supplied_durable_sha != _sha256_json(durable_projection): + raise PublicationBatchError( + "online participant durable commit evidence hash drift" + ) + signature = _verify_online_kms_signature( + online_service_public_key, + envelope["signature"], + domain=ONLINE_TRANSACTION_SIGNATURE_DOMAIN, + payload=payload, + label="online transaction receipt", + ) expected_id = ( "rc06-online-transaction-" - + _sha256_bytes(_canonical(payload) + signature)[:24] + + _sha256_bytes(_canonical(payload) + _canonical(signature))[:24] ) if envelope["receipt_id"] != expected_id: raise PublicationBatchError( @@ -3456,6 +4337,139 @@ def validate_online_transaction_receipt( return envelope +def validate_runtime_snapshot_receipt( + manifest: Mapping[str, Any], + manifest_raw_sha256: str, + approval_raw_sha256: str, + approval: Mapping[str, Any], + apply_service_public_key: Ed25519PublicKey, + online_service_public_key: Any, + online_transaction_receipt: Any, + receipt: Any, + *, + legal_review_binding_sha256: str, + legal_review_expires_at: str, + environment_id: str, + tenant_id: str, +) -> dict[str, Any]: + """Verify read-back proof bound to the exact committed online receipt.""" + + online_receipt = validate_online_transaction_receipt( + manifest, + manifest_raw_sha256, + approval_raw_sha256, + approval, + apply_service_public_key, + online_service_public_key, + online_transaction_receipt, + legal_review_binding_sha256=legal_review_binding_sha256, + legal_review_expires_at=legal_review_expires_at, + environment_id=environment_id, + tenant_id=tenant_id, + ) + envelope = _require_keys( + receipt, + "runtime snapshot receipt", + ("schema_version", "receipt_id", "payload", "signature"), + ) + if envelope["schema_version"] != RUNTIME_RECEIPT_SCHEMA_VERSION: + raise PublicationBatchError("runtime snapshot receipt schema is unsupported") + payload = _require_keys( + envelope["payload"], + "runtime snapshot receipt payload", + ( + "schema_version", + "batch_manifest_raw_sha256", + "batch_manifest_payload_sha256", + "approval_receipt_sha256", + "legal_source_review_binding_sha256", + "environment_id", + "tenant_id", + "idempotency_key", + "transaction_id", + "online_transaction_receipt_sha256", + "captured_at", + "generated_by", + "transaction_mode", + "cross_database_atomicity", + "publication_item_count", + "runtime_snapshot_dependencies", + "runtime_snapshot_dependency_count", + "status", + ), + ) + online_payload = online_receipt["payload"] + expected_online_sha = _sha256_json(online_receipt) + if ( + payload["schema_version"] != RUNTIME_PAYLOAD_SCHEMA_VERSION + or payload["batch_manifest_raw_sha256"] != manifest_raw_sha256 + or payload["batch_manifest_payload_sha256"] + != manifest["manifest_payload_sha256"] + or payload["approval_receipt_sha256"] != approval_raw_sha256 + or payload["legal_source_review_binding_sha256"] + != legal_review_binding_sha256 + or payload["environment_id"] != environment_id + or payload["tenant_id"] != tenant_id + or payload["idempotency_key"] != online_payload["idempotency_key"] + or payload["transaction_id"] != online_payload["transaction_id"] + or payload["online_transaction_receipt_sha256"] != expected_online_sha + or payload["transaction_mode"] != TRANSACTION_MODE + or payload["cross_database_atomicity"] is not False + or payload["publication_item_count"] + != EXPECTED_ASSET_COUNT + EXPECTED_MODULE_COUNT + or payload["runtime_snapshot_dependency_count"] + != EXPECTED_RUNTIME_PROMPT_COUNT + or payload["status"] != "published_and_resolved" + ): + raise PublicationBatchError("runtime snapshot receipt binding drift") + if payload["runtime_snapshot_dependencies"] != expected_runtime_snapshot_results( + manifest + ): + raise PublicationBatchError("runtime snapshot 14-item evidence drift") + captured_at = _parse_time(payload["captured_at"], "runtime captured_at") + committed_at = _parse_time(online_payload["committed_at"], "online committed_at") + approval_expires_at = _parse_time( + approval["payload"]["expires_at"], "approval expires_at" + ) + challenge_expires_at = _parse_time( + online_payload["authority_challenge"]["payload"]["expires_at"], + "apply challenge expires_at", + ) + if ( + captured_at < committed_at + or captured_at >= approval_expires_at + or captured_at >= challenge_expires_at + ): + raise PublicationBatchError("runtime snapshot receipt is outside its authority window") + actor = _require_keys( + payload["generated_by"], + "runtime snapshot generator", + ("type", "component"), + ) + if actor != { + "type": "system", + "component": TRUSTED_ONLINE_APPLY_SERVICE_COMPONENT, + }: + raise PublicationBatchError("runtime snapshot receipt generator is untrusted") + signature = _verify_online_kms_signature( + online_service_public_key, + envelope["signature"], + domain=RUNTIME_SIGNATURE_DOMAIN, + payload=payload, + label="runtime snapshot receipt", + ) + expected_id = ( + "rc06-runtime-snapshot-" + + _sha256_bytes(_canonical(payload) + _canonical(signature))[:24] + ) + if envelope["receipt_id"] != expected_id: + raise PublicationBatchError( + "runtime snapshot receipt_id does not match its signed material" + ) + _assert_no_secret_material(envelope, "runtime snapshot receipt") + return envelope + + def validate_apply_receipt( manifest: Mapping[str, Any], manifest_raw_sha256: str, @@ -3463,9 +4477,14 @@ def validate_apply_receipt( approval_keyring: Mapping[str, Any], approval_root_public_key: Ed25519PublicKey, apply_service_public_key: Ed25519PublicKey, - online_service_public_key: Ed25519PublicKey, + online_service_public_key: Any, approval_raw_sha256: str, receipt: Any, + *, + legal_review_binding_sha256: str, + legal_review_expires_at: str, + environment_id: str, + tenant_id: str, ) -> dict[str, Any]: validate_approval_receipt( manifest, @@ -3485,24 +4504,72 @@ def validate_apply_receipt( envelope["payload"], "apply receipt payload", ( - "schema_version", "batch_manifest_payload_sha256", "approval_receipt_sha256", - "environment_id", "applied_at", "generated_by", "item_results", "write_count", - "status", "online_transaction_receipt", + "schema_version", + "batch_manifest_raw_sha256", + "batch_manifest_payload_sha256", + "approval_receipt_sha256", + "legal_source_review_binding_sha256", + "environment_id", + "tenant_id", + "idempotency_key", + "transaction_id", + "authorized_commit_at", + "countersigned_at", + "generated_by", + "transaction_mode", + "cross_database_atomicity", + "item_results", + "write_count", + "status", + "online_transaction_receipt", + "runtime_snapshot_receipt", ), ) - if row["schema_version"] != APPLY_PAYLOAD_SCHEMA_VERSION or row["status"] != "applied_and_verified": + if ( + row["schema_version"] != APPLY_PAYLOAD_SCHEMA_VERSION + or row["status"] != "applied_and_verified" + or row["transaction_mode"] != TRANSACTION_MODE + or row["cross_database_atomicity"] is not False + ): raise PublicationBatchError("apply receipt is not a successful RC-06 receipt") + if row["batch_manifest_raw_sha256"] != _require_sha( + manifest_raw_sha256, "apply manifest raw SHA-256" + ): + raise PublicationBatchError("apply receipt raw manifest binding drift") if row["batch_manifest_payload_sha256"] != manifest["manifest_payload_sha256"]: raise PublicationBatchError("apply receipt is not bound to this batch") if row["approval_receipt_sha256"] != _require_sha(approval_raw_sha256, "approval raw SHA-256"): raise PublicationBatchError("apply receipt is not bound to the approval receipt") - _require_string(row["environment_id"], "apply environment_id", max_length=256) - applied_at = _parse_time(row["applied_at"], "apply applied_at") - approval_payload = approval["payload"] - if applied_at < _parse_time(approval_payload["approved_at"], "approval approved_at") or applied_at >= _parse_time( - approval_payload["expires_at"], "approval expires_at" + if row["legal_source_review_binding_sha256"] != _require_sha( + legal_review_binding_sha256, "expected legal review binding SHA-256" ): - raise PublicationBatchError("apply receipt was generated outside the approval window") + raise PublicationBatchError("apply receipt is not bound to the legal review") + if ( + row["environment_id"] + != _require_string(environment_id, "expected apply environment_id", max_length=256) + or row["tenant_id"] + != _require_string(tenant_id, "expected apply tenant_id", max_length=64) + ): + raise PublicationBatchError("apply receipt environment or tenant binding drift") + expected_idempotency_key = publication_idempotency_key( + manifest_raw_sha256, + approval_raw_sha256, + legal_review_binding_sha256, + environment_id, + tenant_id, + ) + if ( + row["idempotency_key"] != expected_idempotency_key + or row["transaction_id"] != publication_transaction_id(expected_idempotency_key) + ): + raise PublicationBatchError("apply receipt transaction identity drift") + authorized_commit_at = _parse_time( + row["authorized_commit_at"], "apply authorized_commit_at" + ) + countersigned_at = _parse_time( + row["countersigned_at"], "apply countersigned_at" + ) + approval_payload = approval["payload"] actor = _require_keys(row["generated_by"], "apply generator", ("type", "component")) if actor != {"type": "system", "component": "rc06-apply-authority"}: raise PublicationBatchError("per-item apply evidence must be authority generated") @@ -3521,17 +4588,51 @@ def validate_apply_receipt( apply_service_public_key, online_service_public_key, row["online_transaction_receipt"], + legal_review_binding_sha256=legal_review_binding_sha256, + legal_review_expires_at=legal_review_expires_at, + environment_id=environment_id, + tenant_id=tenant_id, + ) + runtime_receipt = validate_runtime_snapshot_receipt( + manifest, + manifest_raw_sha256, + approval_raw_sha256, + approval, + apply_service_public_key, + online_service_public_key, + online_receipt, + row["runtime_snapshot_receipt"], + legal_review_binding_sha256=legal_review_binding_sha256, + legal_review_expires_at=legal_review_expires_at, + environment_id=environment_id, + tenant_id=tenant_id, ) online_payload = online_receipt["payload"] + runtime_payload = runtime_receipt["payload"] if ( row["environment_id"] != online_payload["environment_id"] - or row["applied_at"] != online_payload["committed_at"] + or row["tenant_id"] != online_payload["tenant_id"] + or row["legal_source_review_binding_sha256"] + != online_payload["legal_source_review_binding_sha256"] + or row["idempotency_key"] != online_payload["idempotency_key"] + or row["transaction_id"] != online_payload["transaction_id"] or row["item_results"] != online_payload["item_results"] or row["write_count"] != online_payload["write_count"] + or runtime_payload["online_transaction_receipt_sha256"] + != _sha256_json(online_receipt) ): raise PublicationBatchError( - "apply receipt does not exactly mirror the trusted online transaction" + "apply receipt does not exactly bind the online/runtime evidence pair" ) + runtime_captured_at = _parse_time( + runtime_payload["captured_at"], "runtime captured_at" + ) + if authorized_commit_at != runtime_captured_at: + raise PublicationBatchError( + "apply authorized commit time must equal the signed runtime read-back time" + ) + if countersigned_at < authorized_commit_at: + raise PublicationBatchError("apply countersign predates runtime read-back evidence") if not HEX64_RE.fullmatch(TRUSTED_APPLY_SERVICE_PUBLIC_KEY_SHA256): raise PublicationBatchError("apply service public key pin is not provisioned") service_public_raw = _public_key_raw(apply_service_public_key) @@ -3539,12 +4640,14 @@ def validate_apply_receipt( raise PublicationBatchError("apply service public key does not match the frozen pin") if service_public_raw == _public_key_raw(approval_root_public_key): raise PublicationBatchError("apply service key must differ from the approval root key") - if service_public_raw == _public_key_raw(online_service_public_key): + service_public_spki = _public_key_spki(apply_service_public_key) + online_public_spki = _public_key_spki(online_service_public_key) + approval_root_spki = _public_key_spki(approval_root_public_key) + if service_public_spki == online_public_spki: raise PublicationBatchError( "apply service key must differ from the online transaction service key" ) - online_public_raw = _public_key_raw(online_service_public_key) - if online_public_raw == _public_key_raw(approval_root_public_key): + if online_public_spki == approval_root_spki: raise PublicationBatchError( "online transaction service key must differ from the approval root key" ) @@ -3555,13 +4658,13 @@ def validate_apply_receipt( ) except (KeyError, AttributeError, UnicodeError, ValueError) as exc: raise PublicationBatchError("approval keyring public key is invalid") from exc - if isinstance(approval_public_key, Ed25519PublicKey) and service_public_raw == _public_key_raw( + if isinstance(approval_public_key, Ed25519PublicKey) and service_public_spki == _public_key_spki( approval_public_key ): raise PublicationBatchError("apply service key must differ from reviewer approval keys") if ( isinstance(approval_public_key, Ed25519PublicKey) - and online_public_raw == _public_key_raw(approval_public_key) + and online_public_spki == _public_key_spki(approval_public_key) ): raise PublicationBatchError( "online transaction service key must differ from reviewer approval keys" @@ -3649,7 +4752,25 @@ def prepare(args: argparse.Namespace) -> dict[str, Any]: ) _, source_payload_sha = _source_manifest_index(source_manifest, args.commit) image_identity = validate_image_identities(image_payload, args.commit, args.tag) - rc05_identity = validate_rc05_plan(rc05_payload) + expected_rc05_candidate = { + "schema_version": RC05_CANDIDATE_SCHEMA_VERSION, + "commit": args.commit, + "tag": args.tag, + "source_manifest": { + "schema_version": "yuqei.release-source-manifest.v1", + "raw_sha256": source_raw_sha, + "payload_sha256": source_payload_sha, + }, + "image_identities": { + "schema_version": image_identity["source_schema"], + "raw_sha256": image_raw_sha, + "identity_set_sha256": image_identity["identity_set_sha256"], + "images": image_identity["images"], + }, + } + rc05_identity = validate_rc05_plan( + rc05_payload, expected_candidate=expected_rc05_candidate + ) verify_rc05_source_pins(repo, args.commit, source_manifest, rc05_payload) rc05_terminal_evidence = validate_rc05_terminal_evidence( rc05_payload, @@ -3747,6 +4868,10 @@ def main(argv: list[str] | None = None) -> int: apply_parser.add_argument("--approval-root-public-key", type=Path, required=True) apply_parser.add_argument("--apply-service-public-key", type=Path, required=True) apply_parser.add_argument("--online-service-public-key", type=Path, required=True) + apply_parser.add_argument("--legal-review-binding-sha256", required=True) + apply_parser.add_argument("--legal-review-expires-at", required=True) + apply_parser.add_argument("--environment-id", required=True) + apply_parser.add_argument("--tenant-id", required=True) apply_parser.add_argument("--apply-receipt", type=Path, required=True) apply_parser.add_argument("--apply-receipt-sha256", required=True) @@ -3813,9 +4938,9 @@ def main(argv: list[str] | None = None) -> int: expected_sha256=TRUSTED_APPLY_SERVICE_PUBLIC_KEY_SHA256, label="apply service public key", ) - online_service_public_key = _read_pinned_raw_public_key( + online_service_public_key = _read_pinned_spki_public_key( args.online_service_public_key, - expected_sha256=TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SHA256, + expected_sha256=TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SPKI_SHA256, label="online publication service public key", ) validate_approval_receipt( @@ -3838,13 +4963,18 @@ def main(argv: list[str] | None = None) -> int: online_service_public_key, approval_raw, apply_receipt, + legal_review_binding_sha256=args.legal_review_binding_sha256, + legal_review_expires_at=args.legal_review_expires_at, + environment_id=args.environment_id, + tenant_id=args.tenant_id, ) result = { "structurally_valid": True, "apply_receipt_raw_sha256": apply_raw, - "evidence_complete": False, + "evidence_complete": True, "release_ready": False, - "blocked_controls": list(BLOCKED_CONTROLS[2:]), + "blocked_controls": [], + "evidence_scope": "rc06_publication_only", "service_writes": 0, } except PublicationBatchError as exc: diff --git a/yuqei-infra/scripts/rc06-release-trust-bootstrap.py b/yuqei-infra/scripts/rc06-release-trust-bootstrap.py index f09addae..ab568efb 100644 --- a/yuqei-infra/scripts/rc06-release-trust-bootstrap.py +++ b/yuqei-infra/scripts/rc06-release-trust-bootstrap.py @@ -93,9 +93,9 @@ AUTHORITY_REGISTRATION_SCHEMA = "yuqei.release-authority-key-registration.v1" PROTECTED_KEY_SCHEMA = "yuqei.release-authority-dpapi-private-key.v1" RECOVERY_BUNDLE_SCHEMA = "yuqei.release-recovery-keyring-bundle.v2" RC06_KEYRING_BUNDLE_SCHEMA = "yuqei.rc06-candidate-keyring-bundle.v2" -APPLY_RECEIPT_BUNDLE_SCHEMA = "yuqei.rc06-apply-receipt-bundle.v3" -APPLY_CHALLENGE_CONSUMPTION_SCHEMA = "yuqei.rc06-apply-challenge-consumption.v1" -APPLY_CHALLENGE_CONTEXT_SCHEMA = "yuqei.rc06-apply-challenge-context.v1" +APPLY_RECEIPT_BUNDLE_SCHEMA = "yuqei.rc06-apply-receipt-bundle.v4" +APPLY_CHALLENGE_CONSUMPTION_SCHEMA = "yuqei.rc06-apply-challenge-consumption.v2" +APPLY_CHALLENGE_CONTEXT_SCHEMA = "yuqei.rc06-apply-challenge-context.v2" PIN_QUARANTINE_SCHEMA = "yuqei.release-authority-pin-quarantine.v1" PIN_PLAN_SCHEMA = "yuqei.release-authority-pin-plan.v1" PIN_TRANSACTION_SCHEMA = "yuqei.release-authority-pin-transaction.v2" @@ -111,8 +111,13 @@ PIN_SOURCE_CONSTANTS: Mapping[str, tuple[str, ...]] = { "TRUSTED_APPROVAL_ROOT_PUBLIC_KEY_SHA256", "FROZEN_APPROVAL_KEYRING_EPOCH", "TRUSTED_APPLY_SERVICE_PUBLIC_KEY_SHA256", - "TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SHA256", + "TRUSTED_ONLINE_APPLY_SERVICE_PROVIDER", + "TRUSTED_ONLINE_APPLY_SERVICE_KEY_REFERENCE", + "TRUSTED_ONLINE_APPLY_SERVICE_KEY_VERSION", + "TRUSTED_ONLINE_APPLY_SERVICE_ALGORITHM", + "TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SPKI_SHA256", "TRUSTED_ONLINE_APPLY_ENVIRONMENT_IDS", + "TRUSTED_SCENE_LEGAL_REVIEW_KEYRING_SHA256", ), "yuqei-infra/deploy/plan-first-release-snapshot-restore.py": ( "TRUSTED_ROOT_PUBLIC_KEY_SHA256", @@ -152,7 +157,7 @@ FIRST_RELEASE_ROOT_KEY_ID = "yuqei-first-release-recovery-root-v1" ROLLBACK_KEYRING_SCHEMA = "yuqei.rollback-rehearsal-keyring.v3" ROLLBACK_ROOT_KEY_ID = "yuqei-rollback-rehearsal-root-v1" REVIEWER_REGISTRATION_SCHEMA = "yuqei.rc06-reviewer-key-registration.v1" -ONLINE_SERVICE_REGISTRATION_SCHEMA = "yuqei.rc06-online-service-key-registration.v1" +ONLINE_SERVICE_REGISTRATION_SCHEMA = "yuqei.rc06-online-service-key-registration.v2" class AuthorityError(RuntimeError): @@ -776,7 +781,7 @@ def load_reviewer_registration(path: Path) -> ReviewerRegistration: def load_online_service_registration( path: Path, -) -> tuple[dict[str, Any], Ed25519PublicKey, bytes]: +) -> tuple[dict[str, Any], Any, bytes]: try: raw = batch.secure_read_bytes( path, @@ -798,12 +803,14 @@ def load_online_service_registration( ) from exc if not isinstance(registration, dict) or set(registration) != { "schema_version", - "key_id", + "provider", + "key_reference", + "key_version", "algorithm", "component", "created_at", - "public_key_raw_base64", - "public_key_raw_sha256", + "public_key_pem", + "public_key_spki_sha256", }: raise AuthorityError( "online publication service registration has an invalid shape" @@ -814,26 +821,50 @@ def load_online_service_registration( ) if ( registration["schema_version"] != ONLINE_SERVICE_REGISTRATION_SCHEMA - or registration["key_id"] != batch.TRUSTED_ONLINE_APPLY_SERVICE_KEY_ID - or registration["algorithm"] != "Ed25519" + or registration["provider"] not in {"aliyun", "tencent"} + or registration["algorithm"] + not in {"RSA_PSS_SHA_256", "RSA_PKCS1_SHA_256", "ECDSA_SHA_256"} or registration["component"] != batch.TRUSTED_ONLINE_APPLY_SERVICE_COMPONENT ): raise AuthorityError("online publication service registration identity drift") + for field in ("key_reference", "key_version"): + if ( + not isinstance(registration[field], str) + or not registration[field].strip() + or registration[field] != registration[field].strip() + or len(registration[field]) > 255 + ): + raise AuthorityError( + f"online publication service registration {field} is invalid" + ) _parse_time(registration["created_at"], "online service registration created_at") try: - public_raw = base64.b64decode( - registration["public_key_raw_base64"], validate=True + public_pem = registration["public_key_pem"].encode("ascii") + public_key = serialization.load_pem_public_key(public_pem) + public_spki = public_key.public_bytes( + serialization.Encoding.DER, + serialization.PublicFormat.SubjectPublicKeyInfo, ) - public_key = Ed25519PublicKey.from_public_bytes(public_raw) - except (TypeError, ValueError) as exc: + except (AttributeError, TypeError, UnicodeError, ValueError) as exc: raise AuthorityError( "online publication service registration public key is invalid" ) from exc - if registration["public_key_raw_sha256"] != batch._sha256_bytes(public_raw): + algorithm = registration["algorithm"] + if ( + algorithm.startswith("RSA_") + and not isinstance(public_key, batch.rsa.RSAPublicKey) + ) or ( + algorithm == "ECDSA_SHA_256" + and not isinstance(public_key, batch.ec.EllipticCurvePublicKey) + ): + raise AuthorityError( + "online publication service registration algorithm/key type drift" + ) + if registration["public_key_spki_sha256"] != batch._sha256_bytes(public_spki): raise AuthorityError( "online publication service registration fingerprint drift" ) - return registration, public_key, public_raw + return registration, public_key, public_spki def _assert_reviewer_separate(keys: Mapping[str, AuthorityKey], reviewer_raw: bytes) -> None: @@ -1302,13 +1333,23 @@ def _apply_challenge_context_identity( manifest_sha256: str, approval_id: str, approval_sha256: str, + legal_review_binding_sha256: str, + legal_review_expires_at: str, environment_id: str, + tenant_id: str, + idempotency_key: str, + transaction_id: str, ) -> dict[str, str]: return { "manifest_raw_sha256": manifest_sha256, "approval_id": approval_id, "approval_raw_sha256": approval_sha256, + "legal_source_review_binding_sha256": legal_review_binding_sha256, + "legal_source_review_expires_at": legal_review_expires_at, "environment_id": environment_id, + "tenant_id": tenant_id, + "idempotency_key": idempotency_key, + "transaction_id": transaction_id, "operation": batch.APPLY_OPERATION, } @@ -1343,6 +1384,7 @@ def _load_apply_challenge_context( "context", "challenge", "challenge_raw_sha256", + "superseded_challenge_ids", } challenge = ledger.get("challenge") if ( @@ -1351,6 +1393,15 @@ def _load_apply_challenge_context( or ledger.get("context_id") != context_id or ledger.get("context") != dict(identity) or not isinstance(challenge, dict) + or not isinstance(ledger.get("superseded_challenge_ids"), list) + or len(set(ledger["superseded_challenge_ids"])) + != len(ledger["superseded_challenge_ids"]) + or any( + not isinstance(value, str) + or not re.fullmatch(r"rc06-apply-challenge-[0-9a-f]{24}", value) + for value in ledger["superseded_challenge_ids"] + ) + or challenge.get("challenge_id") in ledger["superseded_challenge_ids"] ): raise AuthorityError("apply challenge context ledger identity drift") challenge_raw = _canonical(challenge) @@ -1406,7 +1457,14 @@ def issue_apply_challenge( approval_keyring_file: Path, approval_keyring_sha256: str, approval_root_public_key_file: Path, + legal_review_request_file: Path, + legal_review_request_sha256: str, + legal_review_decision_file: Path, + legal_review_decision_sha256: str, + legal_review_keyring_file: Path, + legal_review_keyring_sha256: str, environment_id: str, + tenant_id: str, unprotect: UnprotectFunction = _unprotect_current_user, now: datetime | None = None, validity: timedelta = batch.MAX_APPLY_CHALLENGE_TTL, @@ -1421,6 +1479,13 @@ def issue_apply_challenge( or environment_id not in batch.TRUSTED_ONLINE_APPLY_ENVIRONMENT_IDS ): raise AuthorityError("apply challenge environment is not in the frozen allowlist") + if ( + not isinstance(tenant_id, str) + or not tenant_id.strip() + or tenant_id != tenant_id.strip() + or len(tenant_id) > 64 + ): + raise AuthorityError("apply challenge tenant_id is invalid") state_directory = signer._assert_local_safe_path( state_directory, "release authority state directory", @@ -1434,7 +1499,14 @@ def issue_apply_challenge( raise AuthorityError("frozen RC-06 root pin does not match the initialized authority") if batch.TRUSTED_APPLY_SERVICE_PUBLIC_KEY_SHA256 != apply_service.public_sha256: raise AuthorityError("frozen apply-service pin does not match the initialized authority") - if not HEX64_RE.fullmatch(batch.TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SHA256): + if ( + not HEX64_RE.fullmatch( + batch.TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SPKI_SHA256 + ) + or not batch.TRUSTED_ONLINE_APPLY_SERVICE_KEY_REFERENCE + or not batch.TRUSTED_ONLINE_APPLY_SERVICE_KEY_VERSION + or not batch.TRUSTED_ONLINE_APPLY_SERVICE_ALGORITHM + ): raise AuthorityError("online apply adapter trust is not provisioned") manifest, _manifest_raw = _read_expected_json( manifest_file, "RC-06 publication manifest", manifest_sha256 @@ -1447,6 +1519,21 @@ def issue_apply_challenge( "RC-06 approval keyring", approval_keyring_sha256, ) + legal_request, _legal_request_raw = _read_expected_json( + legal_review_request_file, + "scene legal review request", + legal_review_request_sha256, + ) + legal_decision, _legal_decision_raw = _read_expected_json( + legal_review_decision_file, + "scene legal review decision", + legal_review_decision_sha256, + ) + legal_keyring, _legal_keyring_raw = _read_expected_json( + legal_review_keyring_file, + "scene legal review keyring", + legal_review_keyring_sha256, + ) try: validated_manifest = batch.validate_manifest(manifest) root_raw = batch.secure_read_bytes( @@ -1466,20 +1553,48 @@ def issue_apply_challenge( keyring, root_public_key, ) + _legal_summary, legal_review_binding_sha256 = ( + batch.validate_scene_legal_review_evidence( + REPO_ROOT, + validated_manifest, + approval, + legal_request, + legal_decision, + legal_keyring, + legal_review_keyring_sha256, + now=current, + ) + ) except (ValueError, batch.PublicationBatchError) as exc: raise AuthorityError(f"RC-06 approval chain is invalid: {exc}") from exc + idempotency_key = batch.publication_idempotency_key( + manifest_sha256, + approval_sha256, + legal_review_binding_sha256, + environment_id, + tenant_id, + ) + transaction_id = batch.publication_transaction_id(idempotency_key) + legal_review_expires_at = _legal_summary["expires_at"] context_identity = _apply_challenge_context_identity( manifest_sha256=manifest_sha256, approval_id=approval["approval_id"], approval_sha256=approval_sha256, + legal_review_binding_sha256=legal_review_binding_sha256, + legal_review_expires_at=legal_review_expires_at, environment_id=environment_id, + tenant_id=tenant_id, + idempotency_key=idempotency_key, + transaction_id=transaction_id, ) context_path, context_id = _apply_challenge_context_path( state_directory, context_identity, ) + superseded_challenge_ids: list[str] = [] + replacing_expired_challenge = False if context_path.exists() or context_path.is_symlink(): - _ledger, challenge, challenge_raw = _load_apply_challenge_context( + context_ledger, challenge, challenge_raw = _load_apply_challenge_context( context_path, context_id, context_identity, @@ -1491,31 +1606,59 @@ def issue_apply_challenge( approval_sha256, apply_service.public_key, challenge, + legal_review_binding_sha256=legal_review_binding_sha256, + legal_review_expires_at=legal_review_expires_at, + environment_id=environment_id, + tenant_id=tenant_id, ) except batch.PublicationBatchError as exc: raise AuthorityError( f"persisted apply challenge failed verification: {exc}" ) from exc - challenge_path = _publish_persisted_apply_challenge( - state_directory, - challenge, - challenge_raw, + challenge_expires = _parse_time( + challenge["payload"]["expires_at"], "apply challenge expires_at" ) - return { - "status": "apply_challenge_already_issued_not_replaced", - "authorization_context_id": context_id, - "challenge_id": challenge["challenge_id"], - "challenge_file": os.fspath(challenge_path), - "expires_at": challenge["payload"]["expires_at"], - "environment_id": environment_id, - "operation": batch.APPLY_OPERATION, - } + if current < challenge_expires: + challenge_path = _publish_persisted_apply_challenge( + state_directory, + challenge, + challenge_raw, + ) + return { + "status": "apply_challenge_already_issued_not_replaced", + "authorization_context_id": context_id, + "challenge_id": challenge["challenge_id"], + "challenge_file": os.fspath(challenge_path), + "expires_at": challenge["payload"]["expires_at"], + "environment_id": environment_id, + "tenant_id": tenant_id, + "idempotency_key": idempotency_key, + "transaction_id": transaction_id, + "operation": batch.APPLY_OPERATION, + } + consumption_path = ( + state_directory + / "apply-challenge-consumption" + / f"{challenge['challenge_id']}.json" + ) + if consumption_path.exists() or consumption_path.is_symlink(): + raise AuthorityError( + "expired apply challenge has a reservation/consumption record and cannot be replaced" + ) + superseded_challenge_ids = [ + *context_ledger["superseded_challenge_ids"], + challenge["challenge_id"], + ] + replacing_expired_challenge = True approved_at = _parse_time(approval["payload"]["approved_at"], "approval approved_at") approval_expires = _parse_time( approval["payload"]["expires_at"], "approval expires_at" ) - expires = min(current + validity, approval_expires) + legal_review_expires = _parse_time( + legal_review_expires_at, "legal review expires_at" + ) + expires = min(current + validity, approval_expires, legal_review_expires) if current < approved_at or expires <= current: raise AuthorityError("current time is outside the approval window") signing_keys = load_authority( @@ -1535,8 +1678,13 @@ def issue_apply_challenge( "manifest_payload_sha256" ], "approval_receipt_sha256": approval_sha256, + "legal_source_review_binding_sha256": legal_review_binding_sha256, + "legal_source_review_expires_at": legal_review_expires_at, "operation": batch.APPLY_OPERATION, "environment_id": environment_id, + "tenant_id": tenant_id, + "idempotency_key": idempotency_key, + "transaction_id": transaction_id, "actor": { "type": "system", "component": batch.TRUSTED_ONLINE_APPLY_SERVICE_COMPONENT, @@ -1563,6 +1711,10 @@ def issue_apply_challenge( approval_sha256, signing_apply_service.public_key, challenge, + legal_review_binding_sha256=legal_review_binding_sha256, + legal_review_expires_at=legal_review_expires_at, + environment_id=environment_id, + tenant_id=tenant_id, ) except batch.PublicationBatchError as exc: raise AuthorityError(f"apply challenge self-verification failed: {exc}") from exc @@ -1573,24 +1725,36 @@ def issue_apply_challenge( "context": context_identity, "challenge": challenge, "challenge_raw_sha256": batch._sha256_bytes(challenge_raw), + "superseded_challenge_ids": superseded_challenge_ids, } - _publish_private_no_replace( - context_path, - _canonical(context_ledger), - "apply challenge context ledger", - ) + if replacing_expired_challenge: + _write_pin_journal(context_path, context_ledger) + else: + _publish_private_no_replace( + context_path, + _canonical(context_ledger), + "apply challenge context ledger", + ) challenge_path = _publish_persisted_apply_challenge( state_directory, challenge, challenge_raw, ) return { - "status": "apply_challenge_issued_not_consumed", + "status": ( + "expired_unconsumed_apply_challenge_replaced" + if replacing_expired_challenge + else "apply_challenge_issued_not_consumed" + ), "authorization_context_id": context_id, "challenge_id": challenge["challenge_id"], "challenge_file": os.fspath(challenge_path), "expires_at": payload["expires_at"], "environment_id": environment_id, + "tenant_id": tenant_id, + "idempotency_key": idempotency_key, + "transaction_id": transaction_id, + "superseded_challenge_ids": superseded_challenge_ids, "operation": batch.APPLY_OPERATION, } @@ -1623,26 +1787,55 @@ def _normalize_online_transaction_receipts( return normalized +def _normalize_runtime_snapshot_receipt( + payload: Any, + raw: bytes, +) -> dict[str, Any]: + if not isinstance(payload, dict): + raise AuthorityError("runtime snapshot input must contain exactly one signed receipt") + if _canonical(payload) != raw: + raise AuthorityError("runtime snapshot input must use exact canonical JSON bytes") + if payload.get("schema_version") != batch.RUNTIME_RECEIPT_SCHEMA_VERSION: + raise AuthorityError("runtime snapshot receipt uses an unsupported schema") + return payload + + def _apply_payload_from_online_receipt( online_receipt: Mapping[str, Any], + runtime_receipt: Mapping[str, Any], + *, + countersigned_at: datetime, ) -> dict[str, Any]: online_payload = online_receipt["payload"] return { "schema_version": batch.APPLY_PAYLOAD_SCHEMA_VERSION, + "batch_manifest_raw_sha256": online_payload[ + "batch_manifest_raw_sha256" + ], "batch_manifest_payload_sha256": online_payload[ "batch_manifest_payload_sha256" ], "approval_receipt_sha256": online_payload["approval_receipt_sha256"], + "legal_source_review_binding_sha256": online_payload[ + "legal_source_review_binding_sha256" + ], "environment_id": online_payload["environment_id"], - "applied_at": online_payload["committed_at"], + "tenant_id": online_payload["tenant_id"], + "idempotency_key": online_payload["idempotency_key"], + "transaction_id": online_payload["transaction_id"], + "authorized_commit_at": runtime_receipt["payload"]["captured_at"], + "countersigned_at": _format_time(countersigned_at), "generated_by": { "type": "system", "component": "rc06-apply-authority", }, + "transaction_mode": batch.TRANSACTION_MODE, + "cross_database_atomicity": False, "item_results": online_payload["item_results"], "write_count": online_payload["write_count"], "status": "applied_and_verified", "online_transaction_receipt": dict(online_receipt), + "runtime_snapshot_receipt": dict(runtime_receipt), } @@ -1687,7 +1880,16 @@ def _load_persisted_apply_challenge( manifest_sha256=manifest_sha256, approval_id=str(approval.get("approval_id", "")), approval_sha256=approval_sha256, + legal_review_binding_sha256=str( + challenge_payload.get("legal_source_review_binding_sha256", "") + ), + legal_review_expires_at=str( + challenge_payload.get("legal_source_review_expires_at", "") + ), environment_id=str(challenge_payload.get("environment_id", "")), + tenant_id=str(challenge_payload.get("tenant_id", "")), + idempotency_key=str(challenge_payload.get("idempotency_key", "")), + transaction_id=str(challenge_payload.get("transaction_id", "")), ) context_path, context_id = _apply_challenge_context_path( state_directory, @@ -1723,6 +1925,7 @@ def _apply_consumption_identity( challenge: Mapping[str, Any], challenge_raw: bytes, online_receipt: Mapping[str, Any], + runtime_receipt: Mapping[str, Any], output_directory: Path, manifest_sha256: str, approval_sha256: str, @@ -1734,10 +1937,21 @@ def _apply_consumption_identity( "challenge_raw_sha256": batch._sha256_bytes(challenge_raw), "online_receipt_id": online_receipt["receipt_id"], "online_receipt_payload_sha256": batch._sha256_json(online_receipt), + "runtime_receipt_id": runtime_receipt["receipt_id"], + "runtime_receipt_payload_sha256": batch._sha256_json(runtime_receipt), "manifest_raw_sha256": manifest_sha256, "approval_raw_sha256": approval_sha256, "operation": batch.APPLY_OPERATION, "environment_id": challenge["payload"]["environment_id"], + "tenant_id": challenge["payload"]["tenant_id"], + "legal_source_review_binding_sha256": challenge["payload"][ + "legal_source_review_binding_sha256" + ], + "legal_source_review_expires_at": challenge["payload"][ + "legal_source_review_expires_at" + ], + "idempotency_key": challenge["payload"]["idempotency_key"], + "transaction_id": challenge["payload"]["transaction_id"], "actor": dict(challenge["payload"]["actor"]), "output_directory_resolved": os.fspath(output_directory.resolve(strict=False)), } @@ -1851,6 +2065,8 @@ def sign_apply_payloads( online_service_public_key_file: Path, online_transaction_receipts_file: Path, online_transaction_receipts_sha256: str, + runtime_snapshot_receipt_file: Path, + runtime_snapshot_receipt_sha256: str, output_directory: Path, unprotect: UnprotectFunction = _unprotect_current_user, now: datetime | None = None, @@ -1886,9 +2102,15 @@ def sign_apply_payloads( raise AuthorityError("frozen RC-06 root pin does not match the initialized authority") if batch.TRUSTED_APPLY_SERVICE_PUBLIC_KEY_SHA256 != apply_service.public_sha256: raise AuthorityError("frozen apply-service pin does not match the initialized authority") - if not HEX64_RE.fullmatch( - batch.TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SHA256 - ) or not batch.TRUSTED_ONLINE_APPLY_ENVIRONMENT_IDS: + if ( + not HEX64_RE.fullmatch( + batch.TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SPKI_SHA256 + ) + or not batch.TRUSTED_ONLINE_APPLY_SERVICE_KEY_REFERENCE + or not batch.TRUSTED_ONLINE_APPLY_SERVICE_KEY_VERSION + or not batch.TRUSTED_ONLINE_APPLY_SERVICE_ALGORITHM + or not batch.TRUSTED_ONLINE_APPLY_ENVIRONMENT_IDS + ): raise AuthorityError( "online apply adapter trust is not provisioned in frozen source; " "formal receipt signing is disabled" @@ -1918,6 +2140,15 @@ def sign_apply_payloads( online_input, online_input_raw, ) + runtime_input, runtime_input_raw = _read_expected_json( + runtime_snapshot_receipt_file, + "RC-06 runtime snapshot receipt input", + runtime_snapshot_receipt_sha256, + ) + runtime_receipt = _normalize_runtime_snapshot_receipt( + runtime_input, + runtime_input_raw, + ) try: validated_manifest = batch.validate_manifest(manifest) root_raw = batch.secure_read_bytes( @@ -1928,18 +2159,13 @@ def sign_apply_payloads( if root_raw != rc06_root.public_raw: raise AuthorityError("approval root public key does not match the initialized authority") root_public_key = Ed25519PublicKey.from_public_bytes(root_raw) - online_raw = batch.secure_read_bytes( + online_public_key = batch._read_pinned_spki_public_key( online_service_public_key_file, - "RC-06 online publication service public key", - maximum_bytes=32, + expected_sha256=( + batch.TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SPKI_SHA256 + ), + label="RC-06 online publication service public key", ) - if len(online_raw) != 32 or batch._sha256_bytes(online_raw) != ( - batch.TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SHA256 - ): - raise AuthorityError( - "online publication service public key does not match the frozen pin" - ) - online_public_key = Ed25519PublicKey.from_public_bytes(online_raw) batch.validate_approval_receipt( validated_manifest, manifest_sha256, @@ -1956,45 +2182,132 @@ def sign_apply_payloads( approved_at = _parse_time(approval_payload["approved_at"], "approval approved_at") expires_at = _parse_time(approval_payload["expires_at"], "approval expires_at") - verified_payloads: list[dict[str, Any]] = [] - for index, online_receipt in enumerate(online_receipts, start=1): - try: - batch.validate_online_transaction_receipt( - validated_manifest, - manifest_sha256, - approval_sha256, - approval, - apply_service.public_key, - online_public_key, - online_receipt, - ) - except batch.PublicationBatchError as exc: - raise AuthorityError( - f"online transaction receipt {index} is untrusted: {exc}" - ) from exc - verified_payloads.append(_apply_payload_from_online_receipt(online_receipt)) - + online_receipt = online_receipts[0] ( persisted_challenge, persisted_challenge_raw, authorization_context_id, ) = _load_persisted_apply_challenge( state_directory, - online_receipts[0], + online_receipt, manifest_sha256=manifest_sha256, approval=approval, approval_sha256=approval_sha256, ) + challenge_payload = persisted_challenge["payload"] + legal_review_binding_sha256 = challenge_payload[ + "legal_source_review_binding_sha256" + ] + legal_review_expires_at = challenge_payload[ + "legal_source_review_expires_at" + ] + environment_id = challenge_payload["environment_id"] + tenant_id = challenge_payload["tenant_id"] + try: + batch.validate_online_transaction_receipt( + validated_manifest, + manifest_sha256, + approval_sha256, + approval, + apply_service.public_key, + online_public_key, + online_receipt, + legal_review_binding_sha256=legal_review_binding_sha256, + legal_review_expires_at=legal_review_expires_at, + environment_id=environment_id, + tenant_id=tenant_id, + ) + batch.validate_runtime_snapshot_receipt( + validated_manifest, + manifest_sha256, + approval_sha256, + approval, + apply_service.public_key, + online_public_key, + online_receipt, + runtime_receipt, + legal_review_binding_sha256=legal_review_binding_sha256, + legal_review_expires_at=legal_review_expires_at, + environment_id=environment_id, + tenant_id=tenant_id, + ) + except batch.PublicationBatchError as exc: + raise AuthorityError(f"online/runtime receipt pair is untrusted: {exc}") from exc consumption_identity = _apply_consumption_identity( challenge=persisted_challenge, challenge_raw=persisted_challenge_raw, - online_receipt=online_receipts[0], + online_receipt=online_receipt, + runtime_receipt=runtime_receipt, output_directory=output_directory, manifest_sha256=manifest_sha256, approval_sha256=approval_sha256, authorization_context_id=authorization_context_id, ) + def build_bundle( + receipt: Mapping[str, Any], receipt_raw: bytes + ) -> dict[str, Any]: + receipt_index = [ + { + "filename": "apply-receipt-001.json", + "receipt_id": receipt["receipt_id"], + "payload_sha256": batch._sha256_json(receipt["payload"]), + "raw_sha256": batch._sha256_bytes(receipt_raw), + } + ] + return { + "schema_version": APPLY_RECEIPT_BUNDLE_SCHEMA, + "candidate_commit": validated_manifest["candidate"]["commit"], + "candidate_tag": validated_manifest["candidate"]["tag"], + "manifest_raw_sha256": manifest_sha256, + "manifest_payload_sha256": validated_manifest[ + "manifest_payload_sha256" + ], + "approval_raw_sha256": approval_sha256, + "approval_keyring_payload_sha256": keyring[ + "keyring_payload_sha256" + ], + "legal_source_review_binding_sha256": legal_review_binding_sha256, + "legal_source_review_expires_at": legal_review_expires_at, + "environment_id": environment_id, + "tenant_id": tenant_id, + "idempotency_key": challenge_payload["idempotency_key"], + "transaction_id": challenge_payload["transaction_id"], + "transaction_mode": batch.TRANSACTION_MODE, + "cross_database_atomicity": False, + "online_transaction_input_raw_sha256": ( + online_transaction_receipts_sha256 + ), + "runtime_snapshot_input_raw_sha256": runtime_snapshot_receipt_sha256, + "online_transaction_service_provider": ( + batch.TRUSTED_ONLINE_APPLY_SERVICE_PROVIDER + ), + "online_transaction_service_key_reference": ( + batch.TRUSTED_ONLINE_APPLY_SERVICE_KEY_REFERENCE + ), + "online_transaction_service_key_version": ( + batch.TRUSTED_ONLINE_APPLY_SERVICE_KEY_VERSION + ), + "online_transaction_service_algorithm": ( + batch.TRUSTED_ONLINE_APPLY_SERVICE_ALGORITHM + ), + "online_transaction_service_public_key_spki_sha256": ( + batch.TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SPKI_SHA256 + ), + "apply_service_public_key_sha256": apply_service.public_sha256, + "apply_challenge_id": persisted_challenge["challenge_id"], + "authorization_context_id": authorization_context_id, + "apply_challenge_raw_sha256": batch._sha256_bytes( + persisted_challenge_raw + ), + "receipt_count": 1, + "receipts": receipt_index, + "private_keys_in_bundle": False, + "business_api_calls_by_this_tool": 0, + "trusted_online_transaction_receipt_count": 1, + "trusted_runtime_snapshot_receipt_count": 1, + } + if output_directory.exists() or output_directory.is_symlink(): consumption, consumption_path = _load_or_reserve_apply_consumption( state_directory, @@ -2030,62 +2343,31 @@ def sign_apply_payloads( online_public_key, approval_sha256, existing_receipt, + legal_review_binding_sha256=legal_review_binding_sha256, + legal_review_expires_at=legal_review_expires_at, + environment_id=environment_id, + tenant_id=tenant_id, ) except batch.PublicationBatchError as exc: raise AuthorityError( f"existing apply receipt failed formal verification: {exc}" ) from exc - if existing_receipt["payload"] != verified_payloads[0]: + if ( + existing_receipt["payload"]["online_transaction_receipt"] + != online_receipt + or existing_receipt["payload"]["runtime_snapshot_receipt"] + != runtime_receipt + ): raise AuthorityError( - "existing apply receipt is not bound to the supplied online transaction" + "existing apply receipt is not bound to the supplied receipt pair" ) receipt_raw = _canonical(existing_receipt) - receipt_index = [ - { - "filename": "apply-receipt-001.json", - "receipt_id": existing_receipt["receipt_id"], - "payload_sha256": batch._sha256_json(verified_payloads[0]), - "raw_sha256": batch._sha256_bytes(receipt_raw), - } - ] existing_bundle = _read_json( output_directory / "apply-receipt-bundle.json", "existing apply receipt bundle index", maximum_bytes=4 * 1024 * 1024, ) - expected_bundle = { - "schema_version": APPLY_RECEIPT_BUNDLE_SCHEMA, - "candidate_commit": validated_manifest["candidate"]["commit"], - "candidate_tag": validated_manifest["candidate"]["tag"], - "manifest_raw_sha256": manifest_sha256, - "manifest_payload_sha256": validated_manifest[ - "manifest_payload_sha256" - ], - "approval_raw_sha256": approval_sha256, - "approval_keyring_payload_sha256": keyring[ - "keyring_payload_sha256" - ], - "online_transaction_input_raw_sha256": ( - online_transaction_receipts_sha256 - ), - "online_transaction_service_public_key_sha256": batch._sha256_bytes( - online_public_key.public_bytes( - serialization.Encoding.Raw, - serialization.PublicFormat.Raw, - ) - ), - "apply_service_public_key_sha256": apply_service.public_sha256, - "apply_challenge_id": persisted_challenge["challenge_id"], - "authorization_context_id": authorization_context_id, - "apply_challenge_raw_sha256": batch._sha256_bytes( - persisted_challenge_raw - ), - "receipt_count": 1, - "receipts": receipt_index, - "private_keys_in_bundle": False, - "business_api_calls_by_this_tool": 0, - "trusted_online_transaction_receipt_count": 1, - } + expected_bundle = build_bundle(existing_receipt, receipt_raw) if existing_bundle != expected_bundle: raise AuthorityError("existing apply receipt bundle identity drift") existing_bundle_raw = batch.secure_read_bytes( @@ -2109,30 +2391,34 @@ def sign_apply_payloads( **expected_bundle, } - if current < approved_at or current >= expires_at: + legal_expires_at = _parse_time( + legal_review_expires_at, "legal review expires_at" + ) + if ( + current < approved_at + or current >= expires_at + or current >= legal_expires_at + ): raise AuthorityError( "current time is outside the approval window; no apply receipt was signed" ) - for online_receipt in online_receipts: - committed_at = _parse_time( - online_receipt["payload"]["committed_at"], - "online transaction committed_at", - ) - if committed_at > current: - raise AuthorityError( - "online transaction receipt is dated after the current signing time" - ) - challenge_payload = persisted_challenge["payload"] - challenge_issued = _parse_time( - challenge_payload["issued_at"], "apply challenge issued_at" + committed_at = _parse_time( + online_receipt["payload"]["committed_at"], + "online transaction committed_at", ) - challenge_expires = _parse_time( - challenge_payload["expires_at"], "apply challenge expires_at" + runtime_captured_at = _parse_time( + runtime_receipt["payload"]["captured_at"], + "runtime snapshot captured_at", ) - if current < challenge_issued or current >= challenge_expires: + if committed_at > current or runtime_captured_at > current: raise AuthorityError( - "current time is outside the short-lived apply challenge window" + "online/runtime receipt is dated after the current signing time" ) + # The short-lived challenge authorizes the online commit/read-back + # timestamps, which the receipt validators already constrain. Once + # committed, an expired challenge must not strand a truthful saga by + # preventing later offline countersign. Approval validity and one-time + # challenge consumption still apply at countersign time. consumption, consumption_path = _load_or_reserve_apply_consumption( state_directory, consumption_identity, @@ -2153,63 +2439,35 @@ def sign_apply_payloads( if signing_apply_service.public_sha256 != apply_service.public_sha256: raise AuthorityError("apply-service identity changed before signing") - receipt_files: dict[str, bytes] = {} - receipt_index: list[dict[str, Any]] = [] - for index, payload in enumerate(verified_payloads, start=1): - receipt = _build_apply_receipt(payload, signing_apply_service) - try: - batch.validate_apply_receipt( - validated_manifest, - manifest_sha256, - approval, - keyring, - root_public_key, - signing_apply_service.public_key, - online_public_key, - approval_sha256, - receipt, - ) - except batch.PublicationBatchError as exc: - raise AuthorityError(f"apply payload {index} failed formal verification: {exc}") from exc - filename = f"apply-receipt-{index:03d}.json" - receipt_raw = _canonical(receipt) - receipt_files[filename] = receipt_raw - receipt_index.append( - { - "filename": filename, - "receipt_id": receipt["receipt_id"], - "payload_sha256": batch._sha256_json(payload), - "raw_sha256": batch._sha256_bytes(receipt_raw), - } + payload = _apply_payload_from_online_receipt( + online_receipt, + runtime_receipt, + countersigned_at=current, + ) + receipt = _build_apply_receipt(payload, signing_apply_service) + try: + batch.validate_apply_receipt( + validated_manifest, + manifest_sha256, + approval, + keyring, + root_public_key, + signing_apply_service.public_key, + online_public_key, + approval_sha256, + receipt, + legal_review_binding_sha256=legal_review_binding_sha256, + legal_review_expires_at=legal_review_expires_at, + environment_id=environment_id, + tenant_id=tenant_id, ) - - bundle = { - "schema_version": APPLY_RECEIPT_BUNDLE_SCHEMA, - "candidate_commit": validated_manifest["candidate"]["commit"], - "candidate_tag": validated_manifest["candidate"]["tag"], - "manifest_raw_sha256": manifest_sha256, - "manifest_payload_sha256": validated_manifest["manifest_payload_sha256"], - "approval_raw_sha256": approval_sha256, - "approval_keyring_payload_sha256": keyring["keyring_payload_sha256"], - "online_transaction_input_raw_sha256": online_transaction_receipts_sha256, - "online_transaction_service_public_key_sha256": batch._sha256_bytes( - online_public_key.public_bytes( - serialization.Encoding.Raw, - serialization.PublicFormat.Raw, - ) - ), - "apply_service_public_key_sha256": apply_service.public_sha256, - "apply_challenge_id": persisted_challenge["challenge_id"], - "authorization_context_id": authorization_context_id, - "apply_challenge_raw_sha256": batch._sha256_bytes( - persisted_challenge_raw - ), - "receipt_count": len(receipt_index), - "receipts": receipt_index, - "private_keys_in_bundle": False, - "business_api_calls_by_this_tool": 0, - "trusted_online_transaction_receipt_count": len(online_receipts), - } + except batch.PublicationBatchError as exc: + raise AuthorityError( + f"apply payload failed formal verification: {exc}" + ) from exc + receipt_raw = _canonical(receipt) + receipt_files = {"apply-receipt-001.json": receipt_raw} + bundle = build_bundle(receipt, receipt_raw) expected_files = { **receipt_files, "apply-receipt-bundle.json": _canonical(bundle), @@ -2856,6 +3114,7 @@ def plan_pin_updates( expected_current_epoch: int | None = None, online_service_registration_file: Path | None = None, online_environment_ids: tuple[str, ...] = (), + legal_review_keyring_file: Path | None = None, ) -> PinPlan: epoch = _ensure_epoch(epoch) if expected_current_epoch is not None: @@ -2873,6 +3132,7 @@ def plan_pin_updates( "online service registration and environment allowlist must be supplied together" ) online_pin: str | None = None + online_registration: dict[str, Any] | None = None rendered_online_environments: str | None = None normalized_online_environments: tuple[str, ...] = () if online_service_registration_file is not None: @@ -2887,13 +3147,13 @@ def plan_pin_updates( ) ): raise AuthorityError("online publication environment allowlist is invalid") - _registration, _online_public, online_raw = load_online_service_registration( + online_registration, _online_public, online_spki = load_online_service_registration( online_service_registration_file ) - online_fingerprint = batch._sha256_bytes(online_raw) - if online_raw in {item.public_raw for item in keys.values()} or online_fingerprint == ( - keys[ROLE_RC06_ROOT].reviewer_binding["public_key_raw_sha256"] - ): + online_fingerprint = batch._sha256_bytes(online_spki) + if online_spki in { + batch._public_key_spki(item.public_key) for item in keys.values() + }: raise AuthorityError( "online publication service key must differ from reviewer and authority keys" ) @@ -2902,6 +3162,32 @@ def plan_pin_updates( rendered_online_environments = _render_environment_allowlist( normalized_online_environments ) + legal_review_pin: str | None = None + if legal_review_keyring_file is not None: + try: + legal_raw = batch.secure_read_bytes( + legal_review_keyring_file, + "scene legal review keyring", + maximum_bytes=MAX_REGISTRATION_BYTES, + ) + legal_keyring = json.loads( + legal_raw, + object_pairs_hook=batch._reject_duplicate_json_pairs, + parse_constant=batch._reject_json_constant, + ) + legal_fingerprint = batch._sha256_bytes(legal_raw) + batch.validate_scene_legal_review_keyring( + legal_keyring, + legal_fingerprint, + expected_sha256=legal_fingerprint, + ) + except ( + UnicodeDecodeError, + json.JSONDecodeError, + batch.PublicationBatchError, + ) as exc: + raise AuthorityError(f"scene legal review keyring is invalid: {exc}") from exc + legal_review_pin = _quoted_pin(legal_fingerprint) receipt = _read_json( recovery_keyring_directory / "recovery-keyring-bundle.json", "recovery keyring receipt", @@ -2998,14 +3284,36 @@ def plan_pin_updates( allowed_pin(apply_pin), ), ] - if online_pin is not None and rendered_online_environments is not None: + if ( + online_pin is not None + and rendered_online_environments is not None + and online_registration is not None + ): + online_metadata = { + "TRUSTED_ONLINE_APPLY_SERVICE_PROVIDER": json.dumps( + online_registration["provider"] + ), + "TRUSTED_ONLINE_APPLY_SERVICE_KEY_REFERENCE": json.dumps( + online_registration["key_reference"] + ), + "TRUSTED_ONLINE_APPLY_SERVICE_KEY_VERSION": json.dumps( + online_registration["key_version"] + ), + "TRUSTED_ONLINE_APPLY_SERVICE_ALGORITHM": json.dumps( + online_registration["algorithm"] + ), + "TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SPKI_SHA256": online_pin, + } rc06_replacements.extend( [ ( - "TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SHA256", - online_pin, - lambda current, desired=online_pin: current in {'""', desired}, - ), + name, + rendered, + lambda current, desired=rendered: current in {'""', desired}, + ) + for name, rendered in online_metadata.items() + ] + + [ ( "TRUSTED_ONLINE_APPLY_ENVIRONMENT_IDS", rendered_online_environments, @@ -3022,10 +3330,17 @@ def plan_pin_updates( rc06_replacements.extend( [ ( - "TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SHA256", - '""', - lambda current: current == '""', - ), + name, '""', lambda current: current == '""' + ) + for name in ( + "TRUSTED_ONLINE_APPLY_SERVICE_PROVIDER", + "TRUSTED_ONLINE_APPLY_SERVICE_KEY_REFERENCE", + "TRUSTED_ONLINE_APPLY_SERVICE_KEY_VERSION", + "TRUSTED_ONLINE_APPLY_SERVICE_ALGORITHM", + "TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SPKI_SHA256", + ) + ] + + [ ( "TRUSTED_ONLINE_APPLY_ENVIRONMENT_IDS", "()", @@ -3033,6 +3348,22 @@ def plan_pin_updates( ), ] ) + if legal_review_pin is not None: + rc06_replacements.append( + ( + "TRUSTED_SCENE_LEGAL_REVIEW_KEYRING_SHA256", + legal_review_pin, + lambda current, desired=legal_review_pin: current in {'""', desired}, + ) + ) + else: + rc06_replacements.append( + ( + "TRUSTED_SCENE_LEGAL_REVIEW_KEYRING_SHA256", + '""', + lambda current: current == '""', + ) + ) changes = ( _prepare_source_change( @@ -3115,6 +3446,33 @@ def plan_pin_updates( "epoch": epoch, "online_apply_adapter_pinned": online_pin is not None, "online_apply_environment_ids": normalized_online_environments, + "online_apply_provider": ( + online_registration["provider"] + if online_registration is not None + else None + ), + "online_apply_key_reference": ( + online_registration["key_reference"] + if online_registration is not None + else None + ), + "online_apply_key_version": ( + online_registration["key_version"] + if online_registration is not None + else None + ), + "online_apply_algorithm": ( + online_registration["algorithm"] + if online_registration is not None + else None + ), + "online_apply_public_key_spki_sha256": ( + online_pin.strip('"') if online_pin is not None else None + ), + "scene_legal_review_keyring_pinned": legal_review_pin is not None, + "scene_legal_review_keyring_sha256": ( + legal_review_pin.strip('"') if legal_review_pin is not None else None + ), }, ) @@ -3758,6 +4116,10 @@ def apply_pin_plan(plan: PinPlan) -> dict[str, Any]: raise AuthorityError( "online apply adapter trust is not provisioned; source pin apply is blocked" ) + if plan.values.get("scene_legal_review_keyring_pinned") is not True: + raise AuthorityError( + "scene legal review keyring trust is not provisioned; source pin apply is blocked" + ) changed = [change for change in plan.changes if change.original != change.proposed] with _repository_pin_lock(plan.repo, plan.state_directory) as control: journal_path = control / "transaction.json" @@ -3936,6 +4298,7 @@ def _parser() -> argparse.ArgumentParser: pin.add_argument("--expected-current-epoch", type=int) pin.add_argument("--online-service-registration", type=Path) pin.add_argument("--online-environment", action="append", default=[]) + pin.add_argument("--legal-review-keyring", type=Path) pin.add_argument("--apply", action="store_true") recover_pin = commands.add_parser( "recover-pin-source", @@ -3963,7 +4326,14 @@ def _parser() -> argparse.ArgumentParser: challenge.add_argument("--approval-keyring", type=Path, required=True) challenge.add_argument("--approval-keyring-sha256", required=True) challenge.add_argument("--approval-root-public-key", type=Path, required=True) + challenge.add_argument("--legal-review-request", type=Path, required=True) + challenge.add_argument("--legal-review-request-sha256", required=True) + challenge.add_argument("--legal-review-decision", type=Path, required=True) + challenge.add_argument("--legal-review-decision-sha256", required=True) + challenge.add_argument("--legal-review-keyring", type=Path, required=True) + challenge.add_argument("--legal-review-keyring-sha256", required=True) challenge.add_argument("--environment", required=True) + challenge.add_argument("--tenant-id", required=True) apply_sign = commands.add_parser( "sign-apply-payloads", help="sign verifier-ready apply payloads and immediately self-verify every receipt", @@ -3979,6 +4349,8 @@ def _parser() -> argparse.ArgumentParser: apply_sign.add_argument("--online-service-public-key", type=Path, required=True) apply_sign.add_argument("--online-transaction-receipts", type=Path, required=True) apply_sign.add_argument("--online-transaction-receipts-sha256", required=True) + apply_sign.add_argument("--runtime-snapshot-receipt", type=Path, required=True) + apply_sign.add_argument("--runtime-snapshot-receipt-sha256", required=True) apply_sign.add_argument("--output", type=Path, required=True) provision = commands.add_parser( "provision-apply-private-key", @@ -4036,6 +4408,7 @@ def main(argv: list[str] | None = None) -> int: expected_current_epoch=args.expected_current_epoch, online_service_registration_file=args.online_service_registration, online_environment_ids=tuple(args.online_environment), + legal_review_keyring_file=args.legal_review_keyring, ) result = apply_pin_plan(plan) if args.apply else pin_plan_summary(plan) elif args.command == "recover-pin-source": @@ -4062,6 +4435,10 @@ def main(argv: list[str] | None = None) -> int: online_transaction_receipts_sha256=( args.online_transaction_receipts_sha256 ), + runtime_snapshot_receipt_file=args.runtime_snapshot_receipt, + runtime_snapshot_receipt_sha256=( + args.runtime_snapshot_receipt_sha256 + ), output_directory=args.output, ) elif args.command == "issue-apply-challenge": @@ -4074,7 +4451,14 @@ def main(argv: list[str] | None = None) -> int: approval_keyring_file=args.approval_keyring, approval_keyring_sha256=args.approval_keyring_sha256, approval_root_public_key_file=args.approval_root_public_key, + legal_review_request_file=args.legal_review_request, + legal_review_request_sha256=args.legal_review_request_sha256, + legal_review_decision_file=args.legal_review_decision, + legal_review_decision_sha256=args.legal_review_decision_sha256, + legal_review_keyring_file=args.legal_review_keyring, + legal_review_keyring_sha256=args.legal_review_keyring_sha256, environment_id=args.environment, + tenant_id=args.tenant_id, ) else: raise AuthorityError( diff --git a/yuqei-infra/tests/test_rc06_publication_apply.py b/yuqei-infra/tests/test_rc06_publication_apply.py index 6a80143a..154f88e1 100644 --- a/yuqei-infra/tests/test_rc06_publication_apply.py +++ b/yuqei-infra/tests/test_rc06_publication_apply.py @@ -2,6 +2,7 @@ from __future__ import annotations import base64 import copy +import hashlib import importlib.util import json import sqlite3 @@ -13,7 +14,8 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path import pytest -from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import padding, rsa, utils from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey @@ -39,19 +41,71 @@ def _raw_public(key) -> bytes: return key.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) -def _sign_envelope(private_key, *, schema: str, id_prefix: str, domain: bytes, payload: dict) -> dict: +def _spki(key) -> bytes: + return key.public_bytes( + serialization.Encoding.DER, + serialization.PublicFormat.SubjectPublicKeyInfo, + ) + + +def _ed_envelope( + private_key, + *, + schema: str, + id_field: str, + id_prefix: str, + domain: bytes, + payload: dict, +) -> dict: signature = private_key.sign(domain + apply.batch._canonical(payload)) return { "schema_version": schema, - "receipt_id": id_prefix + id_field: id_prefix + apply.batch._sha256_bytes(apply.batch._canonical(payload) + signature)[:24], "payload": payload, "signature": base64.b64encode(signature).decode("ascii"), } +def _kms_envelope( + private_key, + *, + schema: str, + id_prefix: str, + domain: bytes, + payload: dict, + request_id: str, +) -> dict: + digest = hashlib.sha256(domain + apply.batch._canonical(payload)).digest() + signature_value = private_key.sign( + digest, + padding.PSS( + mgf=padding.MGF1(hashes.SHA256()), + salt_length=hashes.SHA256().digest_size, + ), + utils.Prehashed(hashes.SHA256()), + ) + signature = { + "provider": "fixture-kms", + "key_reference": "kms://fixture/rc06-online", + "key_version": "version-1", + "algorithm": "RSA_PSS_SHA_256", + "value_base64": base64.b64encode(signature_value).decode("ascii"), + "request_id": request_id, + } + return { + "schema_version": schema, + "receipt_id": id_prefix + + apply.batch._sha256_bytes( + apply.batch._canonical(payload) + apply.batch._canonical(signature) + )[:24], + "payload": payload, + "signature": signature, + } + + class _FakeCanonicalLegalReviewValidator: - """Test double for build-scene-source-legal-review.py; never production evidence.""" + """Protocol fixture only; it never stands in for a real legal reviewer.""" @staticmethod def verify_request(request: dict, _repo: Path) -> dict: @@ -68,9 +122,9 @@ class _FakeCanonicalLegalReviewValidator: ) -> dict: if decision.get("request_sha256") != request["request_sha256"]: raise ValueError("decision request binding drift") - now = datetime.fromisoformat(checked_at) - expires_at = datetime.fromisoformat(decision["expires_at"]) - if now > expires_at: + if datetime.fromisoformat(checked_at) > datetime.fromisoformat( + decision["expires_at"] + ): raise ValueError("decision expired") expected = { item["occurrence_id"]: item["source_record_sha256"] @@ -109,7 +163,8 @@ def _material(monkeypatch: pytest.MonkeyPatch, now: datetime) -> dict: root_private = Ed25519PrivateKey.generate() reviewer_private = Ed25519PrivateKey.generate() apply_private = Ed25519PrivateKey.generate() - online_private = Ed25519PrivateKey.generate() + legal_private = Ed25519PrivateKey.generate() + online_private = rsa.generate_private_key(public_exponent=65537, key_size=2048) monkeypatch.setattr( apply.batch, "TRUSTED_APPROVAL_ROOT_PUBLIC_KEY_SHA256", @@ -121,21 +176,30 @@ def _material(monkeypatch: pytest.MonkeyPatch, now: datetime) -> dict: "TRUSTED_APPLY_SERVICE_PUBLIC_KEY_SHA256", apply.batch._sha256_bytes(_raw_public(apply_private.public_key())), ) + monkeypatch.setattr(apply.batch, "TRUSTED_ONLINE_APPLY_SERVICE_PROVIDER", "fixture-kms") monkeypatch.setattr( apply.batch, - "TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SHA256", - apply.batch._sha256_bytes(_raw_public(online_private.public_key())), + "TRUSTED_ONLINE_APPLY_SERVICE_KEY_REFERENCE", + "kms://fixture/rc06-online", + ) + monkeypatch.setattr( + apply.batch, "TRUSTED_ONLINE_APPLY_SERVICE_KEY_VERSION", "version-1" + ) + monkeypatch.setattr( + apply.batch, "TRUSTED_ONLINE_APPLY_SERVICE_ALGORITHM", "RSA_PSS_SHA_256" ) monkeypatch.setattr( apply.batch, - "TRUSTED_ONLINE_APPLY_ENVIRONMENT_IDS", - ("rc06-test",), + "TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SPKI_SHA256", + apply.batch._sha256_bytes(_spki(online_private.public_key())), ) monkeypatch.setattr( - apply, - "_load_scene_legal_review_module", - lambda: _FakeCanonicalLegalReviewValidator, + apply.batch, "TRUSTED_ONLINE_APPLY_ENVIRONMENT_IDS", ("rc06-test",) ) + monkeypatch.setattr( + apply, "_load_scene_legal_review_module", lambda: _FakeCanonicalLegalReviewValidator + ) + reviewer_pem = reviewer_private.public_key().public_bytes( serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo, @@ -159,7 +223,7 @@ def _material(monkeypatch: pytest.MonkeyPatch, now: datetime) -> dict: } ], } - keyring = { + approval_keyring = { **keyring_unsigned, "keyring_payload_sha256": apply.batch._sha256_json(keyring_unsigned), "root_signature_base64": base64.b64encode( @@ -174,30 +238,27 @@ def _material(monkeypatch: pytest.MonkeyPatch, now: datetime) -> dict: manifest_raw_sha256=manifest_raw, key_id="rc06-release-owner-test", approval_keyring_epoch=1, - approval_keyring_payload_sha256=keyring["keyring_payload_sha256"], + approval_keyring_payload_sha256=approval_keyring["keyring_payload_sha256"], approved_at=now - timedelta(hours=2), expires_at=now + timedelta(days=2), approver={ **apply.batch.SOLE_RELEASE_REVIEWER, "role": "测试发布负责人", "organization": "测试组织(非真实法律服务主体)", - "authorization_scope": "仅批准本临时 SQLite/HTTP 测试候选,不授权生产发布", + "authorization_scope": "仅批准本协议测试候选,不授权生产发布", "human_confirmed": True, }, ) - approval_signature = reviewer_private.sign( - apply.batch.APPROVAL_SIGNATURE_DOMAIN + apply.batch._canonical(approval_payload) + approval = _ed_envelope( + reviewer_private, + schema=apply.batch.APPROVAL_SCHEMA_VERSION, + id_field="approval_id", + id_prefix="rc06-batch-approval-", + domain=apply.batch.APPROVAL_SIGNATURE_DOMAIN, + payload=approval_payload, ) - approval = { - "schema_version": apply.batch.APPROVAL_SCHEMA_VERSION, - "approval_id": "rc06-batch-approval-" - + apply.batch._sha256_bytes( - apply.batch._canonical(approval_payload) + approval_signature - )[:24], - "payload": approval_payload, - "signature": base64.b64encode(approval_signature).decode("ascii"), - } approval_raw = apply.batch._sha256_bytes(apply.batch._canonical(approval)) + occurrences = [] for asset in sorted( manifest["asset_batch"]["items"], key=lambda item: item["template_id"] @@ -233,7 +294,7 @@ def _material(monkeypatch: pytest.MonkeyPatch, now: datetime) -> dict: "distinct_source_record_count": 8, "distinct_url_count": 7, }, - "source_files": [], + "source_files": ["fixture-only/not-production.json"], "occurrences": occurrences, "guardrails": { "not_legal_approval": True, @@ -243,12 +304,13 @@ def _material(monkeypatch: pytest.MonkeyPatch, now: datetime) -> dict: }, } legal_request["request_sha256"] = apply.batch._sha256_json(legal_request) + legal_expires_at = (now + timedelta(days=1)).isoformat(timespec="seconds") legal_decision = { "schema_version": "yuqei.scene-source-legal-review-decision.v1", "request_sha256": legal_request["request_sha256"], "candidate_commit": manifest["candidate"]["commit"], "reviewed_at": (now - timedelta(hours=1)).isoformat(timespec="seconds"), - "expires_at": (now + timedelta(days=1)).isoformat(timespec="seconds"), + "expires_at": legal_expires_at, "reviewer": { "name": "测试法源复核人(非真实身份)", "organization": "测试组织(非真实法律服务主体)", @@ -275,18 +337,92 @@ def _material(monkeypatch: pytest.MonkeyPatch, now: datetime) -> dict: } legal_keyring = { "schema_version": "yuqei.scene-source-legal-review-keyring.v1", - "keys": [{"key_id": "fixture-legal-reviewer", "status": "trusted"}], + "keys": [ + { + "key_id": "fixture-legal-reviewer", + "status": "trusted", + "revoked_at": None, + "valid_from": (now - timedelta(days=2)).isoformat(timespec="seconds"), + "valid_until": (now + timedelta(days=2)).isoformat(timespec="seconds"), + "reviewer_name": "测试法源复核人(非真实身份)", + "reviewer_organization": "测试组织(非真实法律服务主体)", + "reviewer_qualification": "测试资格声明(不代表真实法律资质)", + "credential_reference": "TEST-FIXTURE-NOT-A-CREDENTIAL", + "public_key_raw_base64": base64.b64encode( + _raw_public(legal_private.public_key()) + ).decode("ascii"), + } + ], } + legal_keyring_raw = apply.batch._sha256_bytes(apply.batch._canonical(legal_keyring)) + monkeypatch.setattr( + apply.batch, "TRUSTED_SCENE_LEGAL_REVIEW_KEYRING_SHA256", legal_keyring_raw + ) + legal_binding = apply.batch._sha256_json( + { + "request_sha256": legal_request["request_sha256"], + "decision_sha256": apply.batch._sha256_json(legal_decision), + "keyring_raw_sha256": legal_keyring_raw, + } + ) + environment_id = "rc06-test" + tenant_id = "tenant-fixture" + idempotency_key = apply.batch.publication_idempotency_key( + manifest_raw, + approval_raw, + legal_binding, + environment_id, + tenant_id, + ) + transaction_id = apply.batch.publication_transaction_id(idempotency_key) + challenge_payload = { + "schema_version": apply.batch.APPLY_CHALLENGE_PAYLOAD_SCHEMA_VERSION, + "nonce_base64": base64.b64encode(b"q" * 32).decode("ascii"), + "candidate_commit": manifest["candidate"]["commit"], + "candidate_tag": manifest["candidate"]["tag"], + "batch_manifest_raw_sha256": manifest_raw, + "batch_manifest_payload_sha256": manifest["manifest_payload_sha256"], + "approval_receipt_sha256": approval_raw, + "legal_source_review_binding_sha256": legal_binding, + "legal_source_review_expires_at": legal_expires_at, + "operation": apply.batch.APPLY_OPERATION, + "environment_id": environment_id, + "tenant_id": tenant_id, + "idempotency_key": idempotency_key, + "transaction_id": transaction_id, + "actor": { + "type": "system", + "component": apply.batch.TRUSTED_ONLINE_APPLY_SERVICE_COMPONENT, + }, + "issued_at": (now - timedelta(minutes=1)).isoformat(timespec="seconds"), + "expires_at": (now + timedelta(minutes=5)).isoformat(timespec="seconds"), + } + authority_challenge = _ed_envelope( + apply_private, + schema=apply.batch.APPLY_CHALLENGE_SCHEMA_VERSION, + id_field="challenge_id", + id_prefix="rc06-apply-challenge-", + domain=apply.batch.APPLY_CHALLENGE_SIGNATURE_DOMAIN, + payload=challenge_payload, + ) return { "now": now, "manifest": manifest, "manifest_raw": manifest_raw, "approval": approval, "approval_raw": approval_raw, - "keyring": keyring, + "approval_keyring": approval_keyring, "legal_request": legal_request, "legal_decision": legal_decision, "legal_keyring": legal_keyring, + "legal_keyring_raw": legal_keyring_raw, + "legal_binding": legal_binding, + "legal_expires_at": legal_expires_at, + "environment_id": environment_id, + "tenant_id": tenant_id, + "idempotency_key": idempotency_key, + "transaction_id": transaction_id, + "authority_challenge": authority_challenge, "root_private": root_private, "apply_private": apply_private, "online_private": online_private, @@ -294,125 +430,85 @@ def _material(monkeypatch: pytest.MonkeyPatch, now: datetime) -> dict: class FakePublicationService: + """Protocol-only double; it is not evidence of a real two-database UAT.""" + def __init__( self, - database: Path, - material: dict, *, - fail_after: int | None = None, - tamper_status: bool = False, - tamper_runtime_status: bool = False, + manifest: dict, + now: datetime, + online_private, + fail_before_commit: bool = False, + tamper_status: str | None = None, + fail_finalize: bool = False, ) -> None: - self.database = database - self.material = material - self.fail_after = fail_after + self.manifest = manifest + self.now = now + self.online_private = online_private + self.fail_before_commit = fail_before_commit self.tamper_status = tamper_status - self.tamper_runtime_status = tamper_runtime_status + self.fail_finalize = fail_finalize self.apply_calls = 0 - with sqlite3.connect(database) as connection: - connection.executescript( - """ - CREATE TABLE transactions ( - transaction_id TEXT PRIMARY KEY, - idempotency_key TEXT NOT NULL UNIQUE, - state TEXT NOT NULL, - legal_review_sha256 TEXT NOT NULL, - response_json TEXT - ); - CREATE TABLE publication_items ( - transaction_id TEXT NOT NULL, - kind TEXT NOT NULL, - item_id TEXT NOT NULL, - content_sha256 TEXT NOT NULL, - PRIMARY KEY(transaction_id, kind, item_id) - ); - CREATE TABLE runtime_snapshots ( - transaction_id TEXT NOT NULL, - kind TEXT NOT NULL, - runtime_key TEXT NOT NULL, - content_sha256 TEXT NOT NULL, - status TEXT NOT NULL, - PRIMARY KEY(transaction_id, runtime_key) - ); - """ - ) + self.rollback_calls = 0 + self.finalize_calls = 0 + self.request: dict | None = None + self.response: dict | None = None + self.state: str | None = None + self.finalize_request: dict | None = None + # The fake online service is deliberately incapable of minting the + # offline authority receipt. + assert not hasattr(self, "apply_private") def _receipt_pair(self, request: dict) -> tuple[dict, dict]: - material = self.material - now = material["now"] - challenge_payload = { - "schema_version": apply.batch.APPLY_CHALLENGE_PAYLOAD_SCHEMA_VERSION, - "nonce_base64": base64.b64encode(b"q" * 32).decode("ascii"), - "candidate_commit": request["manifest"]["candidate"]["commit"], - "candidate_tag": request["manifest"]["candidate"]["tag"], - "batch_manifest_raw_sha256": request["batch_manifest_raw_sha256"], - "batch_manifest_payload_sha256": request["batch_manifest_payload_sha256"], - "approval_receipt_sha256": request["approval_receipt_sha256"], - "operation": apply.batch.APPLY_OPERATION, - "environment_id": request["environment_id"], - "actor": { - "type": "system", - "component": apply.batch.TRUSTED_ONLINE_APPLY_SERVICE_COMPONENT, - }, - "issued_at": (now - timedelta(minutes=1)).isoformat(timespec="seconds"), - "expires_at": (now + timedelta(minutes=5)).isoformat(timespec="seconds"), + manifest = request["manifest"] + item_results = request["expected_publication_item_results"] + images = { + item["role"]: item["manifest_digest"] + for item in manifest["candidate"]["images"] } - challenge = _sign_envelope( - material["apply_private"], - schema=apply.batch.APPLY_CHALLENGE_SCHEMA_VERSION, - id_prefix="rc06-apply-challenge-", - domain=apply.batch.APPLY_CHALLENGE_SIGNATURE_DOMAIN, - payload=challenge_payload, + specs = ( + ( + "ai-platform", + self.now - timedelta(seconds=30), + 14, + 14, + images["ai-platform-api"], + [item for item in item_results if item["kind"] == "prompt_module"], + ), + ( + "contract-platform", + self.now - timedelta(seconds=20), + 60, + 0, + images["contract-api"], + [item for item in item_results if item["kind"] == "asset"], + ), ) - challenge["challenge_id"] = challenge.pop("receipt_id") - committed_at = now.isoformat(timespec="seconds") + participants = [] + for name, committed_at, count, runtime_count, image, results in specs: + projection = { + "participant": name, + "state": "committed", + "transaction_id": request["transaction_id"], + "candidate_commit": manifest["candidate"]["commit"], + "release_image_digest": image, + "committed_at": committed_at.isoformat(timespec="seconds"), + "write_count": count, + "runtime_snapshot_count": runtime_count, + "committed_item_results_sha256": apply.batch._sha256_json(results), + } + participants.append( + { + **projection, + "durable_commit_evidence_sha256": apply.batch._sha256_json( + projection + ), + } + ) online_payload = { "schema_version": apply.batch.ONLINE_TRANSACTION_PAYLOAD_SCHEMA_VERSION, - "key_id": apply.batch.TRUSTED_ONLINE_APPLY_SERVICE_KEY_ID, - "batch_manifest_payload_sha256": request["batch_manifest_payload_sha256"], - "approval_receipt_sha256": request["approval_receipt_sha256"], - "environment_id": request["environment_id"], - "transaction_id": request["transaction_id"], - "database_commit_id": "sqlite-" + request["idempotency_key"][:24], - "committed_at": committed_at, - "authority_challenge": challenge, - "generated_by": { - "type": "system", - "component": apply.batch.TRUSTED_ONLINE_APPLY_SERVICE_COMPONENT, - }, - "item_results": request["expected_publication_item_results"], - "write_count": 74, - "status": "committed_and_verified", - } - online_receipt = _sign_envelope( - material["online_private"], - schema=apply.batch.ONLINE_TRANSACTION_RECEIPT_SCHEMA_VERSION, - id_prefix="rc06-online-transaction-", - domain=apply.batch.ONLINE_TRANSACTION_SIGNATURE_DOMAIN, - payload=online_payload, - ) - apply_payload = { - "schema_version": apply.batch.APPLY_PAYLOAD_SCHEMA_VERSION, - "batch_manifest_payload_sha256": request["batch_manifest_payload_sha256"], - "approval_receipt_sha256": request["approval_receipt_sha256"], - "environment_id": request["environment_id"], - "applied_at": committed_at, - "generated_by": {"type": "system", "component": "rc06-apply-authority"}, - "item_results": request["expected_publication_item_results"], - "write_count": 74, - "status": "applied_and_verified", - "online_transaction_receipt": online_receipt, - } - apply_receipt = _sign_envelope( - material["apply_private"], - schema=apply.batch.APPLY_SCHEMA_VERSION, - id_prefix="rc06-apply-receipt-", - domain=apply.batch.APPLY_SIGNATURE_DOMAIN, - payload=apply_payload, - ) - runtime_payload = { - "schema_version": apply.RUNTIME_PAYLOAD_SCHEMA_VERSION, - "key_id": apply.batch.TRUSTED_ONLINE_APPLY_SERVICE_KEY_ID, + "candidate_commit": manifest["candidate"]["commit"], + "candidate_tag": manifest["candidate"]["tag"], "batch_manifest_raw_sha256": request["batch_manifest_raw_sha256"], "batch_manifest_payload_sha256": request["batch_manifest_payload_sha256"], "approval_receipt_sha256": request["approval_receipt_sha256"], @@ -420,170 +516,234 @@ class FakePublicationService: "legal_source_review_binding_sha256" ], "environment_id": request["environment_id"], + "tenant_id": request["tenant_id"], + "idempotency_key": request["idempotency_key"], "transaction_id": request["transaction_id"], - "captured_at": committed_at, + "transaction_mode": apply.batch.TRANSACTION_MODE, + "cross_database_atomicity": False, + "coordinator_deployment": { + "component": apply.batch.TRUSTED_ONLINE_APPLY_SERVICE_COMPONENT, + "candidate_commit": manifest["candidate"]["commit"], + "release_image_digest": images["contract-api"], + }, + "participant_evidence": participants, + "committed_at": (self.now - timedelta(seconds=10)).isoformat( + timespec="seconds" + ), + "authority_challenge": request["authority_challenge"], "generated_by": { "type": "system", "component": apply.batch.TRUSTED_ONLINE_APPLY_SERVICE_COMPONENT, }, - "transaction_mode": "single_service_atomic", + "item_results": item_results, + "write_count": 74, + "status": "committed_and_verified", + } + online_receipt = _kms_envelope( + self.online_private, + schema=apply.batch.ONLINE_TRANSACTION_RECEIPT_SCHEMA_VERSION, + id_prefix="rc06-online-transaction-", + domain=apply.batch.ONLINE_TRANSACTION_SIGNATURE_DOMAIN, + payload=online_payload, + request_id="fake-kms-online-request", + ) + runtime_payload = { + "schema_version": apply.batch.RUNTIME_PAYLOAD_SCHEMA_VERSION, + "batch_manifest_raw_sha256": request["batch_manifest_raw_sha256"], + "batch_manifest_payload_sha256": request["batch_manifest_payload_sha256"], + "approval_receipt_sha256": request["approval_receipt_sha256"], + "legal_source_review_binding_sha256": request[ + "legal_source_review_binding_sha256" + ], + "environment_id": request["environment_id"], + "tenant_id": request["tenant_id"], + "idempotency_key": request["idempotency_key"], + "transaction_id": request["transaction_id"], + "online_transaction_receipt_sha256": apply.batch._sha256_json( + online_receipt + ), + "captured_at": self.now.isoformat(timespec="seconds"), + "generated_by": { + "type": "system", + "component": apply.batch.TRUSTED_ONLINE_APPLY_SERVICE_COMPONENT, + }, + "transaction_mode": apply.batch.TRANSACTION_MODE, + "cross_database_atomicity": False, "publication_item_count": 74, "runtime_snapshot_dependencies": request[ "expected_runtime_snapshot_dependencies" ], "runtime_snapshot_dependency_count": 14, - "status": "runtime_snapshots_verified", + "status": "published_and_resolved", } - runtime_receipt = _sign_envelope( - material["online_private"], - schema=apply.RUNTIME_RECEIPT_SCHEMA_VERSION, + runtime_receipt = _kms_envelope( + self.online_private, + schema=apply.batch.RUNTIME_RECEIPT_SCHEMA_VERSION, id_prefix="rc06-runtime-snapshot-", - domain=apply.RUNTIME_SIGNATURE_DOMAIN, + domain=apply.batch.RUNTIME_SIGNATURE_DOMAIN, payload=runtime_payload, + request_id="fake-kms-runtime-request", ) - return apply_receipt, runtime_receipt + return online_receipt, runtime_receipt - def apply(self, request: dict, request_sha256: str) -> tuple[int, dict]: + def dispatch(self, request: dict, request_sha256: str) -> tuple[int, dict]: self.apply_calls += 1 - with sqlite3.connect(self.database) as connection: - existing = connection.execute( - "SELECT response_json FROM transactions WHERE idempotency_key=? AND state='committed_and_verified'", - (request["idempotency_key"],), - ).fetchone() - if existing is not None: - return 200, json.loads(existing[0]) - try: - connection.execute("BEGIN IMMEDIATE") - connection.execute( - "INSERT OR REPLACE INTO transactions VALUES (?, ?, 'preparing', ?, NULL)", - ( - request["transaction_id"], - request["idempotency_key"], - request["legal_source_review_binding_sha256"], - ), - ) - for index, item in enumerate(request["expected_publication_item_results"]): - connection.execute( - "INSERT INTO publication_items VALUES (?, ?, ?, ?)", - ( - request["transaction_id"], - item["kind"], - item["item_id"], - item["online_content_sha256"], - ), - ) - if self.fail_after is not None and index + 1 == self.fail_after: - raise RuntimeError("injected transaction failure") - for item in request["expected_runtime_snapshot_dependencies"]: - connection.execute( - "INSERT INTO runtime_snapshots VALUES (?, ?, ?, ?, ?)", - ( - request["transaction_id"], - item["kind"], - item["runtime_key"], - item["online_content_sha256"], - item["status"], - ), - ) - apply_receipt, runtime_receipt = self._receipt_pair(request) - response = { - "schema_version": apply.APPLY_RESPONSE_SCHEMA_VERSION, - "idempotency_key": request["idempotency_key"], - "request_sha256": request_sha256, - "legal_source_review_binding_sha256": request[ - "legal_source_review_binding_sha256" - ], - "transaction_mode": "single_service_atomic", - "publication_item_count": 74, - "runtime_snapshot_dependency_count": 14, - "apply_receipt": apply_receipt, - "runtime_snapshot_receipt": runtime_receipt, - } - connection.execute( - "UPDATE transactions SET state='committed_and_verified', response_json=? WHERE transaction_id=?", - (json.dumps(response, ensure_ascii=False), request["transaction_id"]), - ) - connection.commit() - return 200, response - except RuntimeError: - connection.rollback() - return 409, {"error": "injected transaction failure; SQLite rolled back"} + if self.response is not None: + return 200, self.response + self.request = request + if self.fail_before_commit: + self.state = "prepared" + return 409, {"error": "injected failure before any durable commit"} + online_receipt, runtime_receipt = self._receipt_pair(request) + self.response = { + "schema_version": apply.APPLY_RESPONSE_SCHEMA_VERSION, + "idempotency_key": request["idempotency_key"], + "request_sha256": request_sha256, + "legal_source_review_binding_sha256": request[ + "legal_source_review_binding_sha256" + ], + "legal_source_review_expires_at": request[ + "legal_source_review_expires_at" + ], + "transaction_id": request["transaction_id"], + "environment_id": request["environment_id"], + "tenant_id": request["tenant_id"], + "transaction_mode": apply.batch.TRANSACTION_MODE, + "cross_database_atomicity": False, + "publication_item_count": 74, + "runtime_snapshot_dependency_count": 14, + "online_transaction_receipt": online_receipt, + "runtime_snapshot_receipt": runtime_receipt, + } + self.state = "committed_and_verified" + return 200, self.response - def status(self, transaction_id: str) -> tuple[int, dict]: - with sqlite3.connect(self.database) as connection: - transaction = connection.execute( - "SELECT idempotency_key, state, legal_review_sha256, response_json FROM transactions WHERE transaction_id=?", - (transaction_id,), - ).fetchone() - if transaction is None or transaction[1] == "rolled_back": - return 404, {"error": "transaction is not committed"} - response = json.loads(transaction[3]) - publication = response["apply_receipt"]["payload"]["item_results"] - runtime = response["runtime_snapshot_receipt"]["payload"][ - "runtime_snapshot_dependencies" - ] - if self.tamper_status: - publication = copy.deepcopy(publication) - publication[0]["online_content_sha256"] = "f" * 64 - if self.tamper_runtime_status: - runtime = copy.deepcopy(runtime) - runtime[0]["online_content_sha256"] = "f" * 64 - return 200, { - "schema_version": apply.APPLY_STATUS_SCHEMA_VERSION, - "idempotency_key": transaction[0], - "transaction_id": transaction_id, - "state": "committed_and_verified", - "legal_source_review_binding_sha256": transaction[2], - "publication_item_results": publication, - "publication_item_count": len(publication), - "runtime_snapshot_dependencies": runtime, - "runtime_snapshot_dependency_count": len(runtime), - "apply_receipt_sha256": apply.batch._sha256_json(response["apply_receipt"]), - "runtime_snapshot_receipt_sha256": apply.batch._sha256_json( - response["runtime_snapshot_receipt"] - ), - } + @staticmethod + def _durable_state(participant: str, state: str, write_count: int) -> dict: + projection = { + "participant": participant, + "state": state, + "write_count": write_count, + } + return { + **projection, + "durable_state_sha256": apply.batch._sha256_json(projection), + } def rollback(self, transaction_id: str, request: dict) -> tuple[int, dict]: - with sqlite3.connect(self.database) as connection: - connection.execute("BEGIN IMMEDIATE") - connection.execute( - "DELETE FROM publication_items WHERE transaction_id=?", (transaction_id,) - ) - connection.execute( - "DELETE FROM runtime_snapshots WHERE transaction_id=?", (transaction_id,) - ) - connection.execute( - "INSERT OR REPLACE INTO transactions " - "(transaction_id,idempotency_key,state,legal_review_sha256,response_json) " - "VALUES (?,?,'rolled_back',?,NULL)", - ( - transaction_id, - request["idempotency_key"], - request["legal_source_review_binding_sha256"], - ), - ) - connection.commit() - evidence = { - "transaction_id": transaction_id, - "idempotency_key": request["idempotency_key"], - "restored_item_count": 0, - } - return 200, { + self.rollback_calls += 1 + before = [ + self._durable_state("ai-platform", "prepared", 0), + self._durable_state("contract-platform", "prepared", 0), + ] + after = [ + self._durable_state("ai-platform", "compensated_and_verified", 0), + self._durable_state("contract-platform", "compensated_and_verified", 0), + ] + projection = { "schema_version": apply.ROLLBACK_RESPONSE_SCHEMA_VERSION, "idempotency_key": request["idempotency_key"], "transaction_id": transaction_id, "state": "rolled_back", + "participant_states_before": before, + "participant_states_after": after, + "compensation_item_results": [], "restored_item_count": 0, - "rollback_evidence_sha256": apply.batch._sha256_json(evidence), + "removed_item_count": 0, + } + self.state = "rolled_back" + return 200, { + **projection, + "rollback_evidence_sha256": apply.batch._sha256_json(projection), } - def counts(self) -> tuple[int, int]: - with sqlite3.connect(self.database) as connection: - return ( - connection.execute("SELECT count(*) FROM publication_items").fetchone()[0], - connection.execute("SELECT count(*) FROM runtime_snapshots").fetchone()[0], + def finalize(self, transaction_id: str, request: dict) -> tuple[int, dict]: + self.finalize_calls += 1 + self.finalize_request = request + if self.fail_finalize: + return 503, {"error": "injected countersign persistence failure"} + assert transaction_id == request["transaction_id"] + assert request["apply_receipt_sha256"] == apply.batch._sha256_json( + request["apply_receipt"] + ) + self.state = "finalized" + finalized_at = ( + datetime.fromisoformat( + request["apply_receipt"]["payload"]["countersigned_at"] ) + + timedelta(seconds=1) + ).isoformat(timespec="seconds") + return 200, { + "schema_version": apply.FINALIZE_RESPONSE_SCHEMA_VERSION, + "idempotency_key": request["idempotency_key"], + "transaction_id": transaction_id, + "state": "finalized", + "no_go": False, + "apply_receipt_sha256": request["apply_receipt_sha256"], + "finalized_at": finalized_at, + } + + def status(self, transaction_id: str) -> tuple[int, dict]: + assert self.request is not None + if self.state in {"prepared", "rolled_back"}: + return 200, { + "schema_version": apply.APPLY_STATUS_SCHEMA_VERSION, + "idempotency_key": self.request["idempotency_key"], + "transaction_id": transaction_id, + "state": self.state, + } + assert self.response is not None + online = self.response["online_transaction_receipt"] + runtime = self.response["runtime_snapshot_receipt"] + if self.state == "finalized": + assert self.finalize_request is not None + finalized_at = ( + datetime.fromisoformat( + self.finalize_request["apply_receipt"]["payload"]["countersigned_at"] + ) + + timedelta(seconds=1) + ).isoformat(timespec="seconds") + return 200, { + "schema_version": apply.FINALIZE_STATUS_SCHEMA_VERSION, + "idempotency_key": self.request["idempotency_key"], + "transaction_id": transaction_id, + "state": "finalized", + "no_go": False, + "transaction_mode": apply.batch.TRANSACTION_MODE, + "cross_database_atomicity": False, + "publication_item_count": 74, + "runtime_snapshot_dependency_count": 14, + "online_transaction_receipt_sha256": apply.batch._sha256_json(online), + "runtime_snapshot_receipt_sha256": apply.batch._sha256_json(runtime), + "apply_receipt_sha256": self.finalize_request["apply_receipt_sha256"], + "finalized_at": finalized_at, + } + publication = copy.deepcopy(online["payload"]["item_results"]) + snapshots = copy.deepcopy(runtime["payload"]["runtime_snapshot_dependencies"]) + if self.tamper_status == "publication": + publication[0]["online_content_sha256"] = "f" * 64 + if self.tamper_status == "runtime": + snapshots[0]["online_content_sha256"] = "f" * 64 + return 200, { + "schema_version": apply.APPLY_STATUS_SCHEMA_VERSION, + "idempotency_key": self.request["idempotency_key"], + "transaction_id": transaction_id, + "state": "committed_and_verified", + "legal_source_review_binding_sha256": self.request[ + "legal_source_review_binding_sha256" + ], + "environment_id": self.request["environment_id"], + "tenant_id": self.request["tenant_id"], + "transaction_mode": apply.batch.TRANSACTION_MODE, + "cross_database_atomicity": False, + "publication_item_results": publication, + "publication_item_count": 74, + "runtime_snapshot_dependencies": snapshots, + "runtime_snapshot_dependency_count": 14, + "online_transaction_receipt_sha256": apply.batch._sha256_json(online), + "runtime_snapshot_receipt_sha256": apply.batch._sha256_json(runtime), + "participant_evidence": online["payload"]["participant_evidence"], + } @contextmanager @@ -621,15 +781,25 @@ def _serve(service: FakePublicationService): ): self._send(400, {"error": "identity header mismatch"}) return - status, payload = service.apply( + status, payload = service.dispatch( request, self.headers["X-RC06-Request-SHA256"] ) self._send(status, payload) return if self.path.endswith("/rollback"): - request = self._body() transaction_id = self.path.split("/")[-2] - status, payload = service.rollback(transaction_id, request) + status, payload = service.rollback(transaction_id, self._body()) + self._send(status, payload) + return + if self.path.endswith("/finalize"): + request = self._body() + if self.headers.get("X-RC06-Request-SHA256") != apply.batch._sha256_json( + request + ): + self._send(400, {"error": "finalize identity header mismatch"}) + return + transaction_id = self.path.split("/")[-2] + status, payload = service.finalize(transaction_id, request) self._send(status, payload) return self._send(404, {"error": "not found"}) @@ -656,172 +826,163 @@ def _serve(service: FakePublicationService): def _execute(material: dict, client, ledger) -> dict: - return apply.apply_publication_batch( + return apply.execute_online_publication_batch( manifest=material["manifest"], manifest_raw_sha256=material["manifest_raw"], approval=material["approval"], approval_raw_sha256=material["approval_raw"], - approval_keyring=material["keyring"], + approval_keyring=material["approval_keyring"], approval_root_public_key=material["root_private"].public_key(), apply_service_public_key=material["apply_private"].public_key(), online_service_public_key=material["online_private"].public_key(), legal_review_request=material["legal_request"], legal_review_decision=material["legal_decision"], legal_review_keyring=material["legal_keyring"], - environment_id="rc06-test", + legal_review_keyring_raw_sha256=material["legal_keyring_raw"], + authority_challenge=material["authority_challenge"], + environment_id=material["environment_id"], + tenant_id=material["tenant_id"], client=client, ledger=ledger, now=material["now"], ) -def test_authenticated_apply_is_atomic_hash_verified_and_idempotent( +def _countersign(material: dict, execution: dict, at: datetime) -> dict: + payload = { + "schema_version": apply.batch.APPLY_PAYLOAD_SCHEMA_VERSION, + "batch_manifest_raw_sha256": material["manifest_raw"], + "batch_manifest_payload_sha256": material["manifest"]["manifest_payload_sha256"], + "approval_receipt_sha256": material["approval_raw"], + "legal_source_review_binding_sha256": material["legal_binding"], + "environment_id": material["environment_id"], + "tenant_id": material["tenant_id"], + "idempotency_key": material["idempotency_key"], + "transaction_id": material["transaction_id"], + "authorized_commit_at": execution["runtime_snapshot_receipt"]["payload"][ + "captured_at" + ], + "countersigned_at": at.isoformat(timespec="seconds"), + "generated_by": {"type": "system", "component": "rc06-apply-authority"}, + "transaction_mode": apply.batch.TRANSACTION_MODE, + "cross_database_atomicity": False, + "item_results": apply.batch._expected_apply_item_results(material["manifest"]), + "write_count": 74, + "status": "applied_and_verified", + "online_transaction_receipt": execution["online_transaction_receipt"], + "runtime_snapshot_receipt": execution["runtime_snapshot_receipt"], + } + return _ed_envelope( + material["apply_private"], + schema=apply.batch.APPLY_SCHEMA_VERSION, + id_field="receipt_id", + id_prefix="rc06-apply-receipt-", + domain=apply.batch.APPLY_SIGNATURE_DOMAIN, + payload=payload, + ) + + +def _finalize(material: dict, client, ledger, execution, receipt, now): + return apply.finalize_publication_batch( + manifest=material["manifest"], + manifest_raw_sha256=material["manifest_raw"], + approval=material["approval"], + approval_raw_sha256=material["approval_raw"], + approval_keyring=material["approval_keyring"], + approval_root_public_key=material["root_private"].public_key(), + apply_service_public_key=material["apply_private"].public_key(), + online_service_public_key=material["online_private"].public_key(), + legal_review_binding_sha256=material["legal_binding"], + legal_review_expires_at=material["legal_expires_at"], + environment_id=material["environment_id"], + tenant_id=material["tenant_id"], + online_execution=execution, + apply_receipt=receipt, + client=client, + ledger=ledger, + now=now, + ) + + +def _service(material: dict, **kwargs) -> FakePublicationService: + return FakePublicationService( + manifest=material["manifest"], + now=material["now"], + online_private=material["online_private"], + **kwargs, + ) + + +def test_online_execute_then_offline_countersign_is_durable_and_idempotent( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: material = _material(monkeypatch, datetime(2026, 10, 6, 8, 0, tzinfo=UTC)) - runtime = apply.expected_runtime_snapshot_results(material["manifest"]) - assert len(runtime) == 14 - drafting = next(item for item in runtime if item["kind"] == "composed_drafting") - assert drafting["runtime_key"] == "contract_drafting_v1" - assert drafting["version"] == "tp06-23-20260909-1" - assert sum(item["kind"] == "new_review_module" for item in runtime) == 8 - assert sum(item["kind"] == "existing_domain_prompt" for item in runtime) == 5 - service = FakePublicationService(tmp_path / "remote.sqlite3", material) - with _serve(service) as url: - client = apply.JsonHttpClient( - url, "test-token", allow_http_loopback=True - ) - ledger = apply.ApplyLedger(tmp_path / "ledger.sqlite3") - try: - evidence = _execute(material, client, ledger) - replay = _execute(material, client, ledger) - finally: - ledger.close() - assert replay == evidence - assert evidence["publication_item_count"] == 74 - assert evidence["runtime_snapshot_dependency_count"] == 14 - assert service.counts() == (74, 14) - assert service.apply_calls == 1 - - second_ledger = apply.ApplyLedger(tmp_path / "second-ledger.sqlite3") - try: - assert _execute(material, client, second_ledger) == evidence - finally: - second_ledger.close() - assert service.counts() == (74, 14) - assert service.apply_calls == 2 - - -@pytest.mark.parametrize("mode", ["missing", "expired", "hash_mismatch"]) -def test_legal_source_review_is_fail_closed_before_http( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - mode: str, -) -> None: - material = _material(monkeypatch, datetime(2026, 10, 6, 8, 0, tzinfo=UTC)) - if mode == "missing": - material["legal_request"] = {} - elif mode == "expired": - material["legal_decision"]["reviewed_at"] = "2026-10-04T08:00:00+00:00" - material["legal_decision"]["expires_at"] = "2026-10-05T08:00:00+00:00" - else: - material["legal_decision"]["items"][0]["source_record_sha256"] = "f" * 64 - service = FakePublicationService(tmp_path / "remote.sqlite3", material) + service = _service(material) with _serve(service) as url: client = apply.JsonHttpClient(url, "test-token", allow_http_loopback=True) ledger = apply.ApplyLedger(tmp_path / "ledger.sqlite3") try: - with pytest.raises(apply.ApplyPublicationError, match="scene legal review"): - _execute(material, client, ledger) + execution = _execute(material, client, ledger) + assert _execute(material, client, ledger) == execution + state = ledger.connection.execute( + "SELECT state FROM rc06_apply_operations" + ).fetchone()[0] + assert state == "online_committed_pending_countersign" + assert service.finalize_calls == 0 + + # The short challenge has expired, but the signed commit and runtime + # facts were captured inside it. Delayed offline finalization remains valid. + countersigned_at = material["now"] + timedelta(minutes=10) + receipt = _countersign(material, execution, countersigned_at) + final = _finalize( + material, client, ledger, execution, receipt, countersigned_at + ) + assert final["schema_version"] == apply.FINAL_EVIDENCE_SCHEMA_VERSION + assert service.finalize_request is not None + assert service.finalize_request["apply_receipt"] == receipt + assert _finalize( + material, client, ledger, execution, receipt, countersigned_at + ) == final + state = ledger.connection.execute( + "SELECT state FROM rc06_apply_operations" + ).fetchone()[0] finally: ledger.close() - assert service.apply_calls == 0 - assert service.counts() == (0, 0) + assert state == "succeeded" + assert service.apply_calls == 1 + assert service.finalize_calls == 1 + assert service.rollback_calls == 0 -@pytest.mark.parametrize("field", ["version", "content_sha256"]) -def test_forged_executable_runtime_identity_is_rejected_before_http( +@pytest.mark.parametrize("tamper_status", ["publication", "runtime"]) +def test_terminal_commit_verification_failure_never_rolls_back( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, - field: str, + tamper_status: str, ) -> None: material = _material(monkeypatch, datetime(2026, 10, 6, 8, 0, tzinfo=UTC)) - manifest = material["manifest"] - drafting = next( - item - for item in manifest["runtime_prompt_dependencies"]["items"] - if item["kind"] == "composed_drafting" - ) - drafting[field] = "forged-version" if field == "version" else "f" * 64 - manifest["runtime_prompt_dependencies"]["inventory_sha256"] = apply.batch._sha256_json( - manifest["runtime_prompt_dependencies"]["items"] - ) - manifest["manifest_payload_sha256"] = apply.batch._sha256_json( - {key: value for key, value in manifest.items() if key != "manifest_payload_sha256"} - ) - material["manifest_raw"] = apply.batch._sha256_bytes(apply.batch._canonical(manifest)) - service = FakePublicationService(tmp_path / "remote.sqlite3", material) + service = _service(material, tamper_status=tamper_status) with _serve(service) as url: client = apply.JsonHttpClient(url, "test-token", allow_http_loopback=True) ledger = apply.ApplyLedger(tmp_path / "ledger.sqlite3") try: - with pytest.raises(apply.ApplyPublicationError, match="candidate executable source"): + with pytest.raises(apply.ApplyPublicationError, match="rollback is forbidden"): _execute(material, client, ledger) + state = ledger.connection.execute( + "SELECT state FROM rc06_apply_operations" + ).fetchone()[0] finally: ledger.close() - assert service.apply_calls == 0 - assert service.counts() == (0, 0) + assert state == "committed_evidence_invalid_no_go" + assert service.state == "committed_and_verified" + assert service.rollback_calls == 0 -def test_runtime_snapshot_module_hash_must_match_publication_item( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, +def test_only_pre_terminal_state_is_compensated_with_structured_evidence( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: material = _material(monkeypatch, datetime(2026, 10, 6, 8, 0, tzinfo=UTC)) - manifest = material["manifest"] - drafting = next( - item - for item in manifest["runtime_prompt_dependencies"]["items"] - if item["kind"] == "composed_drafting" - ) - drafting["publication_module_dependencies"][0]["content_sha256"] = "f" * 64 - manifest["runtime_prompt_dependencies"]["inventory_sha256"] = apply.batch._sha256_json( - manifest["runtime_prompt_dependencies"]["items"] - ) - manifest["manifest_payload_sha256"] = apply.batch._sha256_json( - {key: value for key, value in manifest.items() if key != "manifest_payload_sha256"} - ) - material["manifest_raw"] = apply.batch._sha256_bytes(apply.batch._canonical(manifest)) - service = FakePublicationService(tmp_path / "remote.sqlite3", material) - with _serve(service) as url: - client = apply.JsonHttpClient(url, "test-token", allow_http_loopback=True) - ledger = apply.ApplyLedger(tmp_path / "ledger.sqlite3") - try: - with pytest.raises( - apply.batch.PublicationBatchError, - match="exact publication module identities", - ): - _execute(material, client, ledger) - finally: - ledger.close() - assert service.apply_calls == 0 - assert service.counts() == (0, 0) - - -@pytest.mark.parametrize("failure", ["write", "readback_hash", "runtime_hash"]) -def test_failure_never_leaves_partial_or_false_success( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - failure: str, -) -> None: - material = _material(monkeypatch, datetime(2026, 10, 6, 8, 0, tzinfo=UTC)) - service = FakePublicationService( - tmp_path / "remote.sqlite3", - material, - fail_after=17 if failure == "write" else None, - tamper_status=failure == "readback_hash", - tamper_runtime_status=failure == "runtime_hash", - ) + service = _service(material, fail_before_commit=True) with _serve(service) as url: client = apply.JsonHttpClient(url, "test-token", allow_http_loopback=True) ledger = apply.ApplyLedger(tmp_path / "ledger.sqlite3") @@ -834,7 +995,68 @@ def test_failure_never_leaves_partial_or_false_success( finally: ledger.close() assert state == "rolled_back" - assert service.counts() == (0, 0) + assert service.rollback_calls == 1 + + +def test_finalize_failure_remains_no_go_and_never_rolls_back( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + material = _material(monkeypatch, datetime(2026, 10, 6, 8, 0, tzinfo=UTC)) + service = _service(material, fail_finalize=True) + with _serve(service) as url: + client = apply.JsonHttpClient(url, "test-token", allow_http_loopback=True) + ledger = apply.ApplyLedger(tmp_path / "ledger.sqlite3") + try: + execution = _execute(material, client, ledger) + at = material["now"] + timedelta(minutes=10) + receipt = _countersign(material, execution, at) + with pytest.raises(apply.ApplyPublicationError): + _finalize(material, client, ledger, execution, receipt, at) + state = ledger.connection.execute( + "SELECT state FROM rc06_apply_operations" + ).fetchone()[0] + finally: + ledger.close() + assert state == "finalize_failed_no_go" + assert service.state == "committed_and_verified" + assert service.rollback_calls == 0 + + +@pytest.mark.parametrize("mode", ["missing", "expired", "untrusted_keyring"]) +def test_legal_source_review_is_fail_closed_before_http( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + mode: str, +) -> None: + material = _material(monkeypatch, datetime(2026, 10, 6, 8, 0, tzinfo=UTC)) + if mode == "missing": + material["legal_request"] = {} + elif mode == "expired": + material["legal_decision"]["expires_at"] = "2026-10-05T08:00:00+00:00" + else: + monkeypatch.setattr( + apply.batch, "TRUSTED_SCENE_LEGAL_REVIEW_KEYRING_SHA256", "f" * 64 + ) + service = _service(material) + with _serve(service) as url: + client = apply.JsonHttpClient(url, "test-token", allow_http_loopback=True) + ledger = apply.ApplyLedger(tmp_path / "ledger.sqlite3") + try: + with pytest.raises(apply.ApplyPublicationError, match="scene legal review"): + _execute(material, client, ledger) + finally: + ledger.close() + assert service.apply_calls == 0 + + +def test_legacy_ledger_is_rejected_without_silent_upgrade(tmp_path: Path) -> None: + path = tmp_path / "legacy.sqlite3" + with sqlite3.connect(path) as connection: + connection.execute( + "CREATE TABLE rc06_apply_operations (idempotency_key TEXT PRIMARY KEY)" + ) + with pytest.raises(apply.ApplyPublicationError, match="cannot be silently upgraded"): + apply.ApplyLedger(path) def test_http_and_auth_fail_closed() -> None: diff --git a/yuqei-infra/tests/test_rc06_publication_batch.py b/yuqei-infra/tests/test_rc06_publication_batch.py index c8e16dfe..63826900 100644 --- a/yuqei-infra/tests/test_rc06_publication_batch.py +++ b/yuqei-infra/tests/test_rc06_publication_batch.py @@ -2,6 +2,7 @@ from __future__ import annotations import copy import base64 +import hashlib import importlib.util import json import os @@ -12,7 +13,8 @@ from datetime import UTC, datetime, timedelta from pathlib import Path import pytest -from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import padding, rsa, utils from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey @@ -90,6 +92,26 @@ def _image_identity() -> dict: return {"images": images, "identity_set_sha256": batch._sha256_json(images)} +def _rc05_candidate_binding() -> dict: + images = _image_identity() + return { + "schema_version": batch.RC05_CANDIDATE_SCHEMA_VERSION, + "commit": "a" * 40, + "tag": "v2-fixture", + "source_manifest": { + "schema_version": "yuqei.release-source-manifest.v1", + "raw_sha256": "0" * 64, + "payload_sha256": "1" * 64, + }, + "image_identities": { + "schema_version": batch.IMAGE_SCHEMA_VERSION, + "raw_sha256": "2" * 64, + "identity_set_sha256": images["identity_set_sha256"], + "images": images["images"], + }, + } + + def _rc05_plan() -> dict: cases = [] @@ -120,6 +142,7 @@ def _rc05_plan() -> dict: "auto_repair": auto_repair, "operations": operations, "provider_call_budget": len(operations), + "failure_class": "drafting", } for index in range(90): @@ -145,9 +168,21 @@ def _rc05_plan() -> dict: "request": request, "request_sha256": batch._sha256_json(request), "expectations": {}, + "failure_class": operation, }) + for position, canary_id in zip( + (0, 1, 2, 3, 120, 136), + batch.EXPECTED_RC05_CANARY_CASE_IDS, + strict=True, + ): + cases[position]["case_id"] = canary_id + cases[position]["source_case_id"] = canary_id + failure_classes = { + case["case_id"]: case["failure_class"] for case in cases + } return { - "schema_version": "multiscene-evaluation-plan.v1", + "schema_version": batch.RC05_PLAN_SCHEMA_VERSION, + "prepared_at": "2026-09-27T03:00:00Z", "model": "gpt-5.6-sol", "reasoning_effort": "medium", "timeout_seconds": 300, @@ -156,8 +191,15 @@ def _rc05_plan() -> dict: "supplemental_case_count": 24, "operation_limits": batch.EXPECTED_RC05_OPERATION_LIMITS, "total_provider_call_budget": 280, + "candidate": _rc05_candidate_binding(), + "release_controls": { + "schema_version": batch.RC05_RELEASE_CONTROLS_SCHEMA_VERSION, + "canary_case_ids": list(batch.EXPECTED_RC05_CANARY_CASE_IDS), + "failure_classes": failure_classes, + }, "source_pins": {"source.py": "0" * 64}, "cases": cases, + "limitations": ["fixture only"], } @@ -182,6 +224,7 @@ def _terminal_identity(plan_sha: str = "3" * 64) -> dict: def _terminal_documents(plan: dict, plan_sha: str = "3" * 64): + replacement_case_id = plan["release_controls"]["canary_case_ids"][0] attempts = [] for index in range(4): resolved = index == 3 @@ -189,7 +232,7 @@ def _terminal_documents(plan: dict, plan_sha: str = "3" * 64): "resolution_status": "resolved" if resolved else "requires_replacement", "release_eligible": resolved, "reason": "fixture resolution" if resolved else "fixture replacement", - "replacement_case_id": None if resolved else "draft-000", + "replacement_case_id": None if resolved else replacement_case_id, "replacement_operation": None if resolved else "draft", } attempts.append({ @@ -243,13 +286,15 @@ def _terminal_documents(plan: dict, plan_sha: str = "3" * 64): }, "source_pins": dict(sorted(plan["source_pins"].items())), "real_provider": True, + "candidate_binding": plan["candidate"], + "release_controls": plan["release_controls"], "authority": authority, "integrity_not_authentication": True, "historical_usage": historical, } campaign_identity = { "schema_version": batch.RC05_CAMPAIGN_SCHEMA_VERSION, - "campaign_id": "rc05-real-usage-v1", + "campaign_id": "rc05-real-usage-v2", "historical_receipt_sha256": historical["receipt_sha256"], "historical_attempts_sha256": historical["attempts_sha256"], "historical_provider_call_count": 4, @@ -373,7 +418,7 @@ def _terminal_documents(plan: dict, plan_sha: str = "3" * 64): current_calls = sum(current_operations.values()) combined_calls = current_calls + 4 ledger_snapshot = { - "schema_version": "multiscene-call-ledger-snapshot.v1", + "schema_version": batch.RC05_LEDGER_SNAPSHOT_SCHEMA_VERSION, "ledger_identity_sha256": batch._sha256_json(ledger_identity), "real_provider": True, "authority": authority, @@ -395,6 +440,13 @@ def _terminal_documents(plan: dict, plan_sha: str = "3" * 64): "attempted": current_calls, "terminal": current_calls, "unknown": 0, + "release_controls": { + "schema_version": batch.RC05_CONTROL_STATE_SCHEMA_VERSION, + "phase": "full_authorized", + "canary_case_ids": list(batch.EXPECTED_RC05_CANARY_CASE_IDS), + "blocked_failure_classes": [], + "canary_transition_sha256": "9" * 64, + }, } remediation = [ ({ @@ -407,7 +459,7 @@ def _terminal_documents(plan: dict, plan_sha: str = "3" * 64): "attempt_id": attempt["attempt_id"], "status": "replacement_verified", "passed": True, - "replacement_case_id": "draft-000", + "replacement_case_id": replacement_case_id, "replacement_operation": "draft", "provider_passed": True, "business_passed": True, @@ -512,19 +564,7 @@ def _manifest(inventory: dict, *, resolved: bool = False) -> dict: image_identities_sha256="2" * 64, image_identity=_image_identity(), rc05_plan_sha256="3" * 64, - rc05_plan_identity={ - "schema_version": "multiscene-evaluation-plan.v1", - "model": "gpt-5.6-sol", - "reasoning_effort": "medium", - "timeout_seconds": 300, - "case_count": 144, - "drafting_case_count": 120, - "supplemental_case_count": 24, - "operation_limits": batch.EXPECTED_RC05_OPERATION_LIMITS, - "total_provider_call_budget": 280, - "case_inventory_sha256": "5" * 64, - "source_pins_sha256": "4" * 64, - }, + rc05_plan_identity=batch.validate_rc05_plan(_rc05_plan()), rc05_terminal_evidence=_terminal_identity(), inventory=inventory, dispositions=dispositions, @@ -701,7 +741,9 @@ def test_one_reviewer_batch_signature_and_system_item_receipts_remove_human_repe root_private_key = Ed25519PrivateKey.generate() private_key = Ed25519PrivateKey.generate() apply_service_private_key = Ed25519PrivateKey.generate() - online_service_private_key = Ed25519PrivateKey.generate() + online_service_private_key = rsa.generate_private_key( + public_exponent=65537, key_size=2048 + ) root_public_key = root_private_key.public_key() apply_service_public_key = apply_service_private_key.public_key() online_service_public_key = online_service_private_key.public_key() @@ -713,9 +755,9 @@ def test_one_reviewer_batch_signature_and_system_item_receipts_remove_human_repe serialization.Encoding.Raw, serialization.PublicFormat.Raw, ) - online_service_public_raw = online_service_public_key.public_bytes( - serialization.Encoding.Raw, - serialization.PublicFormat.Raw, + online_service_public_spki = online_service_public_key.public_bytes( + serialization.Encoding.DER, + serialization.PublicFormat.SubjectPublicKeyInfo, ) public_pem = private_key.public_key().public_bytes( serialization.Encoding.PEM, @@ -763,8 +805,16 @@ def test_one_reviewer_batch_signature_and_system_item_receipts_remove_human_repe ) monkeypatch.setattr( batch, - "TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SHA256", - batch._sha256_bytes(online_service_public_raw), + "TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SPKI_SHA256", + batch._sha256_bytes(online_service_public_spki), + ) + monkeypatch.setattr(batch, "TRUSTED_ONLINE_APPLY_SERVICE_PROVIDER", "fixture-kms") + monkeypatch.setattr( + batch, "TRUSTED_ONLINE_APPLY_SERVICE_KEY_REFERENCE", "kms://fixture/rc06" + ) + monkeypatch.setattr(batch, "TRUSTED_ONLINE_APPLY_SERVICE_KEY_VERSION", "v1") + monkeypatch.setattr( + batch, "TRUSTED_ONLINE_APPLY_SERVICE_ALGORITHM", "RSA_PSS_SHA_256" ) monkeypatch.setattr( batch, @@ -999,26 +1049,20 @@ def test_one_reviewer_batch_signature_and_system_item_receipts_remove_human_repe ) approval_raw_sha = batch._sha256_bytes(batch._canonical(approval)) - item_results = [ - { - "kind": "asset", - "item_id": item["template_id"], - "expected_content_sha256": item["content_sha256"], - "online_content_sha256": item["content_sha256"], - "status": "applied_and_verified", - } - for item in manifest["asset_batch"]["items"] - ] + [ - { - "kind": "prompt_module", - "item_id": item["key"], - "expected_content_sha256": item["content_sha256"], - "online_content_sha256": item["content_sha256"], - "status": "applied_and_verified", - } - for item in manifest["prompt_module_batch"]["items"] - ] - item_results.sort(key=lambda item: (item["kind"], item["item_id"])) + item_results = batch._expected_apply_item_results(manifest) + runtime_results = batch.expected_runtime_snapshot_results(manifest) + legal_review_binding_sha256 = "d" * 64 + legal_review_expires_at = "2026-09-28T08:00:00+08:00" + environment_id = "isolated-uat-fixture" + tenant_id = "tenant-fixture" + idempotency_key = batch.publication_idempotency_key( + manifest_raw_sha, + approval_raw_sha, + legal_review_binding_sha256, + environment_id, + tenant_id, + ) + transaction_id = batch.publication_transaction_id(idempotency_key) challenge_payload = { "schema_version": batch.APPLY_CHALLENGE_PAYLOAD_SCHEMA_VERSION, "nonce_base64": base64.b64encode(b"c" * 32).decode("ascii"), @@ -1027,8 +1071,13 @@ def test_one_reviewer_batch_signature_and_system_item_receipts_remove_human_repe "batch_manifest_raw_sha256": manifest_raw_sha, "batch_manifest_payload_sha256": manifest["manifest_payload_sha256"], "approval_receipt_sha256": approval_raw_sha, + "legal_source_review_binding_sha256": legal_review_binding_sha256, + "legal_source_review_expires_at": legal_review_expires_at, "operation": batch.APPLY_OPERATION, - "environment_id": "isolated-uat-fixture", + "environment_id": environment_id, + "tenant_id": tenant_id, + "idempotency_key": idempotency_key, + "transaction_id": transaction_id, "actor": {"type": "system", "component": "rc06-publication-service"}, "issued_at": "2026-09-21T09:55:00+08:00", "expires_at": "2026-09-21T10:05:00+08:00", @@ -1047,14 +1096,100 @@ def test_one_reviewer_batch_signature_and_system_item_receipts_remove_human_repe "payload": challenge_payload, "signature": base64.b64encode(challenge_signature).decode("ascii"), } + + def kms_envelope(payload: dict, *, domain: bytes, prefix: str, request_id: str) -> dict: + digest = hashlib.sha256(domain + batch._canonical(payload)).digest() + signature_value = online_service_private_key.sign( + digest, + padding.PSS( + mgf=padding.MGF1(hashes.SHA256()), + salt_length=hashes.SHA256().digest_size, + ), + utils.Prehashed(hashes.SHA256()), + ) + signature_block = { + "provider": "fixture-kms", + "key_reference": "kms://fixture/rc06", + "key_version": "v1", + "algorithm": "RSA_PSS_SHA_256", + "value_base64": base64.b64encode(signature_value).decode("ascii"), + "request_id": request_id, + } + return { + "schema_version": ( + batch.ONLINE_TRANSACTION_RECEIPT_SCHEMA_VERSION + if prefix == "rc06-online-transaction-" + else batch.RUNTIME_RECEIPT_SCHEMA_VERSION + ), + "receipt_id": prefix + + batch._sha256_bytes( + batch._canonical(payload) + batch._canonical(signature_block) + )[:24], + "payload": payload, + "signature": signature_block, + } + + image_digests = { + item["role"]: item["manifest_digest"] + for item in manifest["candidate"]["images"] + } + participant_evidence = [] + participant_specs = ( + ( + "ai-platform", + "2026-09-21T09:58:00+08:00", + 14, + 14, + image_digests["ai-platform-api"], + [item for item in item_results if item["kind"] == "prompt_module"], + ), + ( + "contract-platform", + "2026-09-21T09:59:00+08:00", + 60, + 0, + image_digests["contract-api"], + [item for item in item_results if item["kind"] == "asset"], + ), + ) + for participant, committed_at, write_count, runtime_count, image, results in participant_specs: + projection = { + "participant": participant, + "state": "committed", + "transaction_id": transaction_id, + "candidate_commit": manifest["candidate"]["commit"], + "release_image_digest": image, + "committed_at": committed_at, + "write_count": write_count, + "runtime_snapshot_count": runtime_count, + "committed_item_results_sha256": batch._sha256_json(results), + } + participant_evidence.append( + { + **projection, + "durable_commit_evidence_sha256": batch._sha256_json(projection), + } + ) online_payload = { "schema_version": batch.ONLINE_TRANSACTION_PAYLOAD_SCHEMA_VERSION, - "key_id": batch.TRUSTED_ONLINE_APPLY_SERVICE_KEY_ID, + "candidate_commit": manifest["candidate"]["commit"], + "candidate_tag": manifest["candidate"]["tag"], + "batch_manifest_raw_sha256": manifest_raw_sha, "batch_manifest_payload_sha256": manifest["manifest_payload_sha256"], "approval_receipt_sha256": approval_raw_sha, - "environment_id": "isolated-uat-fixture", - "transaction_id": "fixture-transaction-001", - "database_commit_id": "fixture-database-commit-001", + "legal_source_review_binding_sha256": legal_review_binding_sha256, + "environment_id": environment_id, + "tenant_id": tenant_id, + "idempotency_key": idempotency_key, + "transaction_id": transaction_id, + "transaction_mode": batch.TRANSACTION_MODE, + "cross_database_atomicity": False, + "coordinator_deployment": { + "component": batch.TRUSTED_ONLINE_APPLY_SERVICE_COMPONENT, + "candidate_commit": manifest["candidate"]["commit"], + "release_image_digest": image_digests["contract-api"], + }, + "participant_evidence": participant_evidence, "committed_at": "2026-09-21T10:00:00+08:00", "authority_challenge": authority_challenge, "generated_by": {"type": "system", "component": "rc06-publication-service"}, @@ -1062,29 +1197,63 @@ def test_one_reviewer_batch_signature_and_system_item_receipts_remove_human_repe "write_count": 74, "status": "committed_and_verified", } - online_signature = online_service_private_key.sign( - batch.ONLINE_TRANSACTION_SIGNATURE_DOMAIN + batch._canonical(online_payload) + online_receipt = kms_envelope( + online_payload, + domain=batch.ONLINE_TRANSACTION_SIGNATURE_DOMAIN, + prefix="rc06-online-transaction-", + request_id="kms-online-fixture-001", ) - online_receipt = { - "schema_version": batch.ONLINE_TRANSACTION_RECEIPT_SCHEMA_VERSION, - "receipt_id": ( - "rc06-online-transaction-" - + batch._sha256_bytes(batch._canonical(online_payload) + online_signature)[:24] - ), - "payload": online_payload, - "signature": base64.b64encode(online_signature).decode("ascii"), - } - apply_payload = { - "schema_version": batch.APPLY_PAYLOAD_SCHEMA_VERSION, + runtime_payload = { + "schema_version": batch.RUNTIME_PAYLOAD_SCHEMA_VERSION, + "batch_manifest_raw_sha256": manifest_raw_sha, "batch_manifest_payload_sha256": manifest["manifest_payload_sha256"], "approval_receipt_sha256": approval_raw_sha, - "environment_id": "isolated-uat-fixture", - "applied_at": "2026-09-21T10:00:00+08:00", + "legal_source_review_binding_sha256": legal_review_binding_sha256, + "environment_id": environment_id, + "tenant_id": tenant_id, + "idempotency_key": idempotency_key, + "transaction_id": transaction_id, + "online_transaction_receipt_sha256": batch._sha256_json(online_receipt), + "captured_at": "2026-09-21T10:01:00+08:00", + "generated_by": { + "type": "system", + "component": batch.TRUSTED_ONLINE_APPLY_SERVICE_COMPONENT, + }, + "transaction_mode": batch.TRANSACTION_MODE, + "cross_database_atomicity": False, + "publication_item_count": 74, + "runtime_snapshot_dependencies": runtime_results, + "runtime_snapshot_dependency_count": 14, + "status": "published_and_resolved", + } + runtime_receipt = kms_envelope( + runtime_payload, + domain=batch.RUNTIME_SIGNATURE_DOMAIN, + prefix="rc06-runtime-snapshot-", + request_id="kms-runtime-fixture-001", + ) + apply_payload = { + "schema_version": batch.APPLY_PAYLOAD_SCHEMA_VERSION, + "batch_manifest_raw_sha256": manifest_raw_sha, + "batch_manifest_payload_sha256": manifest["manifest_payload_sha256"], + "approval_receipt_sha256": approval_raw_sha, + "legal_source_review_binding_sha256": legal_review_binding_sha256, + "environment_id": environment_id, + "tenant_id": tenant_id, + "idempotency_key": idempotency_key, + "transaction_id": transaction_id, + "authorized_commit_at": runtime_payload["captured_at"], + # A durable online commit remains countersignable after the short challenge + # window; only the signed online/runtime facts must fall inside that window. + "countersigned_at": "2026-09-21T10:30:00+08:00", "generated_by": {"type": "system", "component": "rc06-apply-authority"}, + "transaction_mode": batch.TRANSACTION_MODE, + "cross_database_atomicity": False, "item_results": item_results, "write_count": 74, "status": "applied_and_verified", "online_transaction_receipt": online_receipt, + "runtime_snapshot_receipt": runtime_receipt, } apply_signature = apply_service_private_key.sign( batch.APPLY_SIGNATURE_DOMAIN + batch._canonical(apply_payload) @@ -1108,31 +1277,12 @@ def test_one_reviewer_batch_signature_and_system_item_receipts_remove_human_repe online_service_public_key, approval_raw_sha, apply_receipt, + legal_review_binding_sha256=legal_review_binding_sha256, + legal_review_expires_at=legal_review_expires_at, + environment_id=environment_id, + tenant_id=tenant_id, ) == apply_receipt - boundary_apply = copy.deepcopy(apply_receipt) - boundary_apply["payload"]["applied_at"] = approval_payload["expires_at"] - boundary_signature = apply_service_private_key.sign( - batch.APPLY_SIGNATURE_DOMAIN + batch._canonical(boundary_apply["payload"]) - ) - boundary_apply["signature"] = base64.b64encode(boundary_signature).decode("ascii") - boundary_apply["receipt_id"] = ( - "rc06-apply-receipt-" - + batch._sha256_bytes(batch._canonical(boundary_apply["payload"]) + boundary_signature)[:24] - ) - with pytest.raises(batch.PublicationBatchError, match="outside the approval window"): - batch.validate_apply_receipt( - manifest, - manifest_raw_sha, - approval, - keyring, - root_public_key, - apply_service_public_key, - online_service_public_key, - approval_raw_sha, - boundary_apply, - ) - another_reviewer = copy.deepcopy(approval) another_reviewer["payload"]["approver"]["approver_id"] = "another-reviewer" with pytest.raises(batch.PublicationBatchError, match="sole release reviewer"): @@ -1146,6 +1296,16 @@ def test_one_reviewer_batch_signature_and_system_item_receipts_remove_human_repe tampered_apply = copy.deepcopy(apply_receipt) tampered_apply["payload"]["item_results"][0]["online_content_sha256"] = "7" * 64 + tampered_signature = apply_service_private_key.sign( + batch.APPLY_SIGNATURE_DOMAIN + batch._canonical(tampered_apply["payload"]) + ) + tampered_apply["signature"] = base64.b64encode(tampered_signature).decode("ascii") + tampered_apply["receipt_id"] = ( + "rc06-apply-receipt-" + + batch._sha256_bytes( + batch._canonical(tampered_apply["payload"]) + tampered_signature + )[:24] + ) with pytest.raises(batch.PublicationBatchError, match="74-item online evidence drift"): batch.validate_apply_receipt( manifest, @@ -1157,6 +1317,79 @@ def test_one_reviewer_batch_signature_and_system_item_receipts_remove_human_repe online_service_public_key, approval_raw_sha, tampered_apply, + legal_review_binding_sha256=legal_review_binding_sha256, + legal_review_expires_at=legal_review_expires_at, + environment_id=environment_id, + tenant_id=tenant_id, + ) + + # These mismatches are signed by the trusted fixture key so the tests prove + # semantic binding checks, not merely signature failure. + deployment_drift_payload = copy.deepcopy(online_payload) + deployment_drift_payload["participant_evidence"][0]["release_image_digest"] = ( + "sha256:" + "f" * 64 + ) + drift_projection = { + key: value + for key, value in deployment_drift_payload["participant_evidence"][0].items() + if key != "durable_commit_evidence_sha256" + } + deployment_drift_payload["participant_evidence"][0][ + "durable_commit_evidence_sha256" + ] = batch._sha256_json(drift_projection) + deployment_drift = kms_envelope( + deployment_drift_payload, + domain=batch.ONLINE_TRANSACTION_SIGNATURE_DOMAIN, + prefix="rc06-online-transaction-", + request_id="kms-online-fixture-deployment-drift", + ) + with pytest.raises(batch.PublicationBatchError, match="participant commit evidence drift"): + batch.validate_online_transaction_receipt( + manifest, + manifest_raw_sha, + approval_raw_sha, + approval, + apply_service_public_key, + online_service_public_key, + deployment_drift, + legal_review_binding_sha256=legal_review_binding_sha256, + legal_review_expires_at=legal_review_expires_at, + environment_id=environment_id, + tenant_id=tenant_id, + ) + + stale_schema = copy.deepcopy(online_receipt) + stale_schema["schema_version"] = "yuqei.rc06-online-transaction-receipt.v3" + with pytest.raises(batch.PublicationBatchError, match="schema is unsupported"): + batch.validate_online_transaction_receipt( + manifest, + manifest_raw_sha, + approval_raw_sha, + approval, + apply_service_public_key, + online_service_public_key, + stale_schema, + legal_review_binding_sha256=legal_review_binding_sha256, + legal_review_expires_at=legal_review_expires_at, + environment_id=environment_id, + tenant_id=tenant_id, + ) + + metadata_drift = copy.deepcopy(online_receipt) + metadata_drift["signature"]["key_version"] = "v2" + with pytest.raises(batch.PublicationBatchError, match="KMS signer identity drift"): + batch.validate_online_transaction_receipt( + manifest, + manifest_raw_sha, + approval_raw_sha, + approval, + apply_service_public_key, + online_service_public_key, + metadata_drift, + legal_review_binding_sha256=legal_review_binding_sha256, + legal_review_expires_at=legal_review_expires_at, + environment_id=environment_id, + tenant_id=tenant_id, ) forged_apply = copy.deepcopy(apply_receipt) @@ -1175,6 +1408,10 @@ def test_one_reviewer_batch_signature_and_system_item_receipts_remove_human_repe online_service_public_key, approval_raw_sha, forged_apply, + legal_review_binding_sha256=legal_review_binding_sha256, + legal_review_expires_at=legal_review_expires_at, + environment_id=environment_id, + tenant_id=tenant_id, ) assert manifest["publication_control"]["blocked_controls"] == list(batch.BLOCKED_CONTROLS) @@ -2189,6 +2426,25 @@ def test_rc05_plan_requires_exact_budget_operations_and_unique_case_inventory() identity = batch.validate_rc05_plan(plan) assert identity["total_provider_call_budget"] == 280 assert identity["operation_limits"] == batch.EXPECTED_RC05_OPERATION_LIMITS + assert identity["candidate"] == _rc05_candidate_binding() + assert identity["release_controls"] == plan["release_controls"] + + legacy = copy.deepcopy(plan) + legacy["schema_version"] = "multiscene-evaluation-plan.v1" + with pytest.raises(batch.PublicationBatchError, match="read-only diagnostic"): + batch.validate_rc05_plan(legacy) + + candidate_drift = copy.deepcopy(plan) + candidate_drift["candidate"]["commit"] = "f" * 40 + with pytest.raises(batch.PublicationBatchError, match="candidate binding differs"): + batch.validate_rc05_plan( + candidate_drift, expected_candidate=_rc05_candidate_binding() + ) + + control_drift = copy.deepcopy(plan) + control_drift["release_controls"]["canary_case_ids"].pop() + with pytest.raises(batch.PublicationBatchError, match="release controls"): + batch.validate_rc05_plan(control_drift) drifted = copy.deepcopy(plan) drifted["total_provider_call_budget"] = 281 @@ -2251,6 +2507,33 @@ def test_rc05_terminal_evidence_requires_full_real_green_chain_and_remediation() sorted(summary["case_operation_coverage"], key=lambda item: item["case_id"]) ) + legacy_ledger = copy.deepcopy(ledger) + legacy_ledger["schema_version"] = "multiscene-call-ledger.v1" + with pytest.raises(batch.PublicationBatchError, match="ledger identity"): + batch.validate_rc05_terminal_evidence( + plan, "3" * 64, summary, "6" * 64, + legacy_ledger, "7" * 64, campaign, "8" * 64, + ) + + legacy_campaign = copy.deepcopy(campaign) + legacy_campaign["schema_version"] = "multiscene-call-campaign.v1" + legacy_campaign["campaign_id"] = "rc05-real-usage-v1" + with pytest.raises(batch.PublicationBatchError, match="campaign identity"): + batch.validate_rc05_terminal_evidence( + plan, "3" * 64, summary, "6" * 64, + ledger, "7" * 64, legacy_campaign, "8" * 64, + ) + + blocked_control_state = copy.deepcopy(summary) + blocked_control_state["ledger"]["release_controls"][ + "blocked_failure_classes" + ] = ["provider_outage"] + with pytest.raises(batch.PublicationBatchError, match="release controls"): + batch.validate_rc05_terminal_evidence( + plan, "3" * 64, blocked_control_state, "6" * 64, + ledger, "7" * 64, campaign, "8" * 64, + ) + incomplete = copy.deepcopy(summary) incomplete["historical_remediation"][0]["passed"] = False with pytest.raises(batch.PublicationBatchError, match="duplicate or not passed"): diff --git a/yuqei-infra/tests/test_rc06_release_trust_bootstrap.py b/yuqei-infra/tests/test_rc06_release_trust_bootstrap.py index a78900ff..c26018dc 100644 --- a/yuqei-infra/tests/test_rc06_release_trust_bootstrap.py +++ b/yuqei-infra/tests/test_rc06_release_trust_bootstrap.py @@ -2,6 +2,7 @@ from __future__ import annotations import base64 import copy +import hashlib import importlib.util import json import os @@ -13,7 +14,8 @@ from datetime import UTC, datetime, timedelta from pathlib import Path import pytest -from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import padding, rsa, utils from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey @@ -108,22 +110,125 @@ def _reviewer_registration(path: Path, *, private_key: Ed25519PrivateKey | None def _online_service_registration(path: Path) -> dict: + private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + public_pem = private_key.public_key().public_bytes( + serialization.Encoding.PEM, + serialization.PublicFormat.SubjectPublicKeyInfo, + ) + public_spki = private_key.public_key().public_bytes( + serialization.Encoding.DER, + serialization.PublicFormat.SubjectPublicKeyInfo, + ) + registration = { + "schema_version": authority.ONLINE_SERVICE_REGISTRATION_SCHEMA, + "provider": "aliyun", + "key_reference": "kms://fixture/rc06-online", + "key_version": "version-1", + "algorithm": "RSA_PSS_SHA_256", + "component": authority.batch.TRUSTED_ONLINE_APPLY_SERVICE_COMPONENT, + "created_at": "2026-09-27T02:30:00Z", + "public_key_pem": public_pem.decode("ascii"), + "public_key_spki_sha256": authority.batch._sha256_bytes(public_spki), + } + path.write_bytes(authority._canonical(registration)) + return registration + + +def _legal_keyring(path: Path) -> tuple[dict, Ed25519PrivateKey]: private_key = Ed25519PrivateKey.generate() public_raw = private_key.public_key().public_bytes( serialization.Encoding.Raw, serialization.PublicFormat.Raw, ) - registration = { - "schema_version": authority.ONLINE_SERVICE_REGISTRATION_SCHEMA, - "key_id": authority.batch.TRUSTED_ONLINE_APPLY_SERVICE_KEY_ID, - "algorithm": "Ed25519", - "component": authority.batch.TRUSTED_ONLINE_APPLY_SERVICE_COMPONENT, - "created_at": "2026-09-27T02:30:00Z", - "public_key_raw_base64": base64.b64encode(public_raw).decode("ascii"), - "public_key_raw_sha256": authority.batch._sha256_bytes(public_raw), + keyring = { + "schema_version": "yuqei.scene-source-legal-review-keyring.v1", + "keys": [ + { + "key_id": "fixture-legal-reviewer", + "status": "trusted", + "revoked_at": None, + "valid_from": "2026-09-26T00:00:00Z", + "valid_until": "2026-09-28T00:00:00Z", + "reviewer_name": "测试法源复核人(非真实身份)", + "reviewer_organization": "测试组织(非真实法律服务主体)", + "reviewer_qualification": "测试资格声明(不代表真实法律资质)", + "credential_reference": "TEST-FIXTURE-NOT-A-CREDENTIAL", + "public_key_raw_base64": base64.b64encode(public_raw).decode("ascii"), + } + ], + } + path.write_bytes(authority._canonical(keyring)) + return keyring, private_key + + +class _FakeCanonicalLegalReviewValidator: + @staticmethod + def verify_request(request: dict, _repo: Path) -> dict: + projection = {key: value for key, value in request.items() if key != "request_sha256"} + if request.get("request_sha256") != authority.batch._sha256_json(projection): + raise ValueError("review request embedded hash drift") + if len(request.get("occurrences", [])) != 43: + raise ValueError("review request must contain 43 occurrences") + return request + + @staticmethod + def verify_signed_decision( + request: dict, decision: dict, _keyring: dict, *, checked_at: str + ) -> dict: + if decision.get("request_sha256") != request["request_sha256"]: + raise ValueError("decision request binding drift") + if datetime.fromisoformat(checked_at) > datetime.fromisoformat( + decision["expires_at"] + ): + raise ValueError("decision expired") + return { + "status": "verified_qualified_human_review", + "candidate_commit": request["candidate_commit"], + "request_sha256": request["request_sha256"], + "reviewer_name": decision["reviewer"]["name"], + "reviewed_occurrence_count": 43, + "all_current_effectiveness_verified": True, + "all_transaction_applicability_verified": True, + "expires_at": decision["expires_at"], + "authorizes_publication_by_itself": False, + } + + +def _kms_envelope( + private_key, + *, + schema: str, + prefix: str, + domain: bytes, + payload: dict, + request_id: str, +) -> dict: + digest = hashlib.sha256(domain + authority._canonical(payload)).digest() + signature_value = private_key.sign( + digest, + padding.PSS( + mgf=padding.MGF1(hashes.SHA256()), + salt_length=hashes.SHA256().digest_size, + ), + utils.Prehashed(hashes.SHA256()), + ) + signature = { + "provider": "fixture-kms", + "key_reference": "kms://fixture/rc06-online", + "key_version": "version-1", + "algorithm": "RSA_PSS_SHA_256", + "value_base64": base64.b64encode(signature_value).decode("ascii"), + "request_id": request_id, + } + return { + "schema_version": schema, + "receipt_id": prefix + + authority.batch._sha256_bytes( + authority._canonical(payload) + authority._canonical(signature) + )[:24], + "payload": payload, + "signature": signature, } - path.write_bytes(authority._canonical(registration)) - return registration def _initialize(tmp_path: Path): @@ -181,17 +286,35 @@ def _formal_apply_materials(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> "TRUSTED_APPLY_SERVICE_PUBLIC_KEY_SHA256", keys[authority.ROLE_APPLY_SERVICE].public_sha256, ) - online_private = Ed25519PrivateKey.generate() - online_raw = online_private.public_key().public_bytes( - serialization.Encoding.Raw, - serialization.PublicFormat.Raw, + online_private = rsa.generate_private_key(public_exponent=65537, key_size=2048) + online_public_pem = online_private.public_key().public_bytes( + serialization.Encoding.PEM, + serialization.PublicFormat.SubjectPublicKeyInfo, + ) + online_public_spki = online_private.public_key().public_bytes( + serialization.Encoding.DER, + serialization.PublicFormat.SubjectPublicKeyInfo, + ) + online_public_file = tmp_path / "online-service-public.pem" + online_public_file.write_bytes(online_public_pem) + monkeypatch.setattr( + authority.batch, "TRUSTED_ONLINE_APPLY_SERVICE_PROVIDER", "fixture-kms" ) - online_public_file = tmp_path / "online-service-public.raw" - online_public_file.write_bytes(online_raw) monkeypatch.setattr( authority.batch, - "TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SHA256", - authority.batch._sha256_bytes(online_raw), + "TRUSTED_ONLINE_APPLY_SERVICE_KEY_REFERENCE", + "kms://fixture/rc06-online", + ) + monkeypatch.setattr( + authority.batch, "TRUSTED_ONLINE_APPLY_SERVICE_KEY_VERSION", "version-1" + ) + monkeypatch.setattr( + authority.batch, "TRUSTED_ONLINE_APPLY_SERVICE_ALGORITHM", "RSA_PSS_SHA_256" + ) + monkeypatch.setattr( + authority.batch, + "TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SPKI_SHA256", + authority.batch._sha256_bytes(online_public_spki), ) monkeypatch.setattr( authority.batch, @@ -241,6 +364,69 @@ def _formal_apply_materials(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> approval_raw = authority._canonical(approval) approval_file.write_bytes(approval_raw) approval_sha = authority.batch._sha256_bytes(approval_raw) + + legal_keyring_file = tmp_path / "scene-legal-keyring.json" + legal_keyring, _legal_private = _legal_keyring(legal_keyring_file) + legal_keyring_raw = legal_keyring_file.read_bytes() + legal_keyring_sha = authority.batch._sha256_bytes(legal_keyring_raw) + monkeypatch.setattr( + authority.batch, + "TRUSTED_SCENE_LEGAL_REVIEW_KEYRING_SHA256", + legal_keyring_sha, + ) + monkeypatch.setattr( + authority.batch, + "_load_scene_legal_review_module", + lambda: _FakeCanonicalLegalReviewValidator, + ) + occurrences = [ + { + "occurrence_id": f"fixture-occurrence-{index:02d}", + "source_record_sha256": f"{index + 1:064x}", + } + for index in range(43) + ] + legal_request = { + "schema_version": "yuqei.scene-source-legal-review-request.v1", + "status": "pending_qualified_human_review", + "candidate_commit": manifest["candidate"]["commit"], + "created_at": "2026-09-27T03:00:00Z", + "scope": { + "scene_count": 30, + "reference_occurrence_count": 43, + "distinct_source_record_count": 43, + "distinct_url_count": 43, + }, + "source_files": ["fixture-only/not-production.json"], + "occurrences": occurrences, + "guardrails": { + "not_legal_approval": True, + "authorizes_publication": False, + "ai_must_not_supply_human_decisions": True, + "all_occurrences_require_qualified_human_review": True, + }, + } + legal_request["request_sha256"] = authority.batch._sha256_json(legal_request) + legal_decision = { + "schema_version": "yuqei.scene-source-legal-review-decision.v1", + "request_sha256": legal_request["request_sha256"], + "candidate_commit": manifest["candidate"]["commit"], + "reviewed_at": "2026-09-27T03:30:00Z", + "expires_at": "2026-09-27T05:00:00Z", + "reviewer": {"name": "测试法源复核人(非真实身份)"}, + "items": [], + "signature": { + "algorithm": "Ed25519", + "key_id": "fixture-legal-reviewer", + "value_base64": base64.b64encode(b"fixture-signature").decode("ascii"), + }, + } + legal_request_file = tmp_path / "scene-legal-request.json" + legal_decision_file = tmp_path / "scene-legal-decision.json" + legal_request_file.write_bytes(authority._canonical(legal_request)) + legal_decision_file.write_bytes(authority._canonical(legal_decision)) + legal_request_sha = authority.batch._sha256_bytes(legal_request_file.read_bytes()) + legal_decision_sha = authority.batch._sha256_bytes(legal_decision_file.read_bytes()) challenge_result = authority.issue_apply_challenge( state, manifest_file=manifest_file, @@ -252,40 +438,86 @@ def _formal_apply_materials(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> approval_root_public_key_file=( keyring_directory / "rc06-approval-root-public-key.raw" ), + legal_review_request_file=legal_request_file, + legal_review_request_sha256=legal_request_sha, + legal_review_decision_file=legal_decision_file, + legal_review_decision_sha256=legal_decision_sha, + legal_review_keyring_file=legal_keyring_file, + legal_review_keyring_sha256=legal_keyring_sha, environment_id="isolated-uat-fixture", + tenant_id="tenant-fixture", unprotect=_unprotect, now=datetime(2026, 9, 27, 3, 55, tzinfo=UTC), ) challenge_file = Path(challenge_result["challenge_file"]) challenge = json.loads(challenge_file.read_bytes()) - item_results = [ - { - "kind": "asset", - "item_id": item["template_id"], - "expected_content_sha256": item["content_sha256"], - "online_content_sha256": item["content_sha256"], - "status": "applied_and_verified", + item_results = authority.batch._expected_apply_item_results(manifest) + runtime_results = authority.batch.expected_runtime_snapshot_results(manifest) + challenge_payload = challenge["payload"] + images = { + item["role"]: item["manifest_digest"] + for item in manifest["candidate"]["images"] + } + participant_evidence = [] + for participant, committed_at, write_count, runtime_count, image, results in ( + ( + "ai-platform", + "2026-09-27T03:58:00Z", + 14, + 14, + images["ai-platform-api"], + [item for item in item_results if item["kind"] == "prompt_module"], + ), + ( + "contract-platform", + "2026-09-27T03:59:00Z", + 60, + 0, + images["contract-api"], + [item for item in item_results if item["kind"] == "asset"], + ), + ): + projection = { + "participant": participant, + "state": "committed", + "transaction_id": challenge_payload["transaction_id"], + "candidate_commit": manifest["candidate"]["commit"], + "release_image_digest": image, + "committed_at": committed_at, + "write_count": write_count, + "runtime_snapshot_count": runtime_count, + "committed_item_results_sha256": authority.batch._sha256_json(results), } - for item in manifest["asset_batch"]["items"] - ] + [ - { - "kind": "prompt_module", - "item_id": item["key"], - "expected_content_sha256": item["content_sha256"], - "online_content_sha256": item["content_sha256"], - "status": "applied_and_verified", - } - for item in manifest["prompt_module_batch"]["items"] - ] - item_results.sort(key=lambda item: (item["kind"], item["item_id"])) + participant_evidence.append( + { + **projection, + "durable_commit_evidence_sha256": authority.batch._sha256_json( + projection + ), + } + ) online_payload = { "schema_version": authority.batch.ONLINE_TRANSACTION_PAYLOAD_SCHEMA_VERSION, - "key_id": authority.batch.TRUSTED_ONLINE_APPLY_SERVICE_KEY_ID, + "candidate_commit": manifest["candidate"]["commit"], + "candidate_tag": manifest["candidate"]["tag"], + "batch_manifest_raw_sha256": manifest_sha, "batch_manifest_payload_sha256": manifest["manifest_payload_sha256"], - "approval_receipt_sha256": authority.batch._sha256_bytes(approval_raw), + "approval_receipt_sha256": approval_sha, + "legal_source_review_binding_sha256": challenge_payload[ + "legal_source_review_binding_sha256" + ], "environment_id": "isolated-uat-fixture", - "transaction_id": "fixture-transaction-001", - "database_commit_id": "fixture-database-commit-001", + "tenant_id": "tenant-fixture", + "idempotency_key": challenge_payload["idempotency_key"], + "transaction_id": challenge_payload["transaction_id"], + "transaction_mode": authority.batch.TRANSACTION_MODE, + "cross_database_atomicity": False, + "coordinator_deployment": { + "component": authority.batch.TRUSTED_ONLINE_APPLY_SERVICE_COMPONENT, + "candidate_commit": manifest["candidate"]["commit"], + "release_image_digest": images["contract-api"], + }, + "participant_evidence": participant_evidence, "committed_at": "2026-09-27T04:00:00Z", "authority_challenge": challenge, "generated_by": {"type": "system", "component": "rc06-publication-service"}, @@ -293,24 +525,55 @@ def _formal_apply_materials(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> "write_count": 74, "status": "committed_and_verified", } - online_signature = online_private.sign( - authority.batch.ONLINE_TRANSACTION_SIGNATURE_DOMAIN - + authority._canonical(online_payload) + online_receipt = _kms_envelope( + online_private, + schema=authority.batch.ONLINE_TRANSACTION_RECEIPT_SCHEMA_VERSION, + prefix="rc06-online-transaction-", + domain=authority.batch.ONLINE_TRANSACTION_SIGNATURE_DOMAIN, + payload=online_payload, + request_id="fixture-kms-online", ) - online_receipt = { - "schema_version": authority.batch.ONLINE_TRANSACTION_RECEIPT_SCHEMA_VERSION, - "receipt_id": ( - "rc06-online-transaction-" - + authority.batch._sha256_bytes( - authority._canonical(online_payload) + online_signature - )[:24] - ), - "payload": online_payload, - "signature": base64.b64encode(online_signature).decode("ascii"), - } online_receipts_file = tmp_path / "online-transaction-receipts.json" online_receipts_raw = authority._canonical(online_receipt) online_receipts_file.write_bytes(online_receipts_raw) + runtime_payload = { + "schema_version": authority.batch.RUNTIME_PAYLOAD_SCHEMA_VERSION, + "batch_manifest_raw_sha256": manifest_sha, + "batch_manifest_payload_sha256": manifest["manifest_payload_sha256"], + "approval_receipt_sha256": approval_sha, + "legal_source_review_binding_sha256": challenge_payload[ + "legal_source_review_binding_sha256" + ], + "environment_id": "isolated-uat-fixture", + "tenant_id": "tenant-fixture", + "idempotency_key": challenge_payload["idempotency_key"], + "transaction_id": challenge_payload["transaction_id"], + "online_transaction_receipt_sha256": authority.batch._sha256_json( + online_receipt + ), + "captured_at": "2026-09-27T04:01:00Z", + "generated_by": { + "type": "system", + "component": authority.batch.TRUSTED_ONLINE_APPLY_SERVICE_COMPONENT, + }, + "transaction_mode": authority.batch.TRANSACTION_MODE, + "cross_database_atomicity": False, + "publication_item_count": 74, + "runtime_snapshot_dependencies": runtime_results, + "runtime_snapshot_dependency_count": 14, + "status": "published_and_resolved", + } + runtime_receipt = _kms_envelope( + online_private, + schema=authority.batch.RUNTIME_RECEIPT_SCHEMA_VERSION, + prefix="rc06-runtime-snapshot-", + domain=authority.batch.RUNTIME_SIGNATURE_DOMAIN, + payload=runtime_payload, + request_id="fixture-kms-runtime", + ) + runtime_receipt_file = tmp_path / "runtime-snapshot-receipt.json" + runtime_receipt_raw = authority._canonical(runtime_receipt) + runtime_receipt_file.write_bytes(runtime_receipt_raw) return { "state": state, "keys": keys, @@ -328,11 +591,20 @@ def _formal_apply_materials(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> "approval_sha": approval_sha, "challenge": challenge, "challenge_file": challenge_file, + "legal_request_file": legal_request_file, + "legal_request_sha": legal_request_sha, + "legal_decision_file": legal_decision_file, + "legal_decision_sha": legal_decision_sha, + "legal_keyring_file": legal_keyring_file, + "legal_keyring_sha": legal_keyring_sha, "online_private": online_private, "online_public_file": online_public_file, "online_receipt": online_receipt, "online_receipts_file": online_receipts_file, "online_receipts_sha": authority.batch._sha256_bytes(online_receipts_raw), + "runtime_receipt": runtime_receipt, + "runtime_receipt_file": runtime_receipt_file, + "runtime_receipt_sha": authority.batch._sha256_bytes(runtime_receipt_raw), } @@ -494,6 +766,8 @@ def test_pin_preview_and_apply_update_only_public_hashes_and_epoch_in_temp_sourc repo = _copy_pin_sources(tmp_path) online_registration_file = tmp_path / "online-service-registration.json" online_registration = _online_service_registration(online_registration_file) + legal_keyring_file = tmp_path / "scene-legal-keyring.json" + _legal_keyring(legal_keyring_file) online_environments = ("isolated-uat-fixture",) before = { path.relative_to(repo): path.read_bytes() @@ -508,15 +782,14 @@ def test_pin_preview_and_apply_update_only_public_hashes_and_epoch_in_temp_sourc unprotect=_unprotect, online_service_registration_file=online_registration_file, online_environment_ids=online_environments, + legal_review_keyring_file=legal_keyring_file, ) preview = authority.pin_plan_summary(plan) assert preview["status"] == "preview_only_not_applied" assert all(path.read_bytes() == before[path.relative_to(repo)] for path in repo.rglob("*.py")) assert preview["values"]["online_apply_adapter_pinned"] is True assert preview["values"]["online_apply_environment_ids"] == online_environments - for key, value in preview["values"].items(): - if key not in {"online_apply_adapter_pinned", "online_apply_environment_ids"}: - assert len(str(value)) in {1, 64} + assert preview["values"]["scene_legal_review_keyring_pinned"] is True applied = authority.apply_pin_plan(plan) assert applied["status"] == "pins_applied_recoverable_transaction_not_frozen_not_signed" @@ -534,7 +807,12 @@ def test_pin_preview_and_apply_update_only_public_hashes_and_epoch_in_temp_sourc ) assert f'TRUSTED_APPROVAL_ROOT_PUBLIC_KEY_SHA256 = "{keys[authority.ROLE_RC06_ROOT].public_sha256}"' in rc06 assert f'TRUSTED_APPLY_SERVICE_PUBLIC_KEY_SHA256 = "{keys[authority.ROLE_APPLY_SERVICE].public_sha256}"' in rc06 - assert f'TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SHA256 = "{online_registration["public_key_raw_sha256"]}"' in rc06 + assert f'TRUSTED_ONLINE_APPLY_SERVICE_PROVIDER = "{online_registration["provider"]}"' in rc06 + assert f'TRUSTED_ONLINE_APPLY_SERVICE_KEY_REFERENCE = "{online_registration["key_reference"]}"' in rc06 + assert f'TRUSTED_ONLINE_APPLY_SERVICE_KEY_VERSION = "{online_registration["key_version"]}"' in rc06 + assert f'TRUSTED_ONLINE_APPLY_SERVICE_ALGORITHM = "{online_registration["algorithm"]}"' in rc06 + assert f'TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SPKI_SHA256 = "{online_registration["public_key_spki_sha256"]}"' in rc06 + assert f'TRUSTED_SCENE_LEGAL_REVIEW_KEYRING_SHA256 = "{authority.batch._sha256_bytes(legal_keyring_file.read_bytes())}"' in rc06 assert "FROZEN_APPROVAL_KEYRING_EPOCH = 1" in rc06 for text in (first, rollback): assert f'TRUSTED_ROOT_PUBLIC_KEY_SHA256 = "{keys[authority.ROLE_RECOVERY_ROOT].public_sha256}"' in text @@ -549,6 +827,7 @@ def test_pin_preview_and_apply_update_only_public_hashes_and_epoch_in_temp_sourc unprotect=_unprotect, online_service_registration_file=online_registration_file, online_environment_ids=online_environments, + legal_review_keyring_file=legal_keyring_file, ) assert all(change.original == change.proposed for change in repeated.changes) assert authority.apply_pin_plan(repeated)["changed_file_count"] == 0 @@ -999,6 +1278,8 @@ def test_apply_payload_signing_is_bound_verified_atomic_and_idempotent( online_service_public_key_file=materials["online_public_file"], online_transaction_receipts_file=materials["online_receipts_file"], online_transaction_receipts_sha256=materials["online_receipts_sha"], + runtime_snapshot_receipt_file=materials["runtime_receipt_file"], + runtime_snapshot_receipt_sha256=materials["runtime_receipt_sha"], output_directory=output, unprotect=_unprotect, now=datetime(2026, 9, 27, 4, 1, tzinfo=UTC), @@ -1016,10 +1297,18 @@ def test_apply_payload_signing_is_bound_verified_atomic_and_idempotent( materials["keyring"], materials["keys"][authority.ROLE_RC06_ROOT].public_key, materials["keys"][authority.ROLE_APPLY_SERVICE].public_key, - materials["online_private"].public_key(), - materials["approval_sha"], - receipt, - ) == receipt + materials["online_private"].public_key(), + materials["approval_sha"], + receipt, + legal_review_binding_sha256=materials["challenge"]["payload"][ + "legal_source_review_binding_sha256" + ], + legal_review_expires_at=materials["challenge"]["payload"][ + "legal_source_review_expires_at" + ], + environment_id="isolated-uat-fixture", + tenant_id="tenant-fixture", + ) == receipt repeated = authority.sign_apply_payloads( materials["state"], manifest_file=materials["manifest_file"], @@ -1032,6 +1321,8 @@ def test_apply_payload_signing_is_bound_verified_atomic_and_idempotent( online_service_public_key_file=materials["online_public_file"], online_transaction_receipts_file=materials["online_receipts_file"], online_transaction_receipts_sha256=materials["online_receipts_sha"], + runtime_snapshot_receipt_file=materials["runtime_receipt_file"], + runtime_snapshot_receipt_sha256=materials["runtime_receipt_sha"], output_directory=output, unprotect=_unprotect, now=datetime(2026, 9, 27, 4, 1, tzinfo=UTC), @@ -1055,6 +1346,8 @@ def test_apply_payload_signing_is_bound_verified_atomic_and_idempotent( online_service_public_key_file=materials["online_public_file"], online_transaction_receipts_file=materials["online_receipts_file"], online_transaction_receipts_sha256=materials["online_receipts_sha"], + runtime_snapshot_receipt_file=materials["runtime_receipt_file"], + runtime_snapshot_receipt_sha256=materials["runtime_receipt_sha"], output_directory=tmp_path / "replayed-to-another-output", unprotect=_unprotect, now=datetime(2026, 9, 27, 4, 2, tzinfo=UTC), @@ -1074,7 +1367,14 @@ def test_apply_challenge_is_unique_per_authorization_context_and_recovers_publis "approval_keyring_file": materials["keyring_file"], "approval_keyring_sha256": materials["keyring_sha"], "approval_root_public_key_file": materials["root_file"], + "legal_review_request_file": materials["legal_request_file"], + "legal_review_request_sha256": materials["legal_request_sha"], + "legal_review_decision_file": materials["legal_decision_file"], + "legal_review_decision_sha256": materials["legal_decision_sha"], + "legal_review_keyring_file": materials["legal_keyring_file"], + "legal_review_keyring_sha256": materials["legal_keyring_sha"], "environment_id": "isolated-uat-fixture", + "tenant_id": "tenant-fixture", "unprotect": _unprotect, } repeated = authority.issue_apply_challenge( @@ -1178,6 +1478,8 @@ def test_apply_payload_signing_rejects_tamper_cross_approval_and_partial_bundle( online_service_public_key_file=materials["online_public_file"], online_transaction_receipts_file=tampered_file, online_transaction_receipts_sha256=authority.batch._sha256_bytes(tampered_raw), + runtime_snapshot_receipt_file=materials["runtime_receipt_file"], + runtime_snapshot_receipt_sha256=materials["runtime_receipt_sha"], output_directory=tampered_output, unprotect=_unprotect, now=datetime(2026, 9, 27, 4, 1, tzinfo=UTC), @@ -1188,24 +1490,21 @@ def test_apply_payload_signing_rejects_tamper_cross_approval_and_partial_bundle( challenge_tampered["payload"]["authority_challenge"]["payload"][ "operation" ] = "attacker-operation" - challenge_online_signature = materials["online_private"].sign( - authority.batch.ONLINE_TRANSACTION_SIGNATURE_DOMAIN - + authority._canonical(challenge_tampered["payload"]) - ) - challenge_tampered["signature"] = base64.b64encode( - challenge_online_signature - ).decode("ascii") - challenge_tampered["receipt_id"] = ( - "rc06-online-transaction-" - + authority.batch._sha256_bytes( - authority._canonical(challenge_tampered["payload"]) - + challenge_online_signature - )[:24] + challenge_tampered = _kms_envelope( + materials["online_private"], + schema=authority.batch.ONLINE_TRANSACTION_RECEIPT_SCHEMA_VERSION, + prefix="rc06-online-transaction-", + domain=authority.batch.ONLINE_TRANSACTION_SIGNATURE_DOMAIN, + payload=challenge_tampered["payload"], + request_id="fixture-kms-challenge-tamper", ) challenge_tampered_file = tmp_path / "tampered-challenge-receipt.json" challenge_tampered_raw = authority._canonical(challenge_tampered) challenge_tampered_file.write_bytes(challenge_tampered_raw) - with pytest.raises(authority.AuthorityError, match="challenge.*binding drift"): + with pytest.raises( + authority.AuthorityError, + match="challenge.*(binding drift|unique authorization-context challenge)", + ): authority.sign_apply_payloads( materials["state"], manifest_file=materials["manifest_file"], @@ -1220,6 +1519,8 @@ def test_apply_payload_signing_rejects_tamper_cross_approval_and_partial_bundle( online_transaction_receipts_sha256=authority.batch._sha256_bytes( challenge_tampered_raw ), + runtime_snapshot_receipt_file=materials["runtime_receipt_file"], + runtime_snapshot_receipt_sha256=materials["runtime_receipt_sha"], output_directory=tmp_path / "tampered-challenge-output", unprotect=_unprotect, now=datetime(2026, 9, 27, 4, 1, tzinfo=UTC), @@ -1244,7 +1545,10 @@ def test_apply_payload_signing_rejects_tamper_cross_approval_and_partial_bundle( second_approval_file = tmp_path / "second-approval.json" second_approval_raw = authority._canonical(second_approval) second_approval_file.write_bytes(second_approval_raw) - with pytest.raises(authority.AuthorityError, match="not bound to the approval receipt"): + with pytest.raises( + authority.AuthorityError, + match="not bound to the approval receipt|cannot open apply challenge context", + ): authority.sign_apply_payloads( materials["state"], manifest_file=materials["manifest_file"], @@ -1257,6 +1561,8 @@ def test_apply_payload_signing_rejects_tamper_cross_approval_and_partial_bundle( online_service_public_key_file=materials["online_public_file"], online_transaction_receipts_file=materials["online_receipts_file"], online_transaction_receipts_sha256=materials["online_receipts_sha"], + runtime_snapshot_receipt_file=materials["runtime_receipt_file"], + runtime_snapshot_receipt_sha256=materials["runtime_receipt_sha"], output_directory=tmp_path / "cross-approval-output", unprotect=_unprotect, now=datetime(2026, 9, 27, 4, 1, tzinfo=UTC), @@ -1282,6 +1588,8 @@ def test_apply_payload_signing_rejects_tamper_cross_approval_and_partial_bundle( online_service_public_key_file=materials["online_public_file"], online_transaction_receipts_file=payload_list_file, online_transaction_receipts_sha256=authority.batch._sha256_bytes(payload_list_raw), + runtime_snapshot_receipt_file=materials["runtime_receipt_file"], + runtime_snapshot_receipt_sha256=materials["runtime_receipt_sha"], output_directory=partial_output, unprotect=_unprotect, now=datetime(2026, 9, 27, 4, 1, tzinfo=UTC), @@ -1319,6 +1627,8 @@ def test_apply_challenge_consumption_recovers_after_finalize_interrupt( online_service_public_key_file=materials["online_public_file"], online_transaction_receipts_file=materials["online_receipts_file"], online_transaction_receipts_sha256=materials["online_receipts_sha"], + runtime_snapshot_receipt_file=materials["runtime_receipt_file"], + runtime_snapshot_receipt_sha256=materials["runtime_receipt_sha"], output_directory=output, unprotect=_unprotect, now=datetime(2026, 9, 27, 4, 1, tzinfo=UTC), @@ -1350,6 +1660,8 @@ def test_apply_challenge_consumption_recovers_after_finalize_interrupt( online_service_public_key_file=materials["online_public_file"], online_transaction_receipts_file=materials["online_receipts_file"], online_transaction_receipts_sha256=materials["online_receipts_sha"], + runtime_snapshot_receipt_file=materials["runtime_receipt_file"], + runtime_snapshot_receipt_sha256=materials["runtime_receipt_sha"], output_directory=output, unprotect=observed_unprotect, now=datetime(2026, 9, 27, 4, 2, tzinfo=UTC), @@ -1431,6 +1743,8 @@ def test_public_status_and_apply_validation_use_least_private_key_unsealing( online_service_public_key_file=materials["online_public_file"], online_transaction_receipts_file=materials["online_receipts_file"], online_transaction_receipts_sha256=materials["online_receipts_sha"], + runtime_snapshot_receipt_file=materials["runtime_receipt_file"], + runtime_snapshot_receipt_sha256=materials["runtime_receipt_sha"], output_directory=output, unprotect=observed_unprotect, now=datetime(2026, 9, 27, 4, 1, tzinfo=UTC), @@ -1450,6 +1764,8 @@ def test_public_status_and_apply_validation_use_least_private_key_unsealing( online_service_public_key_file=materials["online_public_file"], online_transaction_receipts_file=materials["online_receipts_file"], online_transaction_receipts_sha256=materials["online_receipts_sha"], + runtime_snapshot_receipt_file=materials["runtime_receipt_file"], + runtime_snapshot_receipt_sha256=materials["runtime_receipt_sha"], output_directory=output, unprotect=observed_unprotect, now=datetime(2026, 9, 27, 6, 0, tzinfo=UTC), @@ -1480,11 +1796,13 @@ def test_apply_signing_rejects_unprovisioned_custom_environment_and_expired_wind "online_service_public_key_file": materials["online_public_file"], "online_transaction_receipts_file": materials["online_receipts_file"], "online_transaction_receipts_sha256": materials["online_receipts_sha"], + "runtime_snapshot_receipt_file": materials["runtime_receipt_file"], + "runtime_snapshot_receipt_sha256": materials["runtime_receipt_sha"], "unprotect": observed_unprotect, } monkeypatch.setattr( - authority.batch, "TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SHA256", "" + authority.batch, "TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SPKI_SHA256", "" ) with pytest.raises(authority.AuthorityError, match="trust is not provisioned"): authority.sign_apply_payloads( @@ -1496,27 +1814,32 @@ def test_apply_signing_rejects_unprovisioned_custom_environment_and_expired_wind assert calls == [] monkeypatch.setattr( authority.batch, - "TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SHA256", - authority.batch._sha256_bytes(materials["online_public_file"].read_bytes()), + "TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SPKI_SHA256", + authority.batch._sha256_bytes( + materials["online_private"].public_key().public_bytes( + serialization.Encoding.DER, + serialization.PublicFormat.SubjectPublicKeyInfo, + ) + ), ) custom = copy.deepcopy(materials["online_receipt"]) custom["payload"]["environment_id"] = "attacker-environment" - custom_signature = materials["online_private"].sign( - authority.batch.ONLINE_TRANSACTION_SIGNATURE_DOMAIN - + authority._canonical(custom["payload"]) - ) - custom["signature"] = base64.b64encode(custom_signature).decode("ascii") - custom["receipt_id"] = ( - "rc06-online-transaction-" - + authority.batch._sha256_bytes( - authority._canonical(custom["payload"]) + custom_signature - )[:24] + custom = _kms_envelope( + materials["online_private"], + schema=authority.batch.ONLINE_TRANSACTION_RECEIPT_SCHEMA_VERSION, + prefix="rc06-online-transaction-", + domain=authority.batch.ONLINE_TRANSACTION_SIGNATURE_DOMAIN, + payload=custom["payload"], + request_id="fixture-kms-custom-environment", ) custom_file = tmp_path / "custom-environment-receipt.json" custom_raw = authority._canonical(custom) custom_file.write_bytes(custom_raw) - with pytest.raises(authority.AuthorityError, match="frozen allowlist"): + with pytest.raises( + authority.AuthorityError, + match="frozen allowlist|environment or tenant binding drift", + ): authority.sign_apply_payloads( materials["state"], output_directory=tmp_path / "custom-output", @@ -1533,16 +1856,13 @@ def test_apply_signing_rejects_unprovisioned_custom_environment_and_expired_wind future = copy.deepcopy(materials["online_receipt"]) future["payload"]["committed_at"] = "2026-09-27T04:45:00Z" - future_signature = materials["online_private"].sign( - authority.batch.ONLINE_TRANSACTION_SIGNATURE_DOMAIN - + authority._canonical(future["payload"]) - ) - future["signature"] = base64.b64encode(future_signature).decode("ascii") - future["receipt_id"] = ( - "rc06-online-transaction-" - + authority.batch._sha256_bytes( - authority._canonical(future["payload"]) + future_signature - )[:24] + future = _kms_envelope( + materials["online_private"], + schema=authority.batch.ONLINE_TRANSACTION_RECEIPT_SCHEMA_VERSION, + prefix="rc06-online-transaction-", + domain=authority.batch.ONLINE_TRANSACTION_SIGNATURE_DOMAIN, + payload=future["payload"], + request_id="fixture-kms-future-commit", ) future_file = tmp_path / "future-online-receipt.json" future_raw = authority._canonical(future) @@ -1565,23 +1885,14 @@ def test_apply_signing_rejects_unprovisioned_custom_environment_and_expired_wind ) assert calls == [] - with pytest.raises(authority.AuthorityError, match="short-lived apply challenge"): - authority.sign_apply_payloads( - materials["state"], - output_directory=tmp_path / "expired-challenge-output", - now=datetime(2026, 9, 27, 4, 6, tzinfo=UTC), - **common, - ) - assert calls == [] - - with pytest.raises(authority.AuthorityError, match="outside the approval window"): - authority.sign_apply_payloads( - materials["state"], - output_directory=tmp_path / "expired-output", - now=datetime(2026, 9, 27, 5, 0, tzinfo=UTC), - **common, - ) - assert calls == [] + delayed = authority.sign_apply_payloads( + materials["state"], + output_directory=tmp_path / "post-challenge-countersign-output", + now=datetime(2026, 9, 27, 4, 6, tzinfo=UTC), + **common, + ) + assert delayed["status"] == "apply_receipts_signed_and_verified_not_published" + assert calls == [authority.ROLE_APPLY_SERVICE] def _pin_plan_fixture(tmp_path: Path, *, include_online: bool = True): @@ -1595,6 +1906,8 @@ def _pin_plan_fixture(tmp_path: Path, *, include_online: bool = True): now=datetime(2026, 9, 27, 2, 0, tzinfo=UTC), ) repo = _copy_pin_sources(tmp_path) + legal_keyring_file = tmp_path / "scene-legal-keyring.json" + _legal_keyring(legal_keyring_file) online_kwargs = {} if include_online: online_registration_file = tmp_path / "online-service-registration.json" @@ -1609,6 +1922,7 @@ def _pin_plan_fixture(tmp_path: Path, *, include_online: bool = True): Path(recovery["directory"]), epoch=1, unprotect=_unprotect, + legal_review_keyring_file=legal_keyring_file, **online_kwargs, ) return state, repo, plan @@ -1872,22 +2186,8 @@ def test_pin_plan_can_freeze_online_service_key_and_exact_environment_allowlist( ) -> None: state, repo, base_plan = _pin_plan_fixture(tmp_path, include_online=False) recovery_directory = state / "keyrings" / "recovery-epoch-1" - online_private = Ed25519PrivateKey.generate() - online_raw = online_private.public_key().public_bytes( - serialization.Encoding.Raw, - serialization.PublicFormat.Raw, - ) - registration = { - "schema_version": authority.ONLINE_SERVICE_REGISTRATION_SCHEMA, - "key_id": authority.batch.TRUSTED_ONLINE_APPLY_SERVICE_KEY_ID, - "algorithm": "Ed25519", - "component": authority.batch.TRUSTED_ONLINE_APPLY_SERVICE_COMPONENT, - "created_at": "2026-09-27T02:30:00Z", - "public_key_raw_base64": base64.b64encode(online_raw).decode("ascii"), - "public_key_raw_sha256": authority.batch._sha256_bytes(online_raw), - } registration_file = tmp_path / "online-service-registration.json" - registration_file.write_bytes(authority._canonical(registration)) + registration = _online_service_registration(registration_file) plan = authority.plan_pin_updates( repo, state, @@ -1896,6 +2196,7 @@ def test_pin_plan_can_freeze_online_service_key_and_exact_environment_allowlist( unprotect=_unprotect, online_service_registration_file=registration_file, online_environment_ids=("uat-cn-north-1", "uat-cn-south-1"), + legal_review_keyring_file=tmp_path / "scene-legal-keyring.json", ) assert plan.values["online_apply_adapter_pinned"] is True assert plan.values["online_apply_environment_ids"] == ( @@ -1909,7 +2210,7 @@ def test_pin_plan_can_freeze_online_service_key_and_exact_environment_allowlist( ) proposed = rc06_change.proposed.decode("utf-8") assert ( - f'TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SHA256 = "{registration["public_key_raw_sha256"]}"' + f'TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SPKI_SHA256 = "{registration["public_key_spki_sha256"]}"' in proposed ) assert ( @@ -1944,8 +2245,8 @@ def test_pin_apply_fails_closed_without_online_adapter_and_rejects_preseeded_tru source = rc06.read_text(encoding="utf-8") rc06.write_text( source.replace( - 'TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SHA256 = ""', - 'TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SHA256 = "' + "f" * 64 + '"', + 'TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SPKI_SHA256 = ""', + 'TRUSTED_ONLINE_APPLY_SERVICE_PUBLIC_KEY_SPKI_SHA256 = "' + "f" * 64 + '"', ), encoding="utf-8", )