A Python port of the Invisible Internet Project (I2P)
at main 256 lines 10 kB view raw
1"""Tests for SipHash-2-4 implementation. 2 3Test vectors from the SipHash paper by Jean-Philippe Aumasson and Daniel J. Bernstein. 4Key = 00 01 02 ... 0f => k0 = 0x0706050403020100, k1 = 0x0f0e0d0c0b0a0908 5""" 6 7import pytest 8 9from i2p_crypto.siphash import siphash_2_4, SipHashRatchet 10 11 12# Standard key from the SipHash paper 13K0 = 0x0706050403020100 14K1 = 0x0f0e0d0c0b0a0908 15 16# Official test vectors from the SipHash reference implementation. 17# Source: https://github.com/veorq/SipHash/blob/master/vectors.h 18# The reference outputs are given as byte arrays; the uint64_t value 19# is the LE interpretation of those bytes. 20# Key = 00 01 02 ... 0f, input of length n = 00 01 ... (n-1). 21_VEC_BYTES = [ 22 [0x31, 0x0e, 0x0e, 0xdd, 0x47, 0xdb, 0x6f, 0x72], 23 [0xfd, 0x67, 0xdc, 0x93, 0xc5, 0x39, 0xf8, 0x74], 24 [0x5a, 0x4f, 0xa9, 0xd9, 0x09, 0x80, 0x6c, 0x0d], 25] 26 27PAPER_VECTORS = [ 28 (bytes(range(i)), int.from_bytes(bytes(v), "little")) 29 for i, v in enumerate(_VEC_BYTES) 30] 31 32 33class TestSipHash24: 34 """Core SipHash-2-4 function tests.""" 35 36 @pytest.mark.parametrize("data,expected", PAPER_VECTORS) 37 def test_paper_vectors(self, data, expected): 38 """Verify against the official SipHash paper test vectors.""" 39 result = siphash_2_4(K0, K1, data) 40 assert result == expected, ( 41 f"siphash_2_4({K0:#x}, {K1:#x}, {data!r}) = {result:#018x}, " 42 f"expected {expected:#018x}" 43 ) 44 45 def test_return_type_is_int(self): 46 """Return value must be a Python int.""" 47 result = siphash_2_4(K0, K1, b"hello") 48 assert isinstance(result, int) 49 50 def test_output_fits_64_bits(self): 51 """Output must be a 64-bit unsigned integer.""" 52 result = siphash_2_4(K0, K1, b"test data 12345678") 53 assert 0 <= result < (1 << 64) 54 55 def test_different_keys_produce_different_output(self): 56 """Different keys must produce different hashes for the same data.""" 57 data = b"same input" 58 h1 = siphash_2_4(0, 0, data) 59 h2 = siphash_2_4(1, 0, data) 60 h3 = siphash_2_4(0, 1, data) 61 assert h1 != h2 62 assert h1 != h3 63 assert h2 != h3 64 65 def test_different_data_produces_different_output(self): 66 """Different data must produce different hashes for the same key.""" 67 h1 = siphash_2_4(K0, K1, b"aaa") 68 h2 = siphash_2_4(K0, K1, b"aab") 69 assert h1 != h2 70 71 def test_deterministic(self): 72 """Same inputs must always produce the same output.""" 73 data = b"determinism check" 74 h1 = siphash_2_4(K0, K1, data) 75 h2 = siphash_2_4(K0, K1, data) 76 assert h1 == h2 77 78 def test_zero_key_zero_data(self): 79 """Zero key with empty data should still produce a valid hash.""" 80 result = siphash_2_4(0, 0, b"") 81 assert isinstance(result, int) 82 assert 0 <= result < (1 << 64) 83 84 def test_exactly_8_bytes(self): 85 """Data that is exactly one 8-byte block (no padding needed beyond final).""" 86 result = siphash_2_4(K0, K1, b"\x00\x01\x02\x03\x04\x05\x06\x07") 87 assert isinstance(result, int) 88 assert 0 <= result < (1 << 64) 89 90 def test_16_bytes(self): 91 """Data that is exactly two 8-byte blocks.""" 92 data = bytes(range(16)) 93 result = siphash_2_4(K0, K1, data) 94 assert isinstance(result, int) 95 assert 0 <= result < (1 << 64) 96 97 def test_i2p_style_8byte_le_iv(self): 98 """I2P NTCP2 usage: hash the 8-byte LE representation of an IV.""" 99 iv = 42 100 data = iv.to_bytes(8, "little") 101 result = siphash_2_4(K0, K1, data) 102 assert isinstance(result, int) 103 assert 0 <= result < (1 << 64) 104 105 def test_extended_vectors(self): 106 """Extended test vectors from the SipHash reference implementation. 107 108 key = 00 01 02 ... 0f 109 For input of length n, data = 00 01 02 ... (n-1). 110 These are the first several entries from the 64 reference vectors. 111 """ 112 # Reference vectors from vectors.h (byte arrays), converted to 113 # LE uint64 via int.from_bytes(..., 'little'). 114 # Source: https://github.com/veorq/SipHash/blob/master/vectors.h 115 vector_bytes = [ 116 [0x31, 0x0e, 0x0e, 0xdd, 0x47, 0xdb, 0x6f, 0x72], # len 0 117 [0xfd, 0x67, 0xdc, 0x93, 0xc5, 0x39, 0xf8, 0x74], # len 1 118 [0x5a, 0x4f, 0xa9, 0xd9, 0x09, 0x80, 0x6c, 0x0d], # len 2 119 [0x2d, 0x7e, 0xfb, 0xd7, 0x96, 0x66, 0x67, 0x85], # len 3 120 [0xb7, 0x87, 0x71, 0x27, 0xe0, 0x94, 0x27, 0xcf], # len 4 121 [0x8d, 0xa6, 0x99, 0xcd, 0x64, 0x55, 0x76, 0x18], # len 5 122 [0xce, 0xe3, 0xfe, 0x58, 0x6e, 0x46, 0xc9, 0xcb], # len 6 123 [0x37, 0xd1, 0x01, 0x8b, 0xf5, 0x00, 0x02, 0xab], # len 7 124 [0x62, 0x24, 0x93, 0x9a, 0x79, 0xf5, 0xf5, 0x93], # len 8 125 [0xb0, 0xe4, 0xa9, 0x0b, 0xdf, 0x82, 0x00, 0x9e], # len 9 126 [0xf3, 0xb9, 0xdd, 0x94, 0xc5, 0xbb, 0x5d, 0x7a], # len 10 127 [0xa7, 0xad, 0x6b, 0x22, 0x46, 0x2f, 0xb3, 0xf4], # len 11 128 [0xfb, 0xe5, 0x0e, 0x86, 0xbc, 0x8f, 0x1e, 0x75], # len 12 129 [0x90, 0x3d, 0x84, 0xc0, 0x27, 0x56, 0xea, 0x14], # len 13 130 [0xee, 0xf2, 0x7a, 0x8e, 0x90, 0xca, 0x23, 0xf7], # len 14 131 [0xe5, 0x45, 0xbe, 0x49, 0x61, 0xca, 0x29, 0xa1], # len 15 132 ] 133 vectors = [ 134 (i, int.from_bytes(bytes(b), "little")) 135 for i, b in enumerate(vector_bytes) 136 ] 137 for length, expected in vectors: 138 data = bytes(range(length)) 139 result = siphash_2_4(K0, K1, data) 140 assert result == expected, ( 141 f"Vector length={length}: got {result:#018x}, expected {expected:#018x}" 142 ) 143 144 145class TestSipHashRatchet: 146 """SipHashRatchet class tests.""" 147 148 def test_construction(self): 149 """Ratchet can be constructed with k0, k1, iv.""" 150 r = SipHashRatchet(K0, K1, 0) 151 assert r is not None 152 153 def test_next_returns_int(self): 154 """next() returns an integer.""" 155 r = SipHashRatchet(K0, K1, 0) 156 result = r.next() 157 assert isinstance(result, int) 158 159 def test_next_returns_64_bit(self): 160 """next() returns a 64-bit unsigned integer.""" 161 r = SipHashRatchet(K0, K1, 0) 162 result = r.next() 163 assert 0 <= result < (1 << 64) 164 165 def test_next_hashes_iv_as_8byte_le(self): 166 """next() computes SipHash of the IV as 8-byte little-endian data.""" 167 iv = 0 168 r = SipHashRatchet(K0, K1, iv) 169 expected = siphash_2_4(K0, K1, iv.to_bytes(8, "little")) 170 result = r.next() 171 assert result == expected 172 173 def test_ratchet_advances_iv(self): 174 """After next(), the IV is updated to the hash output.""" 175 r = SipHashRatchet(K0, K1, 0) 176 h1 = r.next() 177 # The IV is now h1, so next call should hash h1 178 expected_h2 = siphash_2_4(K0, K1, h1.to_bytes(8, "little")) 179 h2 = r.next() 180 assert h2 == expected_h2 181 182 def test_ratchet_chain_three_steps(self): 183 """Verify a chain of three ratchet steps.""" 184 iv = 12345 185 r = SipHashRatchet(K0, K1, iv) 186 187 h1 = siphash_2_4(K0, K1, iv.to_bytes(8, "little")) 188 assert r.next() == h1 189 190 h2 = siphash_2_4(K0, K1, h1.to_bytes(8, "little")) 191 assert r.next() == h2 192 193 h3 = siphash_2_4(K0, K1, h2.to_bytes(8, "little")) 194 assert r.next() == h3 195 196 def test_different_iv_different_sequence(self): 197 """Different initial IVs produce different sequences.""" 198 r1 = SipHashRatchet(K0, K1, 0) 199 r2 = SipHashRatchet(K0, K1, 1) 200 assert r1.next() != r2.next() 201 202 def test_different_keys_different_sequence(self): 203 """Different keys produce different sequences.""" 204 r1 = SipHashRatchet(0, 0, 42) 205 r2 = SipHashRatchet(1, 0, 42) 206 assert r1.next() != r2.next() 207 208 def test_obfuscate_length_returns_bytes(self): 209 """obfuscate_length returns 2 bytes.""" 210 r = SipHashRatchet(K0, K1, 0) 211 result = r.obfuscate_length(256) 212 assert isinstance(result, bytes) 213 assert len(result) == 2 214 215 def test_deobfuscate_length_returns_int(self): 216 """deobfuscate_length returns an integer.""" 217 r1 = SipHashRatchet(K0, K1, 0) 218 obf = r1.obfuscate_length(256) 219 r2 = SipHashRatchet(K0, K1, 0) 220 result = r2.deobfuscate_length(obf) 221 assert isinstance(result, int) 222 223 def test_obfuscate_deobfuscate_roundtrip(self): 224 """Obfuscating then deobfuscating returns the original length.""" 225 for length in [0, 1, 255, 256, 1000, 65535]: 226 r1 = SipHashRatchet(K0, K1, 0) 227 obf = r1.obfuscate_length(length) 228 r2 = SipHashRatchet(K0, K1, 0) 229 recovered = r2.deobfuscate_length(obf) 230 assert recovered == length, ( 231 f"Roundtrip failed for length={length}: got {recovered}" 232 ) 233 234 def test_obfuscated_differs_from_plaintext(self): 235 """Obfuscated bytes should differ from the plain big-endian length 236 (unless the XOR mask happens to be zero, which is astronomically unlikely).""" 237 r = SipHashRatchet(K0, K1, 42) 238 length = 1000 239 obf = r.obfuscate_length(length) 240 plain = length.to_bytes(2, "big") 241 # With overwhelmingly high probability these differ 242 assert obf != plain 243 244 def test_obfuscate_sequential_lengths(self): 245 """Consecutive obfuscated lengths from the same ratchet should differ 246 because each call advances the ratchet.""" 247 r = SipHashRatchet(K0, K1, 0) 248 results = [r.obfuscate_length(100) for _ in range(5)] 249 # All should be different because the ratchet advances each time 250 assert len(set(results)) == 5 251 252 def test_deobfuscate_requires_2_bytes(self): 253 """deobfuscate_length expects exactly 2 bytes of input.""" 254 r = SipHashRatchet(K0, K1, 0) 255 with pytest.raises((ValueError, IndexError, Exception)): 256 r.deobfuscate_length(b"\x00")