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

1"""Plugin signing verification boundary.""" 

2 

3from __future__ import annotations 

4 

5from collections.abc import Callable 

6from dataclasses import dataclass 

7from typing import Any 

8 

9from .discovery import DiscoveredPlugin 

10from .errors import PluginSignatureError 

11 

12_UNSIGNED_PLUGIN_ACK = "i-understand-plugins-are-unsigned" 

13 

14 

15def _parse_identity_set(raw: str) -> frozenset[str]: 

16 return frozenset(identity.strip() for identity in raw.split(",") if identity.strip()) 

17 

18 

19@dataclass(frozen=True, slots=True) 

20class PluginTrustPolicy: 

21 """Operator-configured plugin publisher trust policy.""" 

22 

23 trusted_publishers: frozenset[str] = frozenset() 

24 override_publishers: frozenset[str] = frozenset() 

25 

26 @classmethod 

27 def from_settings(cls) -> PluginTrustPolicy: 

28 from stigmem_node.settings import settings 

29 

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 ) 

34 

35 

36@dataclass(frozen=True, slots=True) 

37class PluginSigningInfo: 

38 """Verified plugin signing metadata used during registration.""" 

39 

40 signing_identity: str 

41 trust_decision: str 

42 trust_reason: str | None = None 

43 

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 

49 

50 

51PluginSignatureVerifier = Callable[[DiscoveredPlugin], PluginSigningInfo] 

52 

53 

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 

57 

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 ) 

64 

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 ) 

73 

74 

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. 

81 

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

86 

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 )