From a8cef41ff407545ad8d513a26152991e8a8d0f49 Mon Sep 17 00:00:00 2001 From: agent-14fd4b738639d573 Date: Fri, 24 Apr 2026 20:06:47 +0200 Subject: [PATCH] build(agent): melter#14fd4b iteration --- exprove/contracts.py | 1 + exprove/crypto.py | 81 +++++++++++++++++++++++++++++++++++++++ pyproject.toml | 1 + tests/test_crypto_sign.py | 23 +++++++++++ 4 files changed, 106 insertions(+) create mode 100644 exprove/crypto.py create mode 100644 tests/test_crypto_sign.py diff --git a/exprove/contracts.py b/exprove/contracts.py index bbd2714..5d7bd9d 100644 --- a/exprove/contracts.py +++ b/exprove/contracts.py @@ -63,6 +63,7 @@ class Attestation: timestamp: float contract_id: str version: int + signature: Optional[str] = None def to_json(self) -> str: return to_json(asdict(self)) diff --git a/exprove/crypto.py b/exprove/crypto.py new file mode 100644 index 0000000..7c2e11b --- /dev/null +++ b/exprove/crypto.py @@ -0,0 +1,81 @@ +"""Lightweight Ed25519 signing helpers using PyNaCl. + +Provides simple key generation, signing and verification utilities used by +the Attestation flow in ExProve. We keep this intentionally small to make +it easy to replace with HSM/DID integrations later. +""" +from __future__ import annotations + +from typing import Tuple +import hashlib +import hmac +import secrets + +try: + # Prefer PyNaCl if available for real Ed25519 signatures + from nacl.signing import SigningKey, VerifyKey + from nacl.encoding import HexEncoder + + def generate_signing_key() -> SigningKey: + """Generate a new Ed25519 signing key.""" + return SigningKey.generate() + + + def signing_key_to_hex(sk: SigningKey) -> str: + return sk.encode(encoder=HexEncoder).decode("utf-8") + + + def verify_key_hex_from_signing_key(sk: SigningKey) -> str: + vk = sk.verify_key + return vk.encode(encoder=HexEncoder).decode("utf-8") + + + def sign_message_hex(sk: SigningKey, message: bytes) -> str: + """Sign a message and return signature as hex string.""" + sig = sk.sign(message).signature + return sig.hex() + + + def verify_signature_hex(verify_key_hex: str, message: bytes, sig_hex: str) -> bool: + """Verify signature hex against message. Returns True if valid.""" + vk = VerifyKey(verify_key_hex, encoder=HexEncoder) + try: + vk.verify(message, bytes.fromhex(sig_hex)) + return True + except Exception: + return False + +except Exception: + # Fallback: HMAC-SHA256-based signing for environments without PyNaCl. + # This is NOT cryptographically equivalent to Ed25519 and is only a + # pragmatic fallback for tests and lightweight local usage. + + class _FallbackKey: + def __init__(self, key: bytes): + self._key = key + + def raw(self) -> bytes: + return self._key + + + def generate_signing_key() -> _FallbackKey: + return _FallbackKey(secrets.token_bytes(32)) + + + def signing_key_to_hex(sk: _FallbackKey) -> str: + return sk.raw().hex() + + + def verify_key_hex_from_signing_key(sk: _FallbackKey) -> str: + return signing_key_to_hex(sk) + + + def sign_message_hex(sk: _FallbackKey, message: bytes) -> str: + sig = hmac.new(sk.raw(), message, hashlib.sha256).digest() + return sig.hex() + + + def verify_signature_hex(verify_key_hex: str, message: bytes, sig_hex: str) -> bool: + key = bytes.fromhex(verify_key_hex) + expected = hmac.new(key, message, hashlib.sha256).digest() + return hmac.compare_digest(expected, bytes.fromhex(sig_hex)) diff --git a/pyproject.toml b/pyproject.toml index f95fa53..f888b8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ authors = [ { name = "ExProve Team" } ] dependencies = [ "dataclasses; python_version < '3.7'", "typing; python_version < '3.8'", + "pynacl>=1.5.0", ] [project.urls] diff --git a/tests/test_crypto_sign.py b/tests/test_crypto_sign.py new file mode 100644 index 0000000..75ae98f --- /dev/null +++ b/tests/test_crypto_sign.py @@ -0,0 +1,23 @@ +import json + +from exprove.contracts import LocalExecutionTask, SharedMarketContext, PlanDelta, Attestation +from exprove.contracts import compute_delta +from exprove.crypto import generate_signing_key, verify_key_hex_from_signing_key, sign_message_hex, verify_signature_hex + + +def test_sign_and_verify_plan_delta(): + task = LocalExecutionTask(task_id="t-sig", instrument="AAPL", venue="V1", objective="VWAP", constraints={}) + ctx = SharedMarketContext(signals={"depth": 5}, version=1, contract_id="c-sig") + + delta = compute_delta(task, ctx) + # canonical payload for signing + payload = json.dumps(delta.to_dict(), sort_keys=True).encode("utf-8") + + sk = generate_signing_key() + vk_hex = verify_key_hex_from_signing_key(sk) + sig_hex = sign_message_hex(sk, payload) + + # attach signature to attestation and verify + a = Attestation(entry=delta.to_json(), signer=vk_hex, timestamp=0.0, contract_id=ctx.contract_id, version=1, signature=sig_hex) + + assert verify_signature_hex(vk_hex, payload, a.signature) is True