525 lines
23 KiB
Python
525 lines
23 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
test_cognitive_signature.py — Unit tests для cognitive_signature.py
|
||
|
||
Montana Protocol
|
||
Тестирование когнитивных подписей с domain separation
|
||
"""
|
||
|
||
import sys
|
||
import os
|
||
import unittest
|
||
import secrets
|
||
import hashlib
|
||
from datetime import datetime, timezone
|
||
|
||
# Добавляем путь к модулю
|
||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../гиппокамп'))
|
||
from cognitive_signature import (
|
||
DomainType,
|
||
get_domain_prefix,
|
||
CognitiveSignature,
|
||
generate_keypair,
|
||
derive_key_from_passphrase
|
||
)
|
||
|
||
|
||
class TestDomainType(unittest.TestCase):
|
||
"""Тесты типов доменов"""
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════
|
||
# DOMAIN TYPES
|
||
# ═══════════════════════════════════════════════════════════════════════
|
||
|
||
def test_domain_type_values(self):
|
||
"""Проверка значений доменов"""
|
||
self.assertEqual(DomainType.THOUGHT.value, "montana.thought")
|
||
self.assertEqual(DomainType.MESSAGE.value, "montana.message")
|
||
self.assertEqual(DomainType.TRANSACTION.value, "montana.transaction")
|
||
self.assertEqual(DomainType.PRESENCE.value, "montana.presence")
|
||
self.assertEqual(DomainType.NODE_AUTH.value, "montana.node.auth")
|
||
self.assertEqual(DomainType.SYNC.value, "montana.sync")
|
||
|
||
def test_get_domain_prefix(self):
|
||
"""get_domain_prefix() возвращает bytes"""
|
||
prefix = get_domain_prefix(DomainType.THOUGHT)
|
||
|
||
self.assertIsInstance(prefix, bytes)
|
||
self.assertEqual(prefix, b"montana.thought")
|
||
|
||
def test_all_domains_have_prefix(self):
|
||
"""Все домены имеют префикс"""
|
||
for domain in DomainType:
|
||
prefix = get_domain_prefix(domain)
|
||
self.assertIsInstance(prefix, bytes)
|
||
self.assertGreater(len(prefix), 0)
|
||
|
||
|
||
class TestCognitiveSignatureInit(unittest.TestCase):
|
||
"""Тесты инициализации CognitiveSignature"""
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════
|
||
# INITIALIZATION
|
||
# ═══════════════════════════════════════════════════════════════════════
|
||
|
||
def test_init_generates_key(self):
|
||
"""Инициализация генерирует ключ если не указан"""
|
||
signer = CognitiveSignature()
|
||
|
||
self.assertIsNotNone(signer.secret_key)
|
||
self.assertEqual(len(signer.secret_key), 32)
|
||
|
||
def test_init_with_custom_key(self):
|
||
"""Инициализация с кастомным ключом"""
|
||
custom_key = secrets.token_bytes(32)
|
||
signer = CognitiveSignature(secret_key=custom_key)
|
||
|
||
self.assertEqual(signer.secret_key, custom_key)
|
||
|
||
def test_init_with_wrong_key_length(self):
|
||
"""Инициализация с неправильной длиной ключа"""
|
||
wrong_key = secrets.token_bytes(16) # Неправильная длина
|
||
|
||
with self.assertRaises(ValueError):
|
||
CognitiveSignature(secret_key=wrong_key)
|
||
|
||
def test_different_signers_different_keys(self):
|
||
"""Разные signers генерируют разные ключи"""
|
||
signer1 = CognitiveSignature()
|
||
signer2 = CognitiveSignature()
|
||
|
||
self.assertNotEqual(signer1.secret_key, signer2.secret_key)
|
||
|
||
|
||
class TestCognitiveSignatureSign(unittest.TestCase):
|
||
"""Тесты подписания"""
|
||
|
||
def setUp(self):
|
||
"""Создаём signer для каждого теста"""
|
||
self.signer = CognitiveSignature()
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════
|
||
# SIGNING
|
||
# ═══════════════════════════════════════════════════════════════════════
|
||
|
||
def test_sign_basic(self):
|
||
"""Базовое подписание"""
|
||
message = "Test message"
|
||
signed = self.signer.sign(message, DomainType.MESSAGE)
|
||
|
||
self.assertIn("message", signed)
|
||
self.assertIn("domain", signed)
|
||
self.assertIn("timestamp", signed)
|
||
self.assertIn("signature", signed)
|
||
self.assertIn("metadata", signed)
|
||
|
||
def test_sign_preserves_message(self):
|
||
"""sign() сохраняет оригинальное сообщение"""
|
||
message = "Test message"
|
||
signed = self.signer.sign(message, DomainType.MESSAGE)
|
||
|
||
self.assertEqual(signed["message"], message)
|
||
|
||
def test_sign_includes_domain(self):
|
||
"""sign() включает домен"""
|
||
signed = self.signer.sign("message", DomainType.THOUGHT)
|
||
|
||
self.assertEqual(signed["domain"], "montana.thought")
|
||
|
||
def test_sign_generates_signature(self):
|
||
"""sign() генерирует подпись"""
|
||
signed = self.signer.sign("message", DomainType.MESSAGE)
|
||
|
||
self.assertIsInstance(signed["signature"], str)
|
||
self.assertEqual(len(signed["signature"]), 64) # SHA256 hex = 64 символа
|
||
|
||
def test_sign_with_timestamp(self):
|
||
"""sign() с кастомным timestamp"""
|
||
timestamp = "2026-01-20T12:00:00+00:00"
|
||
signed = self.signer.sign("message", DomainType.MESSAGE, timestamp=timestamp)
|
||
|
||
self.assertEqual(signed["timestamp"], timestamp)
|
||
|
||
def test_sign_generates_timestamp(self):
|
||
"""sign() генерирует timestamp если не указан"""
|
||
signed = self.signer.sign("message", DomainType.MESSAGE)
|
||
|
||
self.assertIsNotNone(signed["timestamp"])
|
||
# Должен быть в ISO format
|
||
datetime.fromisoformat(signed["timestamp"]) # Не должно упасть
|
||
|
||
def test_sign_with_metadata(self):
|
||
"""sign() с метаданными"""
|
||
metadata = {"user_id": 123, "custom": "data"}
|
||
signed = self.signer.sign("message", DomainType.MESSAGE, metadata=metadata)
|
||
|
||
self.assertEqual(signed["metadata"], metadata)
|
||
|
||
def test_sign_same_message_different_signature(self):
|
||
"""Одинаковое сообщение с разным timestamp → разные подписи"""
|
||
message = "Test"
|
||
|
||
signed1 = self.signer.sign(message, DomainType.MESSAGE, timestamp="2026-01-01T00:00:00+00:00")
|
||
signed2 = self.signer.sign(message, DomainType.MESSAGE, timestamp="2026-01-02T00:00:00+00:00")
|
||
|
||
self.assertNotEqual(signed1["signature"], signed2["signature"])
|
||
|
||
|
||
class TestCognitiveSignatureVerify(unittest.TestCase):
|
||
"""Тесты верификации"""
|
||
|
||
def setUp(self):
|
||
"""Создаём signer для каждого теста"""
|
||
self.signer = CognitiveSignature()
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════
|
||
# VERIFICATION
|
||
# ═══════════════════════════════════════════════════════════════════════
|
||
|
||
def test_verify_valid_signature(self):
|
||
"""Верификация валидной подписи"""
|
||
signed = self.signer.sign("message", DomainType.MESSAGE)
|
||
is_valid = self.signer.verify(signed)
|
||
|
||
self.assertTrue(is_valid)
|
||
|
||
def test_verify_with_domain_check(self):
|
||
"""Верификация с проверкой домена"""
|
||
signed = self.signer.sign("message", DomainType.MESSAGE)
|
||
is_valid = self.signer.verify(signed, domain=DomainType.MESSAGE)
|
||
|
||
self.assertTrue(is_valid)
|
||
|
||
def test_verify_wrong_domain(self):
|
||
"""Верификация с неправильным доменом"""
|
||
signed = self.signer.sign("message", DomainType.MESSAGE)
|
||
is_valid = self.signer.verify(signed, domain=DomainType.THOUGHT)
|
||
|
||
self.assertFalse(is_valid)
|
||
|
||
def test_verify_tampered_message(self):
|
||
"""Верификация подделанного сообщения"""
|
||
signed = self.signer.sign("original", DomainType.MESSAGE)
|
||
|
||
# Подделываем сообщение
|
||
signed["message"] = "tampered"
|
||
|
||
is_valid = self.signer.verify(signed)
|
||
|
||
self.assertFalse(is_valid)
|
||
|
||
def test_verify_tampered_signature(self):
|
||
"""Верификация подделанной подписи"""
|
||
signed = self.signer.sign("message", DomainType.MESSAGE)
|
||
|
||
# Подделываем подпись
|
||
signed["signature"] = "0" * 64
|
||
|
||
is_valid = self.signer.verify(signed)
|
||
|
||
self.assertFalse(is_valid)
|
||
|
||
def test_verify_tampered_timestamp(self):
|
||
"""Верификация с подделанным timestamp"""
|
||
signed = self.signer.sign("message", DomainType.MESSAGE)
|
||
|
||
# Подделываем timestamp
|
||
signed["timestamp"] = "2030-01-01T00:00:00+00:00"
|
||
|
||
is_valid = self.signer.verify(signed)
|
||
|
||
self.assertFalse(is_valid)
|
||
|
||
def test_verify_different_signer(self):
|
||
"""Верификация подписи другим signer"""
|
||
signed = self.signer.sign("message", DomainType.MESSAGE)
|
||
|
||
# Другой signer с другим ключом
|
||
other_signer = CognitiveSignature()
|
||
is_valid = other_signer.verify(signed)
|
||
|
||
self.assertFalse(is_valid)
|
||
|
||
def test_verify_invalid_format(self):
|
||
"""Верификация невалидного формата"""
|
||
invalid_data = {"not": "correct", "format": "at all"}
|
||
|
||
is_valid = self.signer.verify(invalid_data)
|
||
|
||
self.assertFalse(is_valid)
|
||
|
||
|
||
class TestCognitiveSignatureDomainSeparation(unittest.TestCase):
|
||
"""Тесты domain separation"""
|
||
|
||
def setUp(self):
|
||
"""Создаём signer для каждого теста"""
|
||
self.signer = CognitiveSignature()
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════
|
||
# DOMAIN SEPARATION
|
||
# ═══════════════════════════════════════════════════════════════════════
|
||
|
||
def test_same_message_different_domains(self):
|
||
"""Одно сообщение в разных доменах → разные подписи"""
|
||
message = "Test message"
|
||
timestamp = "2026-01-20T12:00:00+00:00"
|
||
|
||
signed_thought = self.signer.sign(message, DomainType.THOUGHT, timestamp=timestamp)
|
||
signed_message = self.signer.sign(message, DomainType.MESSAGE, timestamp=timestamp)
|
||
|
||
self.assertNotEqual(signed_thought["signature"], signed_message["signature"])
|
||
|
||
def test_domain_separation_prevents_replay(self):
|
||
"""Domain separation предотвращает replay attacks"""
|
||
# Подписываем как THOUGHT
|
||
signed = self.signer.sign("message", DomainType.THOUGHT)
|
||
|
||
# Пытаемся верифицировать как MESSAGE
|
||
is_valid = self.signer.verify(signed, domain=DomainType.MESSAGE)
|
||
|
||
# Должно быть невалидно
|
||
self.assertFalse(is_valid)
|
||
|
||
def test_all_domains_produce_different_signatures(self):
|
||
"""Все домены производят разные подписи для одного сообщения"""
|
||
message = "Test"
|
||
timestamp = "2026-01-20T12:00:00+00:00"
|
||
|
||
signatures = set()
|
||
for domain in DomainType:
|
||
signed = self.signer.sign(message, domain, timestamp=timestamp)
|
||
signatures.add(signed["signature"])
|
||
|
||
# Все подписи должны быть уникальными
|
||
self.assertEqual(len(signatures), len(DomainType))
|
||
|
||
|
||
class TestCognitiveSignatureConvenienceMethods(unittest.TestCase):
|
||
"""Тесты вспомогательных методов"""
|
||
|
||
def setUp(self):
|
||
"""Создаём signer для каждого теста"""
|
||
self.signer = CognitiveSignature()
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════
|
||
# CONVENIENCE METHODS
|
||
# ═══════════════════════════════════════════════════════════════════════
|
||
|
||
def test_sign_thought(self):
|
||
"""sign_thought() работает корректно"""
|
||
thought = "Маска тяжелее лица"
|
||
signed = self.signer.sign_thought(thought, user_id=123)
|
||
|
||
self.assertEqual(signed["message"], thought)
|
||
self.assertEqual(signed["domain"], "montana.thought")
|
||
self.assertEqual(signed["metadata"]["user_id"], 123)
|
||
|
||
def test_sign_message(self):
|
||
"""sign_message() работает корректно"""
|
||
message = "Hello"
|
||
signed = self.signer.sign_message(message, sender_id=456)
|
||
|
||
self.assertEqual(signed["message"], message)
|
||
self.assertEqual(signed["domain"], "montana.message")
|
||
self.assertEqual(signed["metadata"]["sender_id"], 456)
|
||
|
||
def test_sign_transaction(self):
|
||
"""sign_transaction() работает корректно"""
|
||
tx_data = "tx_123"
|
||
signed = self.signer.sign_transaction(
|
||
tx_data,
|
||
from_address="addr1",
|
||
to_address="addr2",
|
||
amount=100.0
|
||
)
|
||
|
||
self.assertEqual(signed["message"], tx_data)
|
||
self.assertEqual(signed["domain"], "montana.transaction")
|
||
self.assertEqual(signed["metadata"]["from"], "addr1")
|
||
self.assertEqual(signed["metadata"]["to"], "addr2")
|
||
self.assertEqual(signed["metadata"]["amount"], 100.0)
|
||
|
||
def test_sign_presence(self):
|
||
"""sign_presence() работает корректно"""
|
||
presence_data = "presence_check_123"
|
||
signed = self.signer.sign_presence(presence_data, user_id=789)
|
||
|
||
self.assertEqual(signed["message"], presence_data)
|
||
self.assertEqual(signed["domain"], "montana.presence")
|
||
self.assertEqual(signed["metadata"]["user_id"], 789)
|
||
|
||
|
||
class TestKeyManagement(unittest.TestCase):
|
||
"""Тесты управления ключами"""
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════
|
||
# KEY MANAGEMENT
|
||
# ═══════════════════════════════════════════════════════════════════════
|
||
|
||
def test_generate_keypair(self):
|
||
"""generate_keypair() генерирует пару ключей"""
|
||
secret_key, public_key_hash = generate_keypair()
|
||
|
||
self.assertIsInstance(secret_key, bytes)
|
||
self.assertIsInstance(public_key_hash, bytes)
|
||
self.assertEqual(len(secret_key), 32)
|
||
self.assertEqual(len(public_key_hash), 32)
|
||
|
||
def test_generate_keypair_different_keys(self):
|
||
"""generate_keypair() генерирует разные ключи"""
|
||
key1, _ = generate_keypair()
|
||
key2, _ = generate_keypair()
|
||
|
||
self.assertNotEqual(key1, key2)
|
||
|
||
def test_public_key_is_hash_of_secret(self):
|
||
"""Публичный ключ — хеш секретного"""
|
||
secret_key, public_key_hash = generate_keypair()
|
||
|
||
expected_hash = hashlib.sha256(secret_key).digest()
|
||
self.assertEqual(public_key_hash, expected_hash)
|
||
|
||
def test_derive_key_from_passphrase(self):
|
||
"""derive_key_from_passphrase() создаёт ключ"""
|
||
passphrase = "my secure passphrase"
|
||
salt = secrets.token_bytes(16)
|
||
|
||
key = derive_key_from_passphrase(passphrase, salt)
|
||
|
||
self.assertIsInstance(key, bytes)
|
||
self.assertEqual(len(key), 32)
|
||
|
||
def test_derive_key_deterministic(self):
|
||
"""derive_key_from_passphrase() детерминирован"""
|
||
passphrase = "test"
|
||
salt = b"0" * 16
|
||
|
||
key1 = derive_key_from_passphrase(passphrase, salt)
|
||
key2 = derive_key_from_passphrase(passphrase, salt)
|
||
|
||
self.assertEqual(key1, key2)
|
||
|
||
def test_derive_key_different_passphrase(self):
|
||
"""Разные passphrase → разные ключи"""
|
||
salt = secrets.token_bytes(16)
|
||
|
||
key1 = derive_key_from_passphrase("passphrase1", salt)
|
||
key2 = derive_key_from_passphrase("passphrase2", salt)
|
||
|
||
self.assertNotEqual(key1, key2)
|
||
|
||
def test_derive_key_different_salt(self):
|
||
"""Разная соль → разные ключи"""
|
||
passphrase = "test"
|
||
|
||
key1 = derive_key_from_passphrase(passphrase, b"0" * 16)
|
||
key2 = derive_key_from_passphrase(passphrase, b"1" * 16)
|
||
|
||
self.assertNotEqual(key1, key2)
|
||
|
||
|
||
class TestSecurityProperties(unittest.TestCase):
|
||
"""Тесты свойств безопасности"""
|
||
|
||
def setUp(self):
|
||
"""Создаём signer для каждого теста"""
|
||
self.signer = CognitiveSignature()
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════
|
||
# SECURITY PROPERTIES
|
||
# ═══════════════════════════════════════════════════════════════════════
|
||
|
||
def test_signature_length_constant(self):
|
||
"""Длина подписи константна"""
|
||
short = self.signer.sign("a", DomainType.MESSAGE)
|
||
long = self.signer.sign("a" * 10000, DomainType.MESSAGE)
|
||
|
||
self.assertEqual(len(short["signature"]), len(long["signature"]))
|
||
self.assertEqual(len(short["signature"]), 64) # SHA256 hex
|
||
|
||
def test_cannot_forge_signature(self):
|
||
"""Невозможно подделать подпись без ключа"""
|
||
message = "Original message"
|
||
signed = self.signer.sign(message, DomainType.MESSAGE)
|
||
|
||
# Пытаемся подделать подпись для другого сообщения
|
||
forged = signed.copy()
|
||
forged["message"] = "Forged message"
|
||
# Подпись остаётся той же
|
||
|
||
is_valid = self.signer.verify(forged)
|
||
|
||
self.assertFalse(is_valid)
|
||
|
||
def test_timing_attack_resistance(self):
|
||
"""Использование hmac.compare_digest для защиты от timing attacks"""
|
||
signed = self.signer.sign("message", DomainType.MESSAGE)
|
||
|
||
# Создаём два почти одинаковых сообщения
|
||
valid = signed.copy()
|
||
invalid = signed.copy()
|
||
invalid["signature"] = invalid["signature"][:-1] + ("0" if invalid["signature"][-1] != "0" else "1")
|
||
|
||
# Оба должны верифицироваться за примерно одинаковое время
|
||
# (мы не можем измерить время в unit тесте, но знаем что используется hmac.compare_digest)
|
||
result_valid = self.signer.verify(valid)
|
||
result_invalid = self.signer.verify(invalid)
|
||
|
||
self.assertTrue(result_valid)
|
||
self.assertFalse(result_invalid)
|
||
|
||
|
||
class TestEdgeCases(unittest.TestCase):
|
||
"""Тесты граничных случаев"""
|
||
|
||
def setUp(self):
|
||
"""Создаём signer для каждого теста"""
|
||
self.signer = CognitiveSignature()
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════
|
||
# EDGE CASES
|
||
# ═══════════════════════════════════════════════════════════════════════
|
||
|
||
def test_empty_message(self):
|
||
"""Подписание пустого сообщения"""
|
||
signed = self.signer.sign("", DomainType.MESSAGE)
|
||
|
||
self.assertEqual(signed["message"], "")
|
||
is_valid = self.signer.verify(signed)
|
||
self.assertTrue(is_valid)
|
||
|
||
def test_unicode_message(self):
|
||
"""Подписание Unicode сообщения"""
|
||
message = "Монтана Montana 蒙大拿 金元Ɉ"
|
||
signed = self.signer.sign(message, DomainType.THOUGHT)
|
||
|
||
self.assertEqual(signed["message"], message)
|
||
is_valid = self.signer.verify(signed)
|
||
self.assertTrue(is_valid)
|
||
|
||
def test_very_long_message(self):
|
||
"""Подписание очень длинного сообщения"""
|
||
message = "a" * 1_000_000 # 1 миллион символов
|
||
signed = self.signer.sign(message, DomainType.MESSAGE)
|
||
|
||
is_valid = self.signer.verify(signed)
|
||
self.assertTrue(is_valid)
|
||
|
||
def test_special_characters(self):
|
||
"""Подписание специальных символов"""
|
||
message = "\\n\\t\\r!@#$%^&*()[]{}||"
|
||
signed = self.signer.sign(message, DomainType.MESSAGE)
|
||
|
||
is_valid = self.signer.verify(signed)
|
||
self.assertTrue(is_valid)
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
# RUN TESTS
|
||
# ═══════════════════════════════════════════════════════════════════════════
|
||
|
||
if __name__ == '__main__':
|
||
# Запускаем с verbose output
|
||
unittest.main(verbosity=2)
|