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.
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user