From 82b57313398f6406e61721f05400d0527f6c0892 Mon Sep 17 00:00:00 2001 From: agent-56a7678c6cd71659 Date: Wed, 29 Apr 2026 21:24:03 +0200 Subject: [PATCH] build(agent): jabba#56a767 iteration --- missionledger/csfd.py | 45 +++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 1 + tests/test_csfd.py | 25 ++++++++++++++++++++++++ 3 files changed, 71 insertions(+) diff --git a/missionledger/csfd.py b/missionledger/csfd.py index a2de1d2..8c15980 100644 --- a/missionledger/csfd.py +++ b/missionledger/csfd.py @@ -4,6 +4,18 @@ from dataclasses import dataclass, asdict from typing import Any, Dict, List import hashlib import json +from typing import Optional + +try: + from nacl.signing import SigningKey, VerifyKey + from nacl.exceptions import BadSignatureError + _HAS_PYNACL = True +except Exception: + # Keep module import-safe for environments without PyNaCl installed. + SigningKey = None + VerifyKey = None + BadSignatureError = Exception + _HAS_PYNACL = False from .dsl import PlanDelta @@ -97,3 +109,36 @@ def generate_csfd(plan_delta: PlanDelta, signer: str = "", n_frames: int = 3) -> csfd = CSFDDigest(frames=frames, signer=signer, digest_hex=digest_hex) return csfd + + +def sign_csfd(csfd: CSFDDigest, signing_key_bytes: bytes) -> str: + """Sign a CSFD digest with an ed25519 private key (SigningKey bytes). + + Returns the signature as a hex string. Requires PyNaCl. + """ + if not _HAS_PYNACL: + raise RuntimeError("PyNaCl is required for signing CSFDs") + + if not signing_key_bytes: + raise ValueError("signing_key_bytes must be provided") + + sk = SigningKey(signing_key_bytes) + # Sign the compact digest_hex to produce a small signature + sig = sk.sign(bytes.fromhex(csfd.digest_hex)).signature + return sig.hex() + + +def verify_csfd_signature(csfd: CSFDDigest, signature_hex: str, verify_key_bytes: bytes) -> bool: + """Verify a previously-created signature on the CSFD digest. + + Returns True if valid, False otherwise. Requires PyNaCl. + """ + if not _HAS_PYNACL: + raise RuntimeError("PyNaCl is required for signature verification") + + vk = VerifyKey(verify_key_bytes) + try: + vk.verify(bytes.fromhex(csfd.digest_hex), bytes.fromhex(signature_hex)) + return True + except BadSignatureError: + return False diff --git a/pyproject.toml b/pyproject.toml index 7994ee0..72852d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ description = "MVP core for verifiable contract-driven mission planning in inter readme = "README.md" requires-python = ">=3.8" license = {text = "MIT"} +dependencies = ["pynacl>=1.4.0"] [tool.setuptools.packages.find] where = ["."] diff --git a/tests/test_csfd.py b/tests/test_csfd.py index 75ca7b9..35a03ae 100644 --- a/tests/test_csfd.py +++ b/tests/test_csfd.py @@ -31,3 +31,28 @@ def test_csfd_size_bound(): serialized = json.dumps({"frames": [f.__dict__ for f in csfd.frames], "signer": csfd.signer, "digest_hex": csfd.digest_hex}, sort_keys=True, separators=(",", ":")) assert len(serialized.encode()) <= 512 + + +def test_csfd_signing_and_verification(): + import pytest + pytest.importorskip("nacl") + + # now import local helpers + from missionledger.dsl import PlanDelta + from missionledger.csfd import generate_csfd, sign_csfd, verify_csfd_signature + + # create a small plandelta + pd = PlanDelta(delta={"x": 1}, version=1, metadata={"invariants": {"a": True}}) + csfd = generate_csfd(pd, signer="s", n_frames=1) + + # Generate an ephemeral Ed25519 keypair for signing + from nacl.signing import SigningKey + + sk = SigningKey.generate() + sig_hex = sign_csfd(csfd, sk.encode()) + assert isinstance(sig_hex, str) + assert len(sig_hex) > 0 + + vk = sk.verify_key + ok = verify_csfd_signature(csfd, sig_hex, vk.encode()) + assert ok is True