"""Plugin signature enforcement policy (ADR-013 Phase 2). One PluginVerifier is built per app from config and shared by the loader (verify-at-load) and the migration manager (verify-at-migrate). A plugin only loads or migrates when it verifies against a trusted key - so signing stops being advisory and becomes enforced everywhere plugin code executes. Default is OFF (PLUGIN_REQUIRE_SIGNED unset), so existing deploys are unchanged. A site opts into hardening by stamping its plugins (`flask plugin stamp-bundled`), pinning keys (PLUGIN_TRUSTED_KEYS), and setting PLUGIN_REQUIRE_SIGNED. Fail-closed: an unverifiable plugin does NOT run. """ import logging from pathlib import Path from . import signing, packaging logger = logging.getLogger(__name__) class PluginVerifier: """Decides whether a plugin may load/migrate under the site's trust policy.""" def __init__(self, plugins_dir, require_signed=False, trusted_key_paths=None, dev_trust_dirs=None, is_dev=False): self.plugins_dir = Path(plugins_dir) self.require_signed = bool(require_signed) self.is_dev = bool(is_dev) # dev trust dirs only mean anything in dev/test (ADR-013); prod ignores. self.dev_trust_dirs = [Path(p).resolve() for p in (dev_trust_dirs or [])] self._keys = signing.load_trusted_keys(trusted_key_paths or []) def _dev_exempt(self, plugin_dir: Path) -> bool: # unsigned dev/external-repo plugins pass, but ONLY under DEBUG/TESTING if not self.is_dev or not self.dev_trust_dirs: return False resolved = Path(plugin_dir).resolve() for trusted in self.dev_trust_dirs: try: resolved.relative_to(trusted) return True except ValueError: continue return False def check(self, plugin_name: str): """(ok, reason). ok True = plugin may load/migrate.""" if not self.require_signed: return True, 'enforcement off' plugin_dir = self.plugins_dir / plugin_name if self._dev_exempt(plugin_dir): return True, 'dev-trusted' if not self._keys: # require-signed with nothing to verify against = refuse everything. return False, ('PLUGIN_REQUIRE_SIGNED is set but no trusted keys ' 'are configured') _, errors = packaging.verify_dir(plugin_dir, self._keys) if errors: return False, '; '.join(errors) return True, 'verified'