""" Tests for i2p_crypto.x25519 — X25519 ECDH key exchange. Includes RFC 7748 Section 6.1 test vectors. """ import pytest from i2p_crypto.x25519 import X25519DH class TestX25519KeyGeneration: """Key generation produces valid 32-byte key pairs.""" def test_generate_keypair_returns_32_byte_keys(self): priv, pub = X25519DH.generate_keypair() assert len(priv) == 32 assert len(pub) == 32 def test_generate_keypair_returns_bytes(self): priv, pub = X25519DH.generate_keypair() assert isinstance(priv, bytes) assert isinstance(pub, bytes) def test_generate_keypair_unique(self): """Two calls produce different key pairs.""" priv1, pub1 = X25519DH.generate_keypair() priv2, pub2 = X25519DH.generate_keypair() assert priv1 != priv2 assert pub1 != pub2 def test_public_from_private_matches_keypair(self): """public_from_private must agree with generate_keypair.""" priv, pub = X25519DH.generate_keypair() derived_pub = X25519DH.public_from_private(priv) assert derived_pub == pub class TestX25519DH: """DH agreement — both parties compute the same shared secret.""" def test_dh_agreement(self): """Alice and Bob independently arrive at the same shared secret.""" alice_priv, alice_pub = X25519DH.generate_keypair() bob_priv, bob_pub = X25519DH.generate_keypair() secret_alice = X25519DH.dh(alice_priv, bob_pub) secret_bob = X25519DH.dh(bob_priv, alice_pub) assert secret_alice == secret_bob assert len(secret_alice) == 32 def test_dh_deterministic(self): """Same inputs always produce the same shared secret.""" alice_priv, _ = X25519DH.generate_keypair() _, bob_pub = X25519DH.generate_keypair() s1 = X25519DH.dh(alice_priv, bob_pub) s2 = X25519DH.dh(alice_priv, bob_pub) assert s1 == s2 class TestX25519RFC7748: """RFC 7748 Section 6.1 test vectors.""" # Alice ALICE_PRIV = bytes.fromhex( "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a" ) ALICE_PUB = bytes.fromhex( "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a" ) # Bob BOB_PRIV = bytes.fromhex( "5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb" ) BOB_PUB = bytes.fromhex( "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f" ) # Expected shared secret SHARED_SECRET = bytes.fromhex( "4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742" ) def test_alice_public_from_private(self): pub = X25519DH.public_from_private(self.ALICE_PRIV) assert pub == self.ALICE_PUB def test_bob_public_from_private(self): pub = X25519DH.public_from_private(self.BOB_PRIV) assert pub == self.BOB_PUB def test_alice_computes_shared_secret(self): secret = X25519DH.dh(self.ALICE_PRIV, self.BOB_PUB) assert secret == self.SHARED_SECRET def test_bob_computes_shared_secret(self): secret = X25519DH.dh(self.BOB_PRIV, self.ALICE_PUB) assert secret == self.SHARED_SECRET def test_both_parties_agree(self): """Full round-trip: both sides derive the same secret.""" sa = X25519DH.dh(self.ALICE_PRIV, self.BOB_PUB) sb = X25519DH.dh(self.BOB_PRIV, self.ALICE_PUB) assert sa == sb == self.SHARED_SECRET class TestX25519Validation: """Input validation.""" def test_dh_rejects_short_private_key(self): _, pub = X25519DH.generate_keypair() with pytest.raises(ValueError, match="Private key must be 32 bytes"): X25519DH.dh(b"\x00" * 16, pub) def test_dh_rejects_short_public_key(self): priv, _ = X25519DH.generate_keypair() with pytest.raises(ValueError, match="Public key must be 32 bytes"): X25519DH.dh(priv, b"\x00" * 16) def test_public_from_private_rejects_short_key(self): with pytest.raises(ValueError, match="Private key must be 32 bytes"): X25519DH.public_from_private(b"\x00" * 10)