fixes global
This commit is contained in:
1 parent
13dc4f39c8
commit
cad0f6aebe
64 files changed
+10379
-254
No files matched your search
@@ -0,0 +1,136 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import logging
|
||||
from dataclasses import asdict
|
||||
from hashlib import sha256
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
|
||||
from app.core.crypto import get_ed25519_manager
|
||||
from app.core.crypto.content_cipher import ContentCipher
|
||||
from app.core.models.validation.validation_models import ValidationResult, ContentSignature
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ContentValidator:
|
||||
"""
|
||||
Основной валидатор контента:
|
||||
- Проверка подписи источника (Ed25519)
|
||||
- Проверка целостности контента/объектов (checksum/content_id)
|
||||
- Интеграция с ContentCipher для дополнительной верификации
|
||||
"""
|
||||
|
||||
def __init__(self, cipher: Optional[ContentCipher] = None):
|
||||
self.cipher = cipher or ContentCipher()
|
||||
logger.debug("ContentValidator initialized")
|
||||
|
||||
def verify_source_signature(
|
||||
self,
|
||||
payload: Dict[str, Any],
|
||||
signature_b64: Optional[str],
|
||||
public_key_hex: Optional[str],
|
||||
) -> ValidationResult:
|
||||
"""
|
||||
Проверка Ed25519 подписи источника.
|
||||
- payload должен сериализоваться идентично тому, что подписывалось.
|
||||
- signature_b64 - base64 строка подписи.
|
||||
- public_key_hex - hex публичного ключа источника.
|
||||
"""
|
||||
try:
|
||||
if not signature_b64 or not public_key_hex:
|
||||
logger.warning("verify_source_signature: missing signature/public key")
|
||||
return ValidationResult(ok=False, reason="missing_signature_or_public_key")
|
||||
|
||||
crypto_mgr = get_ed25519_manager()
|
||||
ok = crypto_mgr.verify_signature(payload, signature_b64, public_key_hex)
|
||||
if not ok:
|
||||
logger.warning("verify_source_signature: invalid signature")
|
||||
return ValidationResult(ok=False, reason="invalid_signature")
|
||||
|
||||
logger.info("verify_source_signature: signature valid")
|
||||
return ValidationResult(ok=True, details={"signer_key": public_key_hex})
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("verify_source_signature error")
|
||||
return ValidationResult(ok=False, reason=str(e))
|
||||
|
||||
def check_content_integrity(
|
||||
self,
|
||||
encrypted_obj: Dict[str, Any],
|
||||
expected_metadata: Optional[Dict[str, Any]] = None,
|
||||
verify_signature: bool = True,
|
||||
) -> ValidationResult:
|
||||
"""
|
||||
Делегирует проверку целостности ContentCipher:
|
||||
- сверка content_id = sha256(ciphertext||nonce||tag||metadata_json)
|
||||
- опциональная проверка встроенной подписи encrypted_obj (если есть signature/signер_pubkey)
|
||||
"""
|
||||
ok, err = self.cipher.verify_content_integrity(
|
||||
encrypted_obj=encrypted_obj,
|
||||
expected_metadata=expected_metadata,
|
||||
verify_signature=verify_signature,
|
||||
)
|
||||
if not ok:
|
||||
return ValidationResult(ok=False, reason=err or "integrity_failed")
|
||||
|
||||
return ValidationResult(ok=True)
|
||||
|
||||
def validate_content(
|
||||
self,
|
||||
content_meta: Dict[str, Any],
|
||||
*,
|
||||
checksum: Optional[str] = None,
|
||||
source_signature: Optional[ContentSignature] = None,
|
||||
encrypted_obj: Optional[Dict[str, Any]] = None,
|
||||
verify_ed25519: bool = True,
|
||||
) -> ValidationResult:
|
||||
"""
|
||||
Комплексная проверка валидности контента:
|
||||
1) Если указан checksum (<algo>:<hex>), сверяем.
|
||||
2) Если указан source_signature, проверяем Ed25519 подпись источника.
|
||||
3) Если передан encrypted_obj, выполняем углублённую проверку ContentCipher.
|
||||
|
||||
content_meta — произвольная структура метаданных, которая была объектом подписи источника.
|
||||
"""
|
||||
# 1. Проверка checksum (формат: "sha256:<hex>")
|
||||
if checksum:
|
||||
try:
|
||||
algo, hexval = checksum.split(":", 1)
|
||||
algo = algo.lower()
|
||||
if algo != "sha256":
|
||||
logger.warning("validate_content: unsupported checksum algo: %s", algo)
|
||||
return ValidationResult(ok=False, reason="unsupported_checksum_algo", details={"algo": algo})
|
||||
|
||||
# Вычислить sha256 по ожидаемым данным невозможно без исходных байт,
|
||||
# поэтому здесь лишь проверка формата. Фактическая сверка должна происходить
|
||||
# на уровне получателя с использованием известного буфера.
|
||||
if not all(c in "0123456789abcdef" for c in hexval.lower()) or len(hexval) != 64:
|
||||
return ValidationResult(ok=False, reason="invalid_checksum_format")
|
||||
|
||||
logger.debug("validate_content: checksum format looks valid (sha256)")
|
||||
except Exception:
|
||||
return ValidationResult(ok=False, reason="invalid_checksum")
|
||||
|
||||
# 2. Проверка подписи источника (если указана)
|
||||
if verify_ed25519 and source_signature:
|
||||
sig_check = self.verify_source_signature(
|
||||
payload=content_meta,
|
||||
signature_b64=source_signature.signature,
|
||||
public_key_hex=source_signature.public_key_hex,
|
||||
)
|
||||
if not sig_check.ok:
|
||||
return ValidationResult(ok=False, reason="source_signature_invalid", details=sig_check.to_dict())
|
||||
|
||||
# 3. Проверка целостности зашифрованного объекта (если присутствует)
|
||||
if encrypted_obj:
|
||||
integ = self.check_content_integrity(
|
||||
encrypted_obj=encrypted_obj,
|
||||
expected_metadata=encrypted_obj.get("metadata"),
|
||||
verify_signature=verify_ed25519,
|
||||
)
|
||||
if not integ.ok:
|
||||
return ValidationResult(ok=False, reason="encrypted_integrity_invalid", details=integ.to_dict())
|
||||
|
||||
logger.info("validate_content: content validation passed")
|
||||
return ValidationResult(ok=True)
|
||||
@@ -0,0 +1,119 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import logging
|
||||
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
||||
|
||||
from app.core.content.chunk_manager import ChunkManager
|
||||
from app.core.crypto.content_cipher import ContentCipher
|
||||
from app.core.models.content.chunk import ContentChunk
|
||||
from app.core.models.validation.validation_models import ValidationResult
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IntegrityChecker:
|
||||
"""
|
||||
Расширенная проверка целостности контента/чанков поверх возможностей ChunkManager:
|
||||
- Поблочная проверка каждой записи (хеш/подпись)
|
||||
- Обнаружение повреждений и дубликатов
|
||||
- Проверка "цепочки" контента (согласованность content_id/индексов)
|
||||
"""
|
||||
|
||||
def __init__(self, chunk_manager: Optional[ChunkManager] = None, cipher: Optional[ContentCipher] = None):
|
||||
self.chunk_manager = chunk_manager or ChunkManager()
|
||||
self.cipher = cipher or self.chunk_manager.cipher
|
||||
logger.debug("IntegrityChecker initialized")
|
||||
|
||||
def check_chunk_integrity(self, chunk: ContentChunk, verify_signature: bool = True) -> ValidationResult:
|
||||
"""
|
||||
Проверяет единичный чанк, используя ChunkManager.verify_chunk_integrity.
|
||||
"""
|
||||
ok, err = self.chunk_manager.verify_chunk_integrity(chunk, verify_signature=verify_signature)
|
||||
if not ok:
|
||||
logger.warning("check_chunk_integrity: chunk invalid: %s -> %s", chunk.chunk_id, err)
|
||||
return ValidationResult(ok=False, reason=err or "chunk_invalid", details={"chunk_id": chunk.chunk_id})
|
||||
return ValidationResult(ok=True, details={"chunk_id": chunk.chunk_id})
|
||||
|
||||
def detect_corruption(self, chunks: Iterable[ContentChunk]) -> ValidationResult:
|
||||
"""
|
||||
Выявляет повреждения и аномалии:
|
||||
- дубликаты chunk_id/chunk_index
|
||||
- несовпадение content_id между чанками
|
||||
- несогласованность индексов (пропуски/повторы)
|
||||
"""
|
||||
try:
|
||||
chunks_list: List[ContentChunk] = sorted(list(chunks), key=lambda c: c.chunk_index)
|
||||
if not chunks_list:
|
||||
return ValidationResult(ok=True, details={"message": "no chunks"})
|
||||
|
||||
content_ids = {c.content_id for c in chunks_list}
|
||||
if len(content_ids) != 1:
|
||||
return ValidationResult(ok=False, reason="mixed_content_ids", details={"content_ids": list(content_ids)})
|
||||
|
||||
seen_ids = set()
|
||||
seen_indexes = set()
|
||||
duplicates: List[str] = []
|
||||
gaps: List[int] = []
|
||||
|
||||
for c in chunks_list:
|
||||
if c.chunk_id in seen_ids:
|
||||
duplicates.append(c.chunk_id)
|
||||
else:
|
||||
seen_ids.add(c.chunk_id)
|
||||
|
||||
if c.chunk_index in seen_indexes:
|
||||
duplicates.append(f"index:{c.chunk_index}")
|
||||
else:
|
||||
seen_indexes.add(c.chunk_index)
|
||||
|
||||
if chunks_list:
|
||||
min_idx = chunks_list[0].chunk_index
|
||||
max_idx = chunks_list[-1].chunk_index
|
||||
expected = set(range(min_idx, max_idx + 1))
|
||||
gaps = sorted(list(expected - seen_indexes))
|
||||
|
||||
if duplicates or gaps:
|
||||
return ValidationResult(
|
||||
ok=False,
|
||||
reason="structure_anomaly",
|
||||
details={"duplicates": duplicates, "missing_indexes": gaps},
|
||||
)
|
||||
|
||||
return ValidationResult(ok=True, details={"content_id": chunks_list[0].content_id})
|
||||
except Exception as e:
|
||||
logger.exception("detect_corruption error")
|
||||
return ValidationResult(ok=False, reason=str(e))
|
||||
|
||||
def verify_content_chain(
|
||||
self,
|
||||
chunks: Iterable[ContentChunk],
|
||||
verify_signatures: bool = True,
|
||||
) -> ValidationResult:
|
||||
"""
|
||||
Полная проверка набора чанков:
|
||||
1) detect_corruption на структуру/последовательность
|
||||
2) check_chunk_integrity для каждого чанка (хеш/подпись)
|
||||
"""
|
||||
try:
|
||||
chunks_list = list(chunks)
|
||||
structure = self.detect_corruption(chunks_list)
|
||||
if not structure.ok:
|
||||
return structure
|
||||
|
||||
errors: List[Dict[str, Any]] = []
|
||||
ok_count = 0
|
||||
for c in chunks_list:
|
||||
res = self.check_chunk_integrity(c, verify_signature=verify_signatures)
|
||||
if not res.ok:
|
||||
errors.append({"chunk_id": c.chunk_id, "error": res.reason})
|
||||
else:
|
||||
ok_count += 1
|
||||
|
||||
if errors:
|
||||
return ValidationResult(ok=False, reason="chain_integrity_failed", details={"verified_ok": ok_count, "errors": errors})
|
||||
|
||||
return ValidationResult(ok=True, details={"verified_ok": ok_count})
|
||||
except Exception as e:
|
||||
logger.exception("verify_content_chain error")
|
||||
return ValidationResult(ok=False, reason=str(e))
|
||||
@@ -0,0 +1,119 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import asdict
|
||||
from typing import Dict, Optional
|
||||
|
||||
from app.core.models.validation.validation_models import TrustScore, NodeTrust
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TrustManager:
|
||||
"""
|
||||
Управление доверием между нодами.
|
||||
- Хранит score (0.0-1.0), blacklist и флаг manual_override.
|
||||
- Предоставляет API для оценки/обновления/проверки доверия.
|
||||
"""
|
||||
|
||||
def __init__(self, default_score: float = 0.5, min_trusted: float = 0.6):
|
||||
self._nodes: Dict[str, NodeTrust] = {}
|
||||
self.default_score = max(0.0, min(1.0, float(default_score)))
|
||||
self.min_trusted = max(0.0, min(1.0, float(min_trusted)))
|
||||
logger.debug("TrustManager initialized: default_score=%s, min_trusted=%s", self.default_score, self.min_trusted)
|
||||
|
||||
def _get_or_create(self, node_id: str) -> NodeTrust:
|
||||
if node_id not in self._nodes:
|
||||
self._nodes[node_id] = NodeTrust(node_id=node_id, score=self.default_score)
|
||||
logger.info("TrustManager: new node registered with default score: %s", node_id)
|
||||
return self._nodes[node_id]
|
||||
|
||||
def assess_node_trust(self, node_id: str) -> TrustScore:
|
||||
"""
|
||||
Вернуть текущий TrustScore для ноды.
|
||||
"""
|
||||
state = self._get_or_create(node_id)
|
||||
logger.debug("assess_node_trust: %s -> score=%.3f, blacklisted=%s, override=%s",
|
||||
node_id, state.score, state.blacklisted, state.manual_override)
|
||||
return TrustScore(node_id=node_id, score=state.score, reason=("blacklisted" if state.blacklisted else None))
|
||||
|
||||
def update_trust_score(self, node_id: str, delta: float, *, reason: Optional[str] = None) -> TrustScore:
|
||||
"""
|
||||
Обновить score ноды на delta в диапазоне [0.0, 1.0].
|
||||
Положительное delta увеличивает доверие, отрицательное — уменьшает.
|
||||
"""
|
||||
state = self._get_or_create(node_id)
|
||||
prev = state.score
|
||||
state.score = max(0.0, min(1.0, prev + float(delta)))
|
||||
if reason:
|
||||
state.note = reason
|
||||
logger.info("update_trust_score: %s: %.3f -> %.3f (reason=%s)", node_id, prev, state.score, reason)
|
||||
return TrustScore(node_id=node_id, score=state.score, reason=reason)
|
||||
|
||||
def set_blacklist(self, node_id: str, blacklisted: bool = True, *, note: Optional[str] = None) -> NodeTrust:
|
||||
"""
|
||||
Добавить/убрать ноду из blacklist.
|
||||
"""
|
||||
state = self._get_or_create(node_id)
|
||||
state.blacklisted = bool(blacklisted)
|
||||
if note:
|
||||
state.note = note
|
||||
logger.warning("set_blacklist: %s -> %s", node_id, state.blacklisted)
|
||||
return state
|
||||
|
||||
def set_manual_override(self, node_id: str, override: bool = True, *, note: Optional[str] = None) -> NodeTrust:
|
||||
"""
|
||||
Установить ручной override доверия для ноды (форсированное доверие).
|
||||
"""
|
||||
state = self._get_or_create(node_id)
|
||||
state.manual_override = bool(override)
|
||||
if note:
|
||||
state.note = note
|
||||
logger.warning("set_manual_override: %s -> %s", node_id, state.manual_override)
|
||||
return state
|
||||
|
||||
def is_node_trusted(self, node_id: str) -> bool:
|
||||
"""
|
||||
Возвращает True если нода считается доверенной:
|
||||
- НЕ находится в blacklist
|
||||
- Имеет score >= min_trusted
|
||||
- ЛИБО установлен manual_override (в этом случае blacklist игнорируется только если override True)
|
||||
"""
|
||||
state = self._get_or_create(node_id)
|
||||
|
||||
if state.manual_override:
|
||||
logger.debug("is_node_trusted: %s -> True (manual_override)", node_id)
|
||||
return True
|
||||
|
||||
if state.blacklisted:
|
||||
logger.debug("is_node_trusted: %s -> False (blacklisted)", node_id)
|
||||
return False
|
||||
|
||||
trusted = state.score >= self.min_trusted
|
||||
logger.debug("is_node_trusted: %s -> %s (score=%.3f, min_trusted=%.3f)", node_id, trusted, state.score, self.min_trusted)
|
||||
return trusted
|
||||
|
||||
def export_state(self) -> Dict[str, Dict]:
|
||||
"""
|
||||
Экспорт текущего состояния (для сериализации/персистентности).
|
||||
"""
|
||||
return {nid: self._nodes[nid].to_dict() for nid in self._nodes}
|
||||
|
||||
def import_state(self, data: Dict[str, Dict]) -> None:
|
||||
"""
|
||||
Импорт состояния (восстановление из персистентного хранилища).
|
||||
"""
|
||||
self._nodes.clear()
|
||||
for nid, raw in data.items():
|
||||
try:
|
||||
self._nodes[nid] = NodeTrust(
|
||||
node_id=raw["node_id"],
|
||||
score=float(raw.get("score", self.default_score)),
|
||||
blacklisted=bool(raw.get("blacklisted", False)),
|
||||
manual_override=bool(raw.get("manual_override", False)),
|
||||
note=raw.get("note"),
|
||||
updated_at=raw.get("updated_at"),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Failed to import node trust record %s: %s", nid, e)
|
||||
logger.info("TrustManager state imported: nodes=%d", len(self._nodes))
|
||||
Reference in new issue
Block a user