A Python port of the Invisible Internet Project (I2P)
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)