"""Operator approval signing — mandatory, content- and deployment-bound. Every human approval (executor approve / maild approve-draft / publisher approve) must carry a BIP-340 signature made with the operator key: message = sha256("cayce-ops::::") where content_hash binds the approval to the exact approved content: proposal: canonical json of {kind, title, url, detail} mail: canonical json of {to_addr, subject, body} content: canonical json of {channel, caption, link} There is NO degraded mode: without CAYCE_OPS_OPERATOR_PUBKEY and CAYCE_OPS_DEPLOYMENT_ID the daemons refuse to execute/send/publish (see cayceops.config.Config.signing_credentials). """ from __future__ import annotations import hashlib import json class SignatureError(RuntimeError): """Raised when an approval signature is missing, invalid, or foreign.""" def _canonical(obj: dict) -> str: return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False) def _sha256_hex(text: str) -> str: return hashlib.sha256(text.encode("utf-8")).hexdigest() def proposal_content_hash(kind: str, title: str, url: str | None, detail: dict | None) -> str: return _sha256_hex( _canonical( { "kind": kind, "title": title, "url": url, "detail": detail or {}, } ) ) def mail_content_hash(to_addr: str | None, subject: str | None, body: str | None) -> str: return _sha256_hex( _canonical( { "to_addr": to_addr, "subject": subject, "body": body, } ) ) def content_content_hash(channel: str, caption: str, link: str | None) -> str: return _sha256_hex( _canonical( { "channel": channel, "caption": caption, "link": link, } ) ) def approval_message( item_type: str, deployment_id: str, item_id: int, content_hash: str ) -> bytes: """The exact 32-byte message the operator signs.""" raw = f"cayce-ops:{item_type}:{deployment_id}:{item_id}:{content_hash}" return hashlib.sha256(raw.encode("utf-8")).digest() def verify_signature( item_type: str, deployment_id: str, item_id: int, content_hash: str, signature_hex: str | None, operator_pubkey_hex: str, ) -> bool: """Verify an operator approval signature. Foreign deployments or edited content verify False because both are inside the signed message.""" if not signature_hex or not operator_pubkey_hex or not deployment_id: return False from cayceops.components.publisher.nostr import _schnorr_verify try: return _schnorr_verify( bytes.fromhex(signature_hex), approval_message(item_type, deployment_id, item_id, content_hash), bytes.fromhex(operator_pubkey_hex), ) except (ValueError, TypeError): return False