"""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))