A Python port of the Invisible Internet Project (I2P)
at main 125 lines 4.2 kB view raw
1""" 2Tests for i2p_crypto.x25519 — X25519 ECDH key exchange. 3 4Includes RFC 7748 Section 6.1 test vectors. 5""" 6 7import pytest 8 9from i2p_crypto.x25519 import X25519DH 10 11 12class TestX25519KeyGeneration: 13 """Key generation produces valid 32-byte key pairs.""" 14 15 def test_generate_keypair_returns_32_byte_keys(self): 16 priv, pub = X25519DH.generate_keypair() 17 assert len(priv) == 32 18 assert len(pub) == 32 19 20 def test_generate_keypair_returns_bytes(self): 21 priv, pub = X25519DH.generate_keypair() 22 assert isinstance(priv, bytes) 23 assert isinstance(pub, bytes) 24 25 def test_generate_keypair_unique(self): 26 """Two calls produce different key pairs.""" 27 priv1, pub1 = X25519DH.generate_keypair() 28 priv2, pub2 = X25519DH.generate_keypair() 29 assert priv1 != priv2 30 assert pub1 != pub2 31 32 def test_public_from_private_matches_keypair(self): 33 """public_from_private must agree with generate_keypair.""" 34 priv, pub = X25519DH.generate_keypair() 35 derived_pub = X25519DH.public_from_private(priv) 36 assert derived_pub == pub 37 38 39class TestX25519DH: 40 """DH agreement — both parties compute the same shared secret.""" 41 42 def test_dh_agreement(self): 43 """Alice and Bob independently arrive at the same shared secret.""" 44 alice_priv, alice_pub = X25519DH.generate_keypair() 45 bob_priv, bob_pub = X25519DH.generate_keypair() 46 47 secret_alice = X25519DH.dh(alice_priv, bob_pub) 48 secret_bob = X25519DH.dh(bob_priv, alice_pub) 49 50 assert secret_alice == secret_bob 51 assert len(secret_alice) == 32 52 53 def test_dh_deterministic(self): 54 """Same inputs always produce the same shared secret.""" 55 alice_priv, _ = X25519DH.generate_keypair() 56 _, bob_pub = X25519DH.generate_keypair() 57 58 s1 = X25519DH.dh(alice_priv, bob_pub) 59 s2 = X25519DH.dh(alice_priv, bob_pub) 60 assert s1 == s2 61 62 63class TestX25519RFC7748: 64 """RFC 7748 Section 6.1 test vectors.""" 65 66 # Alice 67 ALICE_PRIV = bytes.fromhex( 68 "77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a" 69 ) 70 ALICE_PUB = bytes.fromhex( 71 "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a" 72 ) 73 74 # Bob 75 BOB_PRIV = bytes.fromhex( 76 "5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb" 77 ) 78 BOB_PUB = bytes.fromhex( 79 "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f" 80 ) 81 82 # Expected shared secret 83 SHARED_SECRET = bytes.fromhex( 84 "4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742" 85 ) 86 87 def test_alice_public_from_private(self): 88 pub = X25519DH.public_from_private(self.ALICE_PRIV) 89 assert pub == self.ALICE_PUB 90 91 def test_bob_public_from_private(self): 92 pub = X25519DH.public_from_private(self.BOB_PRIV) 93 assert pub == self.BOB_PUB 94 95 def test_alice_computes_shared_secret(self): 96 secret = X25519DH.dh(self.ALICE_PRIV, self.BOB_PUB) 97 assert secret == self.SHARED_SECRET 98 99 def test_bob_computes_shared_secret(self): 100 secret = X25519DH.dh(self.BOB_PRIV, self.ALICE_PUB) 101 assert secret == self.SHARED_SECRET 102 103 def test_both_parties_agree(self): 104 """Full round-trip: both sides derive the same secret.""" 105 sa = X25519DH.dh(self.ALICE_PRIV, self.BOB_PUB) 106 sb = X25519DH.dh(self.BOB_PRIV, self.ALICE_PUB) 107 assert sa == sb == self.SHARED_SECRET 108 109 110class TestX25519Validation: 111 """Input validation.""" 112 113 def test_dh_rejects_short_private_key(self): 114 _, pub = X25519DH.generate_keypair() 115 with pytest.raises(ValueError, match="Private key must be 32 bytes"): 116 X25519DH.dh(b"\x00" * 16, pub) 117 118 def test_dh_rejects_short_public_key(self): 119 priv, _ = X25519DH.generate_keypair() 120 with pytest.raises(ValueError, match="Public key must be 32 bytes"): 121 X25519DH.dh(priv, b"\x00" * 16) 122 123 def test_public_from_private_rejects_short_key(self): 124 with pytest.raises(ValueError, match="Private key must be 32 bytes"): 125 X25519DH.public_from_private(b"\x00" * 10)