"""Plugin signature verification for external plugins. Uses Ed25519 signatures to verify that a plugin ZIP package has not been tampered with and comes from a trusted source. Usage:: from app.plugins.signature import PluginSignature # Verify a downloaded plugin is_valid = PluginSignature.verify_signature( zip_path=Path("plugin.zip"), signature=b"...", public_key=b"...", ) # Compute hash for allowlist file_hash = PluginSignature.compute_hash(Path("plugin.zip")) # Sign a plugin (for plugin authors) signature = PluginSignature.sign_plugin( zip_path=Path("plugin.zip"), private_key=b"...", ) """ from __future__ import annotations import hashlib import logging from pathlib import Path logger = logging.getLogger(__name__) class PluginSignature: """Verify plugin package signatures using Ed25519.""" @staticmethod def compute_hash(file_path: Path) -> str: """Compute SHA-256 hash of a file. Args: file_path: Path to the file to hash. Returns: Hex-encoded SHA-256 hash string. """ sha256 = hashlib.sha256() with open(file_path, "rb") as f: for chunk in iter(lambda: f.read(8192), b""): sha256.update(chunk) return sha256.hexdigest() @staticmethod def verify_signature( zip_path: Path, signature: bytes, public_key: bytes, ) -> bool: """Verify Ed25519 signature of a plugin ZIP. Args: zip_path: Path to the plugin ZIP file. signature: The Ed25519 signature bytes. public_key: The Ed25519 public key bytes. Returns: True if the signature is valid, False otherwise. """ try: from nacl.exceptions import BadSignatureError from nacl.signing import VerifyKey file_hash = PluginSignature.compute_hash(zip_path) verify_key = VerifyKey(public_key) verify_key.verify(file_hash.encode(), signature) logger.info("Signature verified for %s", zip_path.name) return True except ImportError: logger.warning( "PyNaCl not installed — signature verification disabled. " "Install with: pip install pynacl" ) return False except BadSignatureError: logger.warning("Invalid signature for %s", zip_path.name) return False except Exception: logger.exception("Error verifying signature for %s", zip_path.name) return False @staticmethod def sign_plugin( zip_path: Path, private_key: bytes, ) -> bytes: """Sign a plugin ZIP with an Ed25519 private key. Args: zip_path: Path to the plugin ZIP file. private_key: The Ed25519 private key bytes. Returns: The Ed25519 signature bytes. Raises: ImportError: If PyNaCl is not installed. """ from nacl.signing import SigningKey file_hash = PluginSignature.compute_hash(zip_path) signing_key = SigningKey(private_key) return signing_key.sign(file_hash.encode()).signature @staticmethod def generate_keypair() -> tuple[bytes, bytes]: """Generate a new Ed25519 key pair. Returns: Tuple of (private_key, public_key) bytes. """ from nacl.signing import SigningKey signing_key = SigningKey.generate() private_key = bytes(signing_key) public_key = bytes(signing_key.verify_key) return private_key, public_key