truthound-dashboard 1.3.1__py3-none-any.whl → 1.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. truthound_dashboard/api/alerts.py +258 -0
  2. truthound_dashboard/api/anomaly.py +1302 -0
  3. truthound_dashboard/api/cross_alerts.py +352 -0
  4. truthound_dashboard/api/deps.py +143 -0
  5. truthound_dashboard/api/drift_monitor.py +540 -0
  6. truthound_dashboard/api/lineage.py +1151 -0
  7. truthound_dashboard/api/maintenance.py +363 -0
  8. truthound_dashboard/api/middleware.py +373 -1
  9. truthound_dashboard/api/model_monitoring.py +805 -0
  10. truthound_dashboard/api/notifications_advanced.py +2452 -0
  11. truthound_dashboard/api/plugins.py +2096 -0
  12. truthound_dashboard/api/profile.py +211 -14
  13. truthound_dashboard/api/reports.py +853 -0
  14. truthound_dashboard/api/router.py +147 -0
  15. truthound_dashboard/api/rule_suggestions.py +310 -0
  16. truthound_dashboard/api/schema_evolution.py +231 -0
  17. truthound_dashboard/api/sources.py +47 -3
  18. truthound_dashboard/api/triggers.py +190 -0
  19. truthound_dashboard/api/validations.py +13 -0
  20. truthound_dashboard/api/validators.py +333 -4
  21. truthound_dashboard/api/versioning.py +309 -0
  22. truthound_dashboard/api/websocket.py +301 -0
  23. truthound_dashboard/core/__init__.py +27 -0
  24. truthound_dashboard/core/anomaly.py +1395 -0
  25. truthound_dashboard/core/anomaly_explainer.py +633 -0
  26. truthound_dashboard/core/cache.py +206 -0
  27. truthound_dashboard/core/cached_services.py +422 -0
  28. truthound_dashboard/core/charts.py +352 -0
  29. truthound_dashboard/core/connections.py +1069 -42
  30. truthound_dashboard/core/cross_alerts.py +837 -0
  31. truthound_dashboard/core/drift_monitor.py +1477 -0
  32. truthound_dashboard/core/drift_sampling.py +669 -0
  33. truthound_dashboard/core/i18n/__init__.py +42 -0
  34. truthound_dashboard/core/i18n/detector.py +173 -0
  35. truthound_dashboard/core/i18n/messages.py +564 -0
  36. truthound_dashboard/core/lineage.py +971 -0
  37. truthound_dashboard/core/maintenance.py +443 -5
  38. truthound_dashboard/core/model_monitoring.py +1043 -0
  39. truthound_dashboard/core/notifications/channels.py +1020 -1
  40. truthound_dashboard/core/notifications/deduplication/__init__.py +143 -0
  41. truthound_dashboard/core/notifications/deduplication/policies.py +274 -0
  42. truthound_dashboard/core/notifications/deduplication/service.py +400 -0
  43. truthound_dashboard/core/notifications/deduplication/stores.py +2365 -0
  44. truthound_dashboard/core/notifications/deduplication/strategies.py +422 -0
  45. truthound_dashboard/core/notifications/dispatcher.py +43 -0
  46. truthound_dashboard/core/notifications/escalation/__init__.py +149 -0
  47. truthound_dashboard/core/notifications/escalation/backends.py +1384 -0
  48. truthound_dashboard/core/notifications/escalation/engine.py +429 -0
  49. truthound_dashboard/core/notifications/escalation/models.py +336 -0
  50. truthound_dashboard/core/notifications/escalation/scheduler.py +1187 -0
  51. truthound_dashboard/core/notifications/escalation/state_machine.py +330 -0
  52. truthound_dashboard/core/notifications/escalation/stores.py +2896 -0
  53. truthound_dashboard/core/notifications/events.py +49 -0
  54. truthound_dashboard/core/notifications/metrics/__init__.py +115 -0
  55. truthound_dashboard/core/notifications/metrics/base.py +528 -0
  56. truthound_dashboard/core/notifications/metrics/collectors.py +583 -0
  57. truthound_dashboard/core/notifications/routing/__init__.py +169 -0
  58. truthound_dashboard/core/notifications/routing/combinators.py +184 -0
  59. truthound_dashboard/core/notifications/routing/config.py +375 -0
  60. truthound_dashboard/core/notifications/routing/config_parser.py +867 -0
  61. truthound_dashboard/core/notifications/routing/engine.py +382 -0
  62. truthound_dashboard/core/notifications/routing/expression_engine.py +1269 -0
  63. truthound_dashboard/core/notifications/routing/jinja2_engine.py +774 -0
  64. truthound_dashboard/core/notifications/routing/rules.py +625 -0
  65. truthound_dashboard/core/notifications/routing/validator.py +678 -0
  66. truthound_dashboard/core/notifications/service.py +2 -0
  67. truthound_dashboard/core/notifications/stats_aggregator.py +850 -0
  68. truthound_dashboard/core/notifications/throttling/__init__.py +83 -0
  69. truthound_dashboard/core/notifications/throttling/builder.py +311 -0
  70. truthound_dashboard/core/notifications/throttling/stores.py +1859 -0
  71. truthound_dashboard/core/notifications/throttling/throttlers.py +633 -0
  72. truthound_dashboard/core/openlineage.py +1028 -0
  73. truthound_dashboard/core/plugins/__init__.py +39 -0
  74. truthound_dashboard/core/plugins/docs/__init__.py +39 -0
  75. truthound_dashboard/core/plugins/docs/extractor.py +703 -0
  76. truthound_dashboard/core/plugins/docs/renderers.py +804 -0
  77. truthound_dashboard/core/plugins/hooks/__init__.py +63 -0
  78. truthound_dashboard/core/plugins/hooks/decorators.py +367 -0
  79. truthound_dashboard/core/plugins/hooks/manager.py +403 -0
  80. truthound_dashboard/core/plugins/hooks/protocols.py +265 -0
  81. truthound_dashboard/core/plugins/lifecycle/__init__.py +41 -0
  82. truthound_dashboard/core/plugins/lifecycle/hot_reload.py +584 -0
  83. truthound_dashboard/core/plugins/lifecycle/machine.py +419 -0
  84. truthound_dashboard/core/plugins/lifecycle/states.py +266 -0
  85. truthound_dashboard/core/plugins/loader.py +504 -0
  86. truthound_dashboard/core/plugins/registry.py +810 -0
  87. truthound_dashboard/core/plugins/reporter_executor.py +588 -0
  88. truthound_dashboard/core/plugins/sandbox/__init__.py +59 -0
  89. truthound_dashboard/core/plugins/sandbox/code_validator.py +243 -0
  90. truthound_dashboard/core/plugins/sandbox/engines.py +770 -0
  91. truthound_dashboard/core/plugins/sandbox/protocols.py +194 -0
  92. truthound_dashboard/core/plugins/sandbox.py +617 -0
  93. truthound_dashboard/core/plugins/security/__init__.py +68 -0
  94. truthound_dashboard/core/plugins/security/analyzer.py +535 -0
  95. truthound_dashboard/core/plugins/security/policies.py +311 -0
  96. truthound_dashboard/core/plugins/security/protocols.py +296 -0
  97. truthound_dashboard/core/plugins/security/signing.py +842 -0
  98. truthound_dashboard/core/plugins/security.py +446 -0
  99. truthound_dashboard/core/plugins/validator_executor.py +401 -0
  100. truthound_dashboard/core/plugins/versioning/__init__.py +51 -0
  101. truthound_dashboard/core/plugins/versioning/constraints.py +377 -0
  102. truthound_dashboard/core/plugins/versioning/dependencies.py +541 -0
  103. truthound_dashboard/core/plugins/versioning/semver.py +266 -0
  104. truthound_dashboard/core/profile_comparison.py +601 -0
  105. truthound_dashboard/core/report_history.py +570 -0
  106. truthound_dashboard/core/reporters/__init__.py +57 -0
  107. truthound_dashboard/core/reporters/base.py +296 -0
  108. truthound_dashboard/core/reporters/csv_reporter.py +155 -0
  109. truthound_dashboard/core/reporters/html_reporter.py +598 -0
  110. truthound_dashboard/core/reporters/i18n/__init__.py +65 -0
  111. truthound_dashboard/core/reporters/i18n/base.py +494 -0
  112. truthound_dashboard/core/reporters/i18n/catalogs.py +930 -0
  113. truthound_dashboard/core/reporters/json_reporter.py +160 -0
  114. truthound_dashboard/core/reporters/junit_reporter.py +233 -0
  115. truthound_dashboard/core/reporters/markdown_reporter.py +207 -0
  116. truthound_dashboard/core/reporters/pdf_reporter.py +209 -0
  117. truthound_dashboard/core/reporters/registry.py +272 -0
  118. truthound_dashboard/core/rule_generator.py +2088 -0
  119. truthound_dashboard/core/scheduler.py +822 -12
  120. truthound_dashboard/core/schema_evolution.py +858 -0
  121. truthound_dashboard/core/services.py +152 -9
  122. truthound_dashboard/core/statistics.py +718 -0
  123. truthound_dashboard/core/streaming_anomaly.py +883 -0
  124. truthound_dashboard/core/triggers/__init__.py +45 -0
  125. truthound_dashboard/core/triggers/base.py +226 -0
  126. truthound_dashboard/core/triggers/evaluators.py +609 -0
  127. truthound_dashboard/core/triggers/factory.py +363 -0
  128. truthound_dashboard/core/unified_alerts.py +870 -0
  129. truthound_dashboard/core/validation_limits.py +509 -0
  130. truthound_dashboard/core/versioning.py +709 -0
  131. truthound_dashboard/core/websocket/__init__.py +59 -0
  132. truthound_dashboard/core/websocket/manager.py +512 -0
  133. truthound_dashboard/core/websocket/messages.py +130 -0
  134. truthound_dashboard/db/__init__.py +30 -0
  135. truthound_dashboard/db/models.py +3375 -3
  136. truthound_dashboard/main.py +22 -0
  137. truthound_dashboard/schemas/__init__.py +396 -1
  138. truthound_dashboard/schemas/anomaly.py +1258 -0
  139. truthound_dashboard/schemas/base.py +4 -0
  140. truthound_dashboard/schemas/cross_alerts.py +334 -0
  141. truthound_dashboard/schemas/drift_monitor.py +890 -0
  142. truthound_dashboard/schemas/lineage.py +428 -0
  143. truthound_dashboard/schemas/maintenance.py +154 -0
  144. truthound_dashboard/schemas/model_monitoring.py +374 -0
  145. truthound_dashboard/schemas/notifications_advanced.py +1363 -0
  146. truthound_dashboard/schemas/openlineage.py +704 -0
  147. truthound_dashboard/schemas/plugins.py +1293 -0
  148. truthound_dashboard/schemas/profile.py +420 -34
  149. truthound_dashboard/schemas/profile_comparison.py +242 -0
  150. truthound_dashboard/schemas/reports.py +285 -0
  151. truthound_dashboard/schemas/rule_suggestion.py +434 -0
  152. truthound_dashboard/schemas/schema_evolution.py +164 -0
  153. truthound_dashboard/schemas/source.py +117 -2
  154. truthound_dashboard/schemas/triggers.py +511 -0
  155. truthound_dashboard/schemas/unified_alerts.py +223 -0
  156. truthound_dashboard/schemas/validation.py +25 -1
  157. truthound_dashboard/schemas/validators/__init__.py +11 -0
  158. truthound_dashboard/schemas/validators/base.py +151 -0
  159. truthound_dashboard/schemas/versioning.py +152 -0
  160. truthound_dashboard/static/index.html +2 -2
  161. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/METADATA +147 -23
  162. truthound_dashboard-1.4.1.dist-info/RECORD +239 -0
  163. truthound_dashboard/static/assets/index-BZG20KuF.js +0 -586
  164. truthound_dashboard/static/assets/index-D_HyZ3pb.css +0 -1
  165. truthound_dashboard/static/assets/unmerged_dictionaries-CtpqQBm0.js +0 -1
  166. truthound_dashboard-1.3.1.dist-info/RECORD +0 -110
  167. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/WHEEL +0 -0
  168. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/entry_points.txt +0 -0
  169. {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,842 @@
1
+ """Signature Verification and Trust Store.
2
+
3
+ This module provides:
4
+ - Multiple signature algorithms (HMAC, RSA, Ed25519)
5
+ - Trust store for managing trusted signers
6
+ - Verification chain (Chain of Responsibility pattern)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import base64
12
+ import hashlib
13
+ import hmac
14
+ import logging
15
+ from abc import ABC, abstractmethod
16
+ from dataclasses import dataclass, field
17
+ from datetime import datetime
18
+ from typing import Any, Callable
19
+
20
+ from .protocols import (
21
+ SignatureAlgorithm,
22
+ SignatureInfo,
23
+ TrustLevel,
24
+ VerificationResult,
25
+ )
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ # =============================================================================
31
+ # Signing Service
32
+ # =============================================================================
33
+
34
+
35
+ class SigningService(ABC):
36
+ """Abstract base class for signing services."""
37
+
38
+ @property
39
+ @abstractmethod
40
+ def algorithm(self) -> SignatureAlgorithm:
41
+ """Get the signing algorithm."""
42
+ ...
43
+
44
+ @abstractmethod
45
+ def sign(
46
+ self,
47
+ data: bytes,
48
+ private_key: bytes,
49
+ signer_id: str,
50
+ ) -> SignatureInfo:
51
+ """Sign data and return signature info.
52
+
53
+ Args:
54
+ data: Data to sign.
55
+ private_key: Private key for signing.
56
+ signer_id: ID of the signer.
57
+
58
+ Returns:
59
+ SignatureInfo with the signature.
60
+ """
61
+ ...
62
+
63
+ @abstractmethod
64
+ def verify(
65
+ self,
66
+ data: bytes,
67
+ signature_info: SignatureInfo,
68
+ public_key: bytes | None = None,
69
+ ) -> VerificationResult:
70
+ """Verify a signature.
71
+
72
+ Args:
73
+ data: Original data.
74
+ signature_info: Signature to verify.
75
+ public_key: Public key (required for asymmetric algorithms).
76
+
77
+ Returns:
78
+ VerificationResult with verification status.
79
+ """
80
+ ...
81
+
82
+
83
+ class HMACSigningService(SigningService):
84
+ """HMAC-based signing service (SHA256 or SHA512)."""
85
+
86
+ def __init__(self, use_sha512: bool = False) -> None:
87
+ """Initialize HMAC signing service.
88
+
89
+ Args:
90
+ use_sha512: Use SHA512 instead of SHA256.
91
+ """
92
+ self._use_sha512 = use_sha512
93
+ self._hash_func = hashlib.sha512 if use_sha512 else hashlib.sha256
94
+
95
+ @property
96
+ def algorithm(self) -> SignatureAlgorithm:
97
+ """Get the signing algorithm."""
98
+ return (
99
+ SignatureAlgorithm.HMAC_SHA512
100
+ if self._use_sha512
101
+ else SignatureAlgorithm.HMAC_SHA256
102
+ )
103
+
104
+ def sign(
105
+ self,
106
+ data: bytes,
107
+ private_key: bytes,
108
+ signer_id: str,
109
+ ) -> SignatureInfo:
110
+ """Sign data using HMAC."""
111
+ signature = hmac.new(private_key, data, self._hash_func).hexdigest()
112
+ return SignatureInfo(
113
+ algorithm=self.algorithm,
114
+ signature=signature,
115
+ signer_id=signer_id,
116
+ timestamp=datetime.utcnow(),
117
+ )
118
+
119
+ def verify(
120
+ self,
121
+ data: bytes,
122
+ signature_info: SignatureInfo,
123
+ public_key: bytes | None = None,
124
+ ) -> VerificationResult:
125
+ """Verify HMAC signature."""
126
+ if public_key is None:
127
+ return VerificationResult(
128
+ is_valid=False,
129
+ errors=["HMAC verification requires a key"],
130
+ )
131
+
132
+ try:
133
+ expected = hmac.new(public_key, data, self._hash_func).hexdigest()
134
+ is_valid = hmac.compare_digest(expected, signature_info.signature)
135
+ return VerificationResult(
136
+ is_valid=is_valid,
137
+ trust_level=TrustLevel.VERIFIED if is_valid else TrustLevel.UNVERIFIED,
138
+ signer_id=signature_info.signer_id,
139
+ algorithm=self.algorithm,
140
+ errors=[] if is_valid else ["Signature mismatch"],
141
+ )
142
+ except Exception as e:
143
+ return VerificationResult(
144
+ is_valid=False,
145
+ errors=[f"Verification error: {str(e)}"],
146
+ )
147
+
148
+
149
+ class RSASigningService(SigningService):
150
+ """RSA-based signing service."""
151
+
152
+ @property
153
+ def algorithm(self) -> SignatureAlgorithm:
154
+ """Get the signing algorithm."""
155
+ return SignatureAlgorithm.RSA_SHA256
156
+
157
+ def sign(
158
+ self,
159
+ data: bytes,
160
+ private_key: bytes,
161
+ signer_id: str,
162
+ ) -> SignatureInfo:
163
+ """Sign data using RSA."""
164
+ try:
165
+ from cryptography.hazmat.primitives import hashes, serialization
166
+ from cryptography.hazmat.primitives.asymmetric import padding, rsa
167
+
168
+ # Load private key
169
+ key = serialization.load_pem_private_key(private_key, password=None)
170
+ if not isinstance(key, rsa.RSAPrivateKey):
171
+ raise ValueError("Not an RSA private key")
172
+
173
+ # Sign
174
+ signature = key.sign(
175
+ data,
176
+ padding.PKCS1v15(),
177
+ hashes.SHA256(),
178
+ )
179
+ return SignatureInfo(
180
+ algorithm=self.algorithm,
181
+ signature=base64.b64encode(signature).decode(),
182
+ signer_id=signer_id,
183
+ timestamp=datetime.utcnow(),
184
+ )
185
+ except ImportError:
186
+ raise RuntimeError("cryptography package required for RSA signing")
187
+
188
+ def verify(
189
+ self,
190
+ data: bytes,
191
+ signature_info: SignatureInfo,
192
+ public_key: bytes | None = None,
193
+ ) -> VerificationResult:
194
+ """Verify RSA signature."""
195
+ if public_key is None:
196
+ return VerificationResult(
197
+ is_valid=False,
198
+ errors=["RSA verification requires a public key"],
199
+ )
200
+
201
+ try:
202
+ from cryptography.hazmat.primitives import hashes, serialization
203
+ from cryptography.hazmat.primitives.asymmetric import padding, rsa
204
+
205
+ # Load public key
206
+ key = serialization.load_pem_public_key(public_key)
207
+ if not isinstance(key, rsa.RSAPublicKey):
208
+ return VerificationResult(
209
+ is_valid=False,
210
+ errors=["Not an RSA public key"],
211
+ )
212
+
213
+ # Verify
214
+ signature = base64.b64decode(signature_info.signature)
215
+ key.verify(
216
+ signature,
217
+ data,
218
+ padding.PKCS1v15(),
219
+ hashes.SHA256(),
220
+ )
221
+ return VerificationResult(
222
+ is_valid=True,
223
+ trust_level=TrustLevel.VERIFIED,
224
+ signer_id=signature_info.signer_id,
225
+ algorithm=self.algorithm,
226
+ )
227
+ except ImportError:
228
+ return VerificationResult(
229
+ is_valid=False,
230
+ errors=["cryptography package required for RSA verification"],
231
+ )
232
+ except Exception as e:
233
+ return VerificationResult(
234
+ is_valid=False,
235
+ errors=[f"RSA verification failed: {str(e)}"],
236
+ )
237
+
238
+
239
+ class Ed25519SigningService(SigningService):
240
+ """Ed25519-based signing service (recommended)."""
241
+
242
+ @property
243
+ def algorithm(self) -> SignatureAlgorithm:
244
+ """Get the signing algorithm."""
245
+ return SignatureAlgorithm.ED25519
246
+
247
+ def sign(
248
+ self,
249
+ data: bytes,
250
+ private_key: bytes,
251
+ signer_id: str,
252
+ ) -> SignatureInfo:
253
+ """Sign data using Ed25519."""
254
+ try:
255
+ from cryptography.hazmat.primitives import serialization
256
+ from cryptography.hazmat.primitives.asymmetric import ed25519
257
+
258
+ # Load private key
259
+ key = serialization.load_pem_private_key(private_key, password=None)
260
+ if not isinstance(key, ed25519.Ed25519PrivateKey):
261
+ raise ValueError("Not an Ed25519 private key")
262
+
263
+ # Sign
264
+ signature = key.sign(data)
265
+ return SignatureInfo(
266
+ algorithm=self.algorithm,
267
+ signature=base64.b64encode(signature).decode(),
268
+ signer_id=signer_id,
269
+ timestamp=datetime.utcnow(),
270
+ )
271
+ except ImportError:
272
+ raise RuntimeError("cryptography package required for Ed25519 signing")
273
+
274
+ def verify(
275
+ self,
276
+ data: bytes,
277
+ signature_info: SignatureInfo,
278
+ public_key: bytes | None = None,
279
+ ) -> VerificationResult:
280
+ """Verify Ed25519 signature."""
281
+ if public_key is None:
282
+ return VerificationResult(
283
+ is_valid=False,
284
+ errors=["Ed25519 verification requires a public key"],
285
+ )
286
+
287
+ try:
288
+ from cryptography.hazmat.primitives import serialization
289
+ from cryptography.hazmat.primitives.asymmetric import ed25519
290
+
291
+ # Load public key
292
+ key = serialization.load_pem_public_key(public_key)
293
+ if not isinstance(key, ed25519.Ed25519PublicKey):
294
+ return VerificationResult(
295
+ is_valid=False,
296
+ errors=["Not an Ed25519 public key"],
297
+ )
298
+
299
+ # Verify
300
+ signature = base64.b64decode(signature_info.signature)
301
+ key.verify(signature, data)
302
+ return VerificationResult(
303
+ is_valid=True,
304
+ trust_level=TrustLevel.VERIFIED,
305
+ signer_id=signature_info.signer_id,
306
+ algorithm=self.algorithm,
307
+ )
308
+ except ImportError:
309
+ return VerificationResult(
310
+ is_valid=False,
311
+ errors=["cryptography package required for Ed25519 verification"],
312
+ )
313
+ except Exception as e:
314
+ return VerificationResult(
315
+ is_valid=False,
316
+ errors=[f"Ed25519 verification failed: {str(e)}"],
317
+ )
318
+
319
+
320
+ class SigningServiceImpl:
321
+ """Factory for creating signing services."""
322
+
323
+ _services: dict[SignatureAlgorithm, type[SigningService]] = {
324
+ SignatureAlgorithm.HMAC_SHA256: HMACSigningService,
325
+ SignatureAlgorithm.HMAC_SHA512: HMACSigningService,
326
+ SignatureAlgorithm.RSA_SHA256: RSASigningService,
327
+ SignatureAlgorithm.ED25519: Ed25519SigningService,
328
+ }
329
+
330
+ def __init__(self, algorithm: SignatureAlgorithm = SignatureAlgorithm.HMAC_SHA256) -> None:
331
+ """Initialize signing service.
332
+
333
+ Args:
334
+ algorithm: Signature algorithm to use.
335
+ """
336
+ self.algorithm = algorithm
337
+ if algorithm == SignatureAlgorithm.HMAC_SHA512:
338
+ self._service = HMACSigningService(use_sha512=True)
339
+ elif algorithm in self._services:
340
+ self._service = self._services[algorithm]()
341
+ else:
342
+ raise ValueError(f"Unsupported algorithm: {algorithm}")
343
+
344
+ def sign(
345
+ self,
346
+ data: bytes,
347
+ private_key: bytes,
348
+ signer_id: str,
349
+ ) -> SignatureInfo:
350
+ """Sign data."""
351
+ return self._service.sign(data, private_key, signer_id)
352
+
353
+ def verify(
354
+ self,
355
+ data: bytes,
356
+ signature_info: SignatureInfo,
357
+ public_key: bytes | None = None,
358
+ ) -> VerificationResult:
359
+ """Verify signature."""
360
+ return self._service.verify(data, signature_info, public_key)
361
+
362
+
363
+ # =============================================================================
364
+ # Trust Store
365
+ # =============================================================================
366
+
367
+
368
+ @dataclass
369
+ class TrustedSigner:
370
+ """Information about a trusted signer.
371
+
372
+ Attributes:
373
+ signer_id: Unique identifier for the signer.
374
+ public_key: Public key (PEM format).
375
+ trust_level: Trust level assigned.
376
+ name: Display name.
377
+ email: Contact email.
378
+ organization: Organization name.
379
+ added_at: When the signer was added.
380
+ expires_at: When the trust expires.
381
+ revoked: Whether the signer is revoked.
382
+ metadata: Additional metadata.
383
+ """
384
+
385
+ signer_id: str
386
+ public_key: bytes
387
+ trust_level: TrustLevel
388
+ name: str = ""
389
+ email: str = ""
390
+ organization: str = ""
391
+ added_at: datetime = field(default_factory=datetime.utcnow)
392
+ expires_at: datetime | None = None
393
+ revoked: bool = False
394
+ metadata: dict[str, Any] = field(default_factory=dict)
395
+
396
+
397
+ class TrustStore:
398
+ """Abstract base class for trust stores."""
399
+
400
+ @abstractmethod
401
+ def add_signer(
402
+ self,
403
+ signer_id: str,
404
+ public_key: bytes,
405
+ trust_level: TrustLevel,
406
+ **kwargs: Any,
407
+ ) -> None:
408
+ """Add a trusted signer."""
409
+ ...
410
+
411
+ @abstractmethod
412
+ def remove_signer(self, signer_id: str) -> None:
413
+ """Remove a signer."""
414
+ ...
415
+
416
+ @abstractmethod
417
+ def get_trust_level(self, signer_id: str) -> TrustLevel | None:
418
+ """Get trust level for a signer."""
419
+ ...
420
+
421
+ @abstractmethod
422
+ def get_public_key(self, signer_id: str) -> bytes | None:
423
+ """Get public key for a signer."""
424
+ ...
425
+
426
+ @abstractmethod
427
+ def is_trusted(self, signer_id: str) -> bool:
428
+ """Check if signer is trusted."""
429
+ ...
430
+
431
+ @abstractmethod
432
+ def list_signers(self) -> list[TrustedSigner]:
433
+ """List all signers."""
434
+ ...
435
+
436
+
437
+ class TrustStoreImpl(TrustStore):
438
+ """In-memory trust store implementation."""
439
+
440
+ def __init__(self) -> None:
441
+ """Initialize the trust store."""
442
+ self._signers: dict[str, TrustedSigner] = {}
443
+
444
+ def add_signer(
445
+ self,
446
+ signer_id: str,
447
+ public_key: bytes,
448
+ trust_level: TrustLevel,
449
+ **kwargs: Any,
450
+ ) -> None:
451
+ """Add a trusted signer."""
452
+ self._signers[signer_id] = TrustedSigner(
453
+ signer_id=signer_id,
454
+ public_key=public_key,
455
+ trust_level=trust_level,
456
+ name=kwargs.get("name", ""),
457
+ email=kwargs.get("email", ""),
458
+ organization=kwargs.get("organization", ""),
459
+ expires_at=kwargs.get("expires_at"),
460
+ metadata=kwargs.get("metadata", {}),
461
+ )
462
+ logger.info(f"Added trusted signer: {signer_id} (level: {trust_level.value})")
463
+
464
+ def remove_signer(self, signer_id: str) -> None:
465
+ """Remove a signer."""
466
+ if signer_id in self._signers:
467
+ del self._signers[signer_id]
468
+ logger.info(f"Removed signer: {signer_id}")
469
+
470
+ def revoke_signer(self, signer_id: str) -> None:
471
+ """Revoke a signer without removing."""
472
+ if signer_id in self._signers:
473
+ self._signers[signer_id].revoked = True
474
+ logger.info(f"Revoked signer: {signer_id}")
475
+
476
+ def get_signer(self, signer_id: str) -> TrustedSigner | None:
477
+ """Get signer information."""
478
+ return self._signers.get(signer_id)
479
+
480
+ def get_trust_level(self, signer_id: str) -> TrustLevel | None:
481
+ """Get trust level for a signer."""
482
+ signer = self._signers.get(signer_id)
483
+ if signer is None:
484
+ return None
485
+ if signer.revoked:
486
+ return TrustLevel.UNVERIFIED
487
+ if signer.expires_at and signer.expires_at < datetime.utcnow():
488
+ return TrustLevel.UNVERIFIED
489
+ return signer.trust_level
490
+
491
+ def get_public_key(self, signer_id: str) -> bytes | None:
492
+ """Get public key for a signer."""
493
+ signer = self._signers.get(signer_id)
494
+ if signer is None or signer.revoked:
495
+ return None
496
+ return signer.public_key
497
+
498
+ def is_trusted(self, signer_id: str) -> bool:
499
+ """Check if signer is trusted."""
500
+ trust_level = self.get_trust_level(signer_id)
501
+ return trust_level in (TrustLevel.TRUSTED, TrustLevel.VERIFIED)
502
+
503
+ def list_signers(self) -> list[TrustedSigner]:
504
+ """List all signers."""
505
+ return list(self._signers.values())
506
+
507
+ def set_signer_trust(self, signer_id: str, trust_level: TrustLevel) -> None:
508
+ """Update trust level for a signer."""
509
+ if signer_id in self._signers:
510
+ self._signers[signer_id].trust_level = trust_level
511
+ logger.info(f"Updated trust level for {signer_id}: {trust_level.value}")
512
+
513
+
514
+ # =============================================================================
515
+ # Verification Chain (Chain of Responsibility)
516
+ # =============================================================================
517
+
518
+
519
+ class VerificationHandler(ABC):
520
+ """Abstract handler in the verification chain."""
521
+
522
+ def __init__(self) -> None:
523
+ """Initialize the handler."""
524
+ self._next: VerificationHandler | None = None
525
+
526
+ def set_next(self, handler: "VerificationHandler") -> "VerificationHandler":
527
+ """Set the next handler in the chain.
528
+
529
+ Args:
530
+ handler: Next handler.
531
+
532
+ Returns:
533
+ The next handler (for chaining).
534
+ """
535
+ self._next = handler
536
+ return handler
537
+
538
+ @abstractmethod
539
+ def handle(
540
+ self,
541
+ data: bytes,
542
+ signatures: list[SignatureInfo],
543
+ context: dict[str, Any],
544
+ ) -> VerificationResult | None:
545
+ """Handle verification.
546
+
547
+ Args:
548
+ data: Data to verify.
549
+ signatures: Signatures to verify.
550
+ context: Verification context.
551
+
552
+ Returns:
553
+ VerificationResult if handled, None to pass to next.
554
+ """
555
+ ...
556
+
557
+ def _pass_to_next(
558
+ self,
559
+ data: bytes,
560
+ signatures: list[SignatureInfo],
561
+ context: dict[str, Any],
562
+ ) -> VerificationResult | None:
563
+ """Pass to next handler if available."""
564
+ if self._next:
565
+ return self._next.handle(data, signatures, context)
566
+ return None
567
+
568
+
569
+ class SignatureCountHandler(VerificationHandler):
570
+ """Verify minimum signature count."""
571
+
572
+ def __init__(self, min_signatures: int = 1) -> None:
573
+ """Initialize handler.
574
+
575
+ Args:
576
+ min_signatures: Minimum required signatures.
577
+ """
578
+ super().__init__()
579
+ self.min_signatures = min_signatures
580
+
581
+ def handle(
582
+ self,
583
+ data: bytes,
584
+ signatures: list[SignatureInfo],
585
+ context: dict[str, Any],
586
+ ) -> VerificationResult | None:
587
+ """Check signature count."""
588
+ if len(signatures) < self.min_signatures:
589
+ return VerificationResult(
590
+ is_valid=False,
591
+ trust_level=TrustLevel.UNVERIFIED,
592
+ errors=[
593
+ f"Insufficient signatures: {len(signatures)} < {self.min_signatures}"
594
+ ],
595
+ )
596
+ return self._pass_to_next(data, signatures, context)
597
+
598
+
599
+ class SignerTrustHandler(VerificationHandler):
600
+ """Verify signer trust levels."""
601
+
602
+ def __init__(self, trust_store: TrustStore) -> None:
603
+ """Initialize handler.
604
+
605
+ Args:
606
+ trust_store: Trust store to use.
607
+ """
608
+ super().__init__()
609
+ self.trust_store = trust_store
610
+
611
+ def handle(
612
+ self,
613
+ data: bytes,
614
+ signatures: list[SignatureInfo],
615
+ context: dict[str, Any],
616
+ ) -> VerificationResult | None:
617
+ """Check signer trust."""
618
+ warnings = []
619
+ for sig in signatures:
620
+ trust_level = self.trust_store.get_trust_level(sig.signer_id)
621
+ if trust_level is None:
622
+ warnings.append(f"Unknown signer: {sig.signer_id}")
623
+ elif trust_level == TrustLevel.UNVERIFIED:
624
+ warnings.append(f"Untrusted signer: {sig.signer_id}")
625
+
626
+ context["signer_warnings"] = warnings
627
+ return self._pass_to_next(data, signatures, context)
628
+
629
+
630
+ class CryptographicVerificationHandler(VerificationHandler):
631
+ """Perform cryptographic signature verification."""
632
+
633
+ def __init__(self, trust_store: TrustStore) -> None:
634
+ """Initialize handler.
635
+
636
+ Args:
637
+ trust_store: Trust store for public keys.
638
+ """
639
+ super().__init__()
640
+ self.trust_store = trust_store
641
+
642
+ def handle(
643
+ self,
644
+ data: bytes,
645
+ signatures: list[SignatureInfo],
646
+ context: dict[str, Any],
647
+ ) -> VerificationResult | None:
648
+ """Verify signatures cryptographically."""
649
+ valid_signatures = 0
650
+ verified_signers = []
651
+ errors = []
652
+ warnings = context.get("signer_warnings", [])
653
+
654
+ for sig in signatures:
655
+ public_key = self.trust_store.get_public_key(sig.signer_id)
656
+ if public_key is None:
657
+ warnings.append(f"No public key for signer: {sig.signer_id}")
658
+ continue
659
+
660
+ try:
661
+ service = SigningServiceImpl(sig.algorithm)
662
+ result = service.verify(data, sig, public_key)
663
+ if result.is_valid:
664
+ valid_signatures += 1
665
+ verified_signers.append(sig.signer_id)
666
+ else:
667
+ errors.extend(result.errors)
668
+ except Exception as e:
669
+ errors.append(f"Verification error for {sig.signer_id}: {str(e)}")
670
+
671
+ # Determine final trust level
672
+ if valid_signatures > 0:
673
+ # Check trust levels of verified signers
674
+ max_trust = TrustLevel.UNVERIFIED
675
+ for signer_id in verified_signers:
676
+ trust = self.trust_store.get_trust_level(signer_id)
677
+ if trust == TrustLevel.TRUSTED:
678
+ max_trust = TrustLevel.TRUSTED
679
+ break
680
+ elif trust == TrustLevel.VERIFIED and max_trust != TrustLevel.TRUSTED:
681
+ max_trust = TrustLevel.VERIFIED
682
+
683
+ return VerificationResult(
684
+ is_valid=True,
685
+ trust_level=max_trust,
686
+ signer_id=verified_signers[0] if verified_signers else None,
687
+ warnings=warnings,
688
+ metadata={"valid_signatures": valid_signatures},
689
+ )
690
+ else:
691
+ return VerificationResult(
692
+ is_valid=False,
693
+ trust_level=TrustLevel.UNVERIFIED,
694
+ errors=errors or ["No valid signatures"],
695
+ warnings=warnings,
696
+ )
697
+
698
+
699
+ class VerificationChain:
700
+ """Verification chain that processes handlers in sequence."""
701
+
702
+ def __init__(self, first_handler: VerificationHandler | None = None) -> None:
703
+ """Initialize the chain.
704
+
705
+ Args:
706
+ first_handler: First handler in the chain.
707
+ """
708
+ self._first = first_handler
709
+
710
+ def verify(
711
+ self,
712
+ data: bytes,
713
+ signatures: list[SignatureInfo],
714
+ context: dict[str, Any] | None = None,
715
+ ) -> VerificationResult:
716
+ """Verify signatures using the chain.
717
+
718
+ Args:
719
+ data: Data to verify.
720
+ signatures: Signatures to verify.
721
+ context: Optional context dictionary.
722
+
723
+ Returns:
724
+ VerificationResult from the chain.
725
+ """
726
+ if not signatures:
727
+ return VerificationResult(
728
+ is_valid=False,
729
+ trust_level=TrustLevel.UNVERIFIED,
730
+ errors=["No signatures provided"],
731
+ )
732
+
733
+ ctx = context or {}
734
+ if self._first:
735
+ result = self._first.handle(data, signatures, ctx)
736
+ if result:
737
+ return result
738
+
739
+ # If chain didn't produce a result, return unverified
740
+ return VerificationResult(
741
+ is_valid=False,
742
+ trust_level=TrustLevel.UNVERIFIED,
743
+ errors=["Verification chain did not produce a result"],
744
+ )
745
+
746
+
747
+ class VerificationChainBuilder:
748
+ """Builder for verification chains."""
749
+
750
+ def __init__(self) -> None:
751
+ """Initialize the builder."""
752
+ self._handlers: list[VerificationHandler] = []
753
+
754
+ def with_signature_count(self, min_signatures: int = 1) -> "VerificationChainBuilder":
755
+ """Add signature count verification.
756
+
757
+ Args:
758
+ min_signatures: Minimum required signatures.
759
+
760
+ Returns:
761
+ Self for chaining.
762
+ """
763
+ self._handlers.append(SignatureCountHandler(min_signatures))
764
+ return self
765
+
766
+ def with_signer_trust(self, trust_store: TrustStore) -> "VerificationChainBuilder":
767
+ """Add signer trust verification.
768
+
769
+ Args:
770
+ trust_store: Trust store to use.
771
+
772
+ Returns:
773
+ Self for chaining.
774
+ """
775
+ self._handlers.append(SignerTrustHandler(trust_store))
776
+ return self
777
+
778
+ def with_cryptographic_verification(
779
+ self, trust_store: TrustStore
780
+ ) -> "VerificationChainBuilder":
781
+ """Add cryptographic signature verification.
782
+
783
+ Args:
784
+ trust_store: Trust store for public keys.
785
+
786
+ Returns:
787
+ Self for chaining.
788
+ """
789
+ self._handlers.append(CryptographicVerificationHandler(trust_store))
790
+ return self
791
+
792
+ def with_custom_handler(
793
+ self, handler: VerificationHandler
794
+ ) -> "VerificationChainBuilder":
795
+ """Add a custom handler.
796
+
797
+ Args:
798
+ handler: Custom handler.
799
+
800
+ Returns:
801
+ Self for chaining.
802
+ """
803
+ self._handlers.append(handler)
804
+ return self
805
+
806
+ def build(self) -> VerificationChain:
807
+ """Build the verification chain.
808
+
809
+ Returns:
810
+ Configured VerificationChain.
811
+ """
812
+ if not self._handlers:
813
+ return VerificationChain()
814
+
815
+ # Chain handlers together
816
+ for i in range(len(self._handlers) - 1):
817
+ self._handlers[i].set_next(self._handlers[i + 1])
818
+
819
+ return VerificationChain(self._handlers[0])
820
+
821
+
822
+ def create_verification_chain(
823
+ trust_store: TrustStore | None = None,
824
+ min_signatures: int = 1,
825
+ ) -> VerificationChain:
826
+ """Create a standard verification chain.
827
+
828
+ Args:
829
+ trust_store: Trust store for verification.
830
+ min_signatures: Minimum required signatures.
831
+
832
+ Returns:
833
+ Configured VerificationChain.
834
+ """
835
+ store = trust_store or TrustStoreImpl()
836
+ return (
837
+ VerificationChainBuilder()
838
+ .with_signature_count(min_signatures)
839
+ .with_signer_trust(store)
840
+ .with_cryptographic_verification(store)
841
+ .build()
842
+ )