Coverage for node / src / stigmem_node / plugins / signing.py: 100%
42 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-25 01:49 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-25 01:49 +0000
1"""Plugin signing verification boundary."""
3from __future__ import annotations
5from collections.abc import Callable
6from dataclasses import dataclass
7from typing import Any
9from .discovery import DiscoveredPlugin
10from .errors import PluginSignatureError
12_UNSIGNED_PLUGIN_ACK = "i-understand-plugins-are-unsigned"
15def _parse_identity_set(raw: str) -> frozenset[str]:
16 return frozenset(identity.strip() for identity in raw.split(",") if identity.strip())
19@dataclass(frozen=True, slots=True)
20class PluginTrustPolicy:
21 """Operator-configured plugin publisher trust policy."""
23 trusted_publishers: frozenset[str] = frozenset()
24 override_publishers: frozenset[str] = frozenset()
26 @classmethod
27 def from_settings(cls) -> PluginTrustPolicy:
28 from stigmem_node.settings import settings
30 return cls(
31 trusted_publishers=_parse_identity_set(settings.plugin_trusted_publishers),
32 override_publishers=_parse_identity_set(settings.plugin_trust_override_publishers),
33 )
36@dataclass(frozen=True, slots=True)
37class PluginSigningInfo:
38 """Verified plugin signing metadata used during registration."""
40 signing_identity: str
41 trust_decision: str
42 trust_reason: str | None = None
44 def audit_metadata(self) -> dict[str, Any]:
45 metadata: dict[str, Any] = {"trust_decision": self.trust_decision}
46 if self.trust_reason is not None:
47 metadata["trust_reason"] = self.trust_reason
48 return metadata
51PluginSignatureVerifier = Callable[[DiscoveredPlugin], PluginSigningInfo]
54def allow_unsigned_development_override(plugin: DiscoveredPlugin) -> PluginSigningInfo:
55 """Return explicit development override metadata for an unsigned plugin."""
56 from stigmem_node.settings import settings
58 if settings.plugin_unsigned_ack != _UNSIGNED_PLUGIN_ACK:
59 raise RuntimeError(
60 "STIGMEM_PLUGIN_SIGNING_REQUIRED=false requires "
61 "STIGMEM_PLUGIN_UNSIGNED_ACK='i-understand-plugins-are-unsigned' "
62 "to confirm the operator accepts unsigned-plugin code execution risk."
63 )
65 return PluginSigningInfo(
66 signing_identity=plugin.signing_identity,
67 trust_decision="development_unsigned_override",
68 trust_reason=(
69 "STIGMEM_PLUGIN_SIGNING_REQUIRED=false accepted an unsigned plugin; "
70 "do not use this setting in production"
71 ),
72 )
75def require_verified_signature(
76 plugin: DiscoveredPlugin,
77 *,
78 policy: PluginTrustPolicy | None = None,
79) -> PluginSigningInfo:
80 """Return signing metadata for a pre-verified plugin or fail closed.
82 PR 4-INF.3 wires the production registration gate here. Package-manager and
83 trusted-publisher policy work can replace this verifier with a Sigstore
84 implementation without changing the registry contract.
85 """
87 if not plugin.signature_verified or plugin.signing_identity == "unsigned":
88 raise PluginSignatureError(
89 f"plugin {plugin.manifest.name!r} is unsigned; "
90 "production plugin registration requires Sigstore verification"
91 )
92 trust_policy = policy or PluginTrustPolicy.from_settings()
93 if plugin.signing_identity in trust_policy.trusted_publishers:
94 return PluginSigningInfo(
95 signing_identity=plugin.signing_identity,
96 trust_decision="trusted_publisher",
97 )
98 if plugin.signing_identity in trust_policy.override_publishers:
99 return PluginSigningInfo(
100 signing_identity=plugin.signing_identity,
101 trust_decision="operator_override",
102 trust_reason="signing identity accepted by explicit operator override",
103 )
104 raise PluginSignatureError(
105 f"plugin {plugin.manifest.name!r} is signed by untrusted identity "
106 f"{plugin.signing_identity!r}; add it to STIGMEM_PLUGIN_TRUSTED_PUBLISHERS "
107 "or, for an explicit audited exception, "
108 "STIGMEM_PLUGIN_TRUST_OVERRIDE_PUBLISHERS"
109 )