truthound-dashboard 1.3.0__py3-none-any.whl → 1.4.0__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.
- truthound_dashboard/api/alerts.py +258 -0
- truthound_dashboard/api/anomaly.py +1302 -0
- truthound_dashboard/api/cross_alerts.py +352 -0
- truthound_dashboard/api/deps.py +143 -0
- truthound_dashboard/api/drift_monitor.py +540 -0
- truthound_dashboard/api/lineage.py +1151 -0
- truthound_dashboard/api/maintenance.py +363 -0
- truthound_dashboard/api/middleware.py +373 -1
- truthound_dashboard/api/model_monitoring.py +805 -0
- truthound_dashboard/api/notifications_advanced.py +2452 -0
- truthound_dashboard/api/plugins.py +2096 -0
- truthound_dashboard/api/profile.py +211 -14
- truthound_dashboard/api/reports.py +853 -0
- truthound_dashboard/api/router.py +147 -0
- truthound_dashboard/api/rule_suggestions.py +310 -0
- truthound_dashboard/api/schema_evolution.py +231 -0
- truthound_dashboard/api/sources.py +47 -3
- truthound_dashboard/api/triggers.py +190 -0
- truthound_dashboard/api/validations.py +13 -0
- truthound_dashboard/api/validators.py +333 -4
- truthound_dashboard/api/versioning.py +309 -0
- truthound_dashboard/api/websocket.py +301 -0
- truthound_dashboard/core/__init__.py +27 -0
- truthound_dashboard/core/anomaly.py +1395 -0
- truthound_dashboard/core/anomaly_explainer.py +633 -0
- truthound_dashboard/core/cache.py +206 -0
- truthound_dashboard/core/cached_services.py +422 -0
- truthound_dashboard/core/charts.py +352 -0
- truthound_dashboard/core/connections.py +1069 -42
- truthound_dashboard/core/cross_alerts.py +837 -0
- truthound_dashboard/core/drift_monitor.py +1477 -0
- truthound_dashboard/core/drift_sampling.py +669 -0
- truthound_dashboard/core/i18n/__init__.py +42 -0
- truthound_dashboard/core/i18n/detector.py +173 -0
- truthound_dashboard/core/i18n/messages.py +564 -0
- truthound_dashboard/core/lineage.py +971 -0
- truthound_dashboard/core/maintenance.py +443 -5
- truthound_dashboard/core/model_monitoring.py +1043 -0
- truthound_dashboard/core/notifications/channels.py +1020 -1
- truthound_dashboard/core/notifications/deduplication/__init__.py +143 -0
- truthound_dashboard/core/notifications/deduplication/policies.py +274 -0
- truthound_dashboard/core/notifications/deduplication/service.py +400 -0
- truthound_dashboard/core/notifications/deduplication/stores.py +2365 -0
- truthound_dashboard/core/notifications/deduplication/strategies.py +422 -0
- truthound_dashboard/core/notifications/dispatcher.py +43 -0
- truthound_dashboard/core/notifications/escalation/__init__.py +149 -0
- truthound_dashboard/core/notifications/escalation/backends.py +1384 -0
- truthound_dashboard/core/notifications/escalation/engine.py +429 -0
- truthound_dashboard/core/notifications/escalation/models.py +336 -0
- truthound_dashboard/core/notifications/escalation/scheduler.py +1187 -0
- truthound_dashboard/core/notifications/escalation/state_machine.py +330 -0
- truthound_dashboard/core/notifications/escalation/stores.py +2896 -0
- truthound_dashboard/core/notifications/events.py +49 -0
- truthound_dashboard/core/notifications/metrics/__init__.py +115 -0
- truthound_dashboard/core/notifications/metrics/base.py +528 -0
- truthound_dashboard/core/notifications/metrics/collectors.py +583 -0
- truthound_dashboard/core/notifications/routing/__init__.py +169 -0
- truthound_dashboard/core/notifications/routing/combinators.py +184 -0
- truthound_dashboard/core/notifications/routing/config.py +375 -0
- truthound_dashboard/core/notifications/routing/config_parser.py +867 -0
- truthound_dashboard/core/notifications/routing/engine.py +382 -0
- truthound_dashboard/core/notifications/routing/expression_engine.py +1269 -0
- truthound_dashboard/core/notifications/routing/jinja2_engine.py +774 -0
- truthound_dashboard/core/notifications/routing/rules.py +625 -0
- truthound_dashboard/core/notifications/routing/validator.py +678 -0
- truthound_dashboard/core/notifications/service.py +2 -0
- truthound_dashboard/core/notifications/stats_aggregator.py +850 -0
- truthound_dashboard/core/notifications/throttling/__init__.py +83 -0
- truthound_dashboard/core/notifications/throttling/builder.py +311 -0
- truthound_dashboard/core/notifications/throttling/stores.py +1859 -0
- truthound_dashboard/core/notifications/throttling/throttlers.py +633 -0
- truthound_dashboard/core/openlineage.py +1028 -0
- truthound_dashboard/core/plugins/__init__.py +39 -0
- truthound_dashboard/core/plugins/docs/__init__.py +39 -0
- truthound_dashboard/core/plugins/docs/extractor.py +703 -0
- truthound_dashboard/core/plugins/docs/renderers.py +804 -0
- truthound_dashboard/core/plugins/hooks/__init__.py +63 -0
- truthound_dashboard/core/plugins/hooks/decorators.py +367 -0
- truthound_dashboard/core/plugins/hooks/manager.py +403 -0
- truthound_dashboard/core/plugins/hooks/protocols.py +265 -0
- truthound_dashboard/core/plugins/lifecycle/__init__.py +41 -0
- truthound_dashboard/core/plugins/lifecycle/hot_reload.py +584 -0
- truthound_dashboard/core/plugins/lifecycle/machine.py +419 -0
- truthound_dashboard/core/plugins/lifecycle/states.py +266 -0
- truthound_dashboard/core/plugins/loader.py +504 -0
- truthound_dashboard/core/plugins/registry.py +810 -0
- truthound_dashboard/core/plugins/reporter_executor.py +588 -0
- truthound_dashboard/core/plugins/sandbox/__init__.py +59 -0
- truthound_dashboard/core/plugins/sandbox/code_validator.py +243 -0
- truthound_dashboard/core/plugins/sandbox/engines.py +770 -0
- truthound_dashboard/core/plugins/sandbox/protocols.py +194 -0
- truthound_dashboard/core/plugins/sandbox.py +617 -0
- truthound_dashboard/core/plugins/security/__init__.py +68 -0
- truthound_dashboard/core/plugins/security/analyzer.py +535 -0
- truthound_dashboard/core/plugins/security/policies.py +311 -0
- truthound_dashboard/core/plugins/security/protocols.py +296 -0
- truthound_dashboard/core/plugins/security/signing.py +842 -0
- truthound_dashboard/core/plugins/security.py +446 -0
- truthound_dashboard/core/plugins/validator_executor.py +401 -0
- truthound_dashboard/core/plugins/versioning/__init__.py +51 -0
- truthound_dashboard/core/plugins/versioning/constraints.py +377 -0
- truthound_dashboard/core/plugins/versioning/dependencies.py +541 -0
- truthound_dashboard/core/plugins/versioning/semver.py +266 -0
- truthound_dashboard/core/profile_comparison.py +601 -0
- truthound_dashboard/core/report_history.py +570 -0
- truthound_dashboard/core/reporters/__init__.py +57 -0
- truthound_dashboard/core/reporters/base.py +296 -0
- truthound_dashboard/core/reporters/csv_reporter.py +155 -0
- truthound_dashboard/core/reporters/html_reporter.py +598 -0
- truthound_dashboard/core/reporters/i18n/__init__.py +65 -0
- truthound_dashboard/core/reporters/i18n/base.py +494 -0
- truthound_dashboard/core/reporters/i18n/catalogs.py +930 -0
- truthound_dashboard/core/reporters/json_reporter.py +160 -0
- truthound_dashboard/core/reporters/junit_reporter.py +233 -0
- truthound_dashboard/core/reporters/markdown_reporter.py +207 -0
- truthound_dashboard/core/reporters/pdf_reporter.py +209 -0
- truthound_dashboard/core/reporters/registry.py +272 -0
- truthound_dashboard/core/rule_generator.py +2088 -0
- truthound_dashboard/core/scheduler.py +822 -12
- truthound_dashboard/core/schema_evolution.py +858 -0
- truthound_dashboard/core/services.py +152 -9
- truthound_dashboard/core/statistics.py +718 -0
- truthound_dashboard/core/streaming_anomaly.py +883 -0
- truthound_dashboard/core/triggers/__init__.py +45 -0
- truthound_dashboard/core/triggers/base.py +226 -0
- truthound_dashboard/core/triggers/evaluators.py +609 -0
- truthound_dashboard/core/triggers/factory.py +363 -0
- truthound_dashboard/core/unified_alerts.py +870 -0
- truthound_dashboard/core/validation_limits.py +509 -0
- truthound_dashboard/core/versioning.py +709 -0
- truthound_dashboard/core/websocket/__init__.py +59 -0
- truthound_dashboard/core/websocket/manager.py +512 -0
- truthound_dashboard/core/websocket/messages.py +130 -0
- truthound_dashboard/db/__init__.py +30 -0
- truthound_dashboard/db/models.py +3375 -3
- truthound_dashboard/main.py +22 -0
- truthound_dashboard/schemas/__init__.py +396 -1
- truthound_dashboard/schemas/anomaly.py +1258 -0
- truthound_dashboard/schemas/base.py +4 -0
- truthound_dashboard/schemas/cross_alerts.py +334 -0
- truthound_dashboard/schemas/drift_monitor.py +890 -0
- truthound_dashboard/schemas/lineage.py +428 -0
- truthound_dashboard/schemas/maintenance.py +154 -0
- truthound_dashboard/schemas/model_monitoring.py +374 -0
- truthound_dashboard/schemas/notifications_advanced.py +1363 -0
- truthound_dashboard/schemas/openlineage.py +704 -0
- truthound_dashboard/schemas/plugins.py +1293 -0
- truthound_dashboard/schemas/profile.py +420 -34
- truthound_dashboard/schemas/profile_comparison.py +242 -0
- truthound_dashboard/schemas/reports.py +285 -0
- truthound_dashboard/schemas/rule_suggestion.py +434 -0
- truthound_dashboard/schemas/schema_evolution.py +164 -0
- truthound_dashboard/schemas/source.py +117 -2
- truthound_dashboard/schemas/triggers.py +511 -0
- truthound_dashboard/schemas/unified_alerts.py +223 -0
- truthound_dashboard/schemas/validation.py +25 -1
- truthound_dashboard/schemas/validators/__init__.py +11 -0
- truthound_dashboard/schemas/validators/base.py +151 -0
- truthound_dashboard/schemas/versioning.py +152 -0
- truthound_dashboard/static/index.html +2 -2
- {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/METADATA +142 -18
- truthound_dashboard-1.4.0.dist-info/RECORD +239 -0
- truthound_dashboard/static/assets/index-BCA8H1hO.js +0 -574
- truthound_dashboard/static/assets/index-BNsSQ2fN.css +0 -1
- truthound_dashboard/static/assets/unmerged_dictionaries-CsJWCRx9.js +0 -1
- truthound_dashboard-1.3.0.dist-info/RECORD +0 -110
- {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.3.0.dist-info → truthound_dashboard-1.4.0.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
|
+
)
|