Files
cayce-ops/cayceops/signing.py
T
cayce 40453aadb4 audit-1 (critical 1): mandatory operator signatures bound to content+deployment
- signing module: canonical content hashes + approval_message
  sha256(cayce-ops:<type>:<deployment_id>:<id>:<content_hash>)
- approve/approve-draft/publisher-approve REQUIRE signature; without
  CAYCE_OPS_OPERATOR_PUBKEY+DEPLOYMENT_ID the daemons refuse to
  execute/send/publish (no degraded mode)
- run-time re-verification at execute/send/publish: unsigned rows (raw SQL
  approval) or edited content revert to pre-approval state, audited as error
- FULL signatures stored on rows (operator_signature, migration 0003) and
  in audit detail — never truncated
- signature failures audited from CLI wrappers too (1f)
- AUTONOMOUS_MODE approval path removed (D13): no non-human approvals

tests: SQL-approved row refused at execute/send/publish; to_addr change
blocks send; foreign deployment/content signatures refused; full-signature
persistence. 65 tests green.
2026-10-20 02:20:09 +00:00

104 lines
3.0 KiB
Python

"""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:<type>:<deployment_id>:<id>:<content_hash>")
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