A Python port of the Invisible Internet Project (I2P)
at main 417 lines 15 kB view raw
1"""Tests for i2p_crypto — hash, HMAC, HKDF with byte-identical parity checks.""" 2 3import hashlib 4import hmac as stdlib_hmac 5 6import pytest 7 8 9# === Hash data structures === 10 11class TestHash: 12 def test_create_32_bytes(self): 13 from i2p_crypto.hash_data import Hash 14 data = bytes(range(32)) 15 h = Hash(data) 16 assert h.data == data 17 18 def test_reject_wrong_length(self): 19 from i2p_crypto.hash_data import Hash 20 with pytest.raises(ValueError): 21 Hash(b"too short") 22 23 def test_equality(self): 24 from i2p_crypto.hash_data import Hash 25 a = Hash(b"\x00" * 32) 26 b = Hash(b"\x00" * 32) 27 assert a == b 28 29 def test_inequality(self): 30 from i2p_crypto.hash_data import Hash 31 a = Hash(b"\x00" * 32) 32 b = Hash(b"\x01" + b"\x00" * 31) 33 assert a != b 34 35 def test_hash_code_uses_first_4_bytes(self): 36 from i2p_crypto.hash_data import Hash 37 data = b"\xde\xad\xbe\xef" + b"\x00" * 28 38 h = Hash(data) 39 assert hash(h) == int.from_bytes(b"\xde\xad\xbe\xef", "big") 40 41 def test_create_from_offset(self): 42 from i2p_crypto.hash_data import Hash 43 full = b"\xff" * 10 + bytes(range(32)) + b"\xff" * 10 44 h = Hash.create(full, 10) 45 assert h.data == bytes(range(32)) 46 47 def test_fake_hash(self): 48 from i2p_crypto.hash_data import Hash 49 assert Hash.FAKE_HASH.data == b"\x00" * 32 50 51 def test_bytes_conversion(self): 52 from i2p_crypto.hash_data import Hash 53 data = bytes(range(32)) 54 h = Hash(data) 55 assert bytes(h) == data 56 57 def test_none_creates_zero_hash(self): 58 from i2p_crypto.hash_data import Hash 59 h = Hash() 60 assert h.data == b"\x00" * 32 61 62 def test_immutable_data(self): 63 from i2p_crypto.hash_data import Hash 64 data = bytearray(32) 65 h = Hash(bytes(data)) 66 data[0] = 0xFF 67 assert h.data[0] == 0 # Should not be affected 68 69 70class TestSHA1Hash: 71 def test_create_20_bytes(self): 72 from i2p_crypto.hash_data import SHA1Hash 73 data = bytes(range(20)) 74 h = SHA1Hash(data) 75 assert h.data == data 76 assert SHA1Hash.HASH_LENGTH == 20 77 78 def test_reject_wrong_length(self): 79 from i2p_crypto.hash_data import SHA1Hash 80 with pytest.raises(ValueError): 81 SHA1Hash(b"too short") 82 83 def test_equality(self): 84 from i2p_crypto.hash_data import SHA1Hash 85 a = SHA1Hash(b"\x00" * 20) 86 b = SHA1Hash(b"\x00" * 20) 87 assert a == b 88 89 90class TestHash384: 91 def test_length(self): 92 from i2p_crypto.hash_data import Hash384 93 assert Hash384.HASH_LENGTH == 48 94 h = Hash384(b"\x00" * 48) 95 assert len(h.data) == 48 96 97 def test_reject_wrong_length(self): 98 from i2p_crypto.hash_data import Hash384 99 with pytest.raises(ValueError): 100 Hash384(b"\x00" * 32) 101 102 103class TestHash512: 104 def test_length(self): 105 from i2p_crypto.hash_data import Hash512 106 assert Hash512.HASH_LENGTH == 64 107 h = Hash512(b"\x00" * 64) 108 assert len(h.data) == 64 109 110 def test_reject_wrong_length(self): 111 from i2p_crypto.hash_data import Hash512 112 with pytest.raises(ValueError): 113 Hash512(b"\x00" * 32) 114 115 116# === SHA256Generator — byte-identical with hashlib === 117 118class TestSHA256Generator: 119 def test_known_vector_empty(self): 120 """SHA-256 of empty string — NIST test vector.""" 121 from i2p_crypto.sha256_generator import SHA256Generator 122 gen = SHA256Generator.get_instance() 123 h = gen.calculate_hash(b"") 124 expected = bytes.fromhex( 125 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" 126 ) 127 assert h.data == expected 128 129 def test_known_vector_abc(self): 130 """SHA-256 of 'abc' — NIST test vector.""" 131 from i2p_crypto.sha256_generator import SHA256Generator 132 gen = SHA256Generator.get_instance() 133 h = gen.calculate_hash(b"abc") 134 expected = bytes.fromhex( 135 "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" 136 ) 137 assert h.data == expected 138 139 def test_known_vector_long(self): 140 """SHA-256 of 'abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq'.""" 141 from i2p_crypto.sha256_generator import SHA256Generator 142 gen = SHA256Generator.get_instance() 143 msg = b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq" 144 h = gen.calculate_hash(msg) 145 expected = bytes.fromhex( 146 "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1" 147 ) 148 assert h.data == expected 149 150 def test_matches_hashlib(self): 151 """Byte-identical with Python's hashlib.sha256.""" 152 from i2p_crypto.sha256_generator import SHA256Generator 153 gen = SHA256Generator.get_instance() 154 for data in [b"", b"hello", b"x" * 1000, bytes(range(256))]: 155 assert gen.calculate_hash(data).data == hashlib.sha256(data).digest() 156 157 def test_offset_and_length(self): 158 from i2p_crypto.sha256_generator import SHA256Generator 159 gen = SHA256Generator.get_instance() 160 data = b"XXXhelloXXX" 161 h = gen.calculate_hash(data, 3, 5) 162 assert h.data == hashlib.sha256(b"hello").digest() 163 164 def test_calculate_hash_into(self): 165 from i2p_crypto.sha256_generator import SHA256Generator 166 gen = SHA256Generator.get_instance() 167 out = bytearray(48) 168 gen.calculate_hash_into(b"test", 0, 4, out, 8) 169 expected = hashlib.sha256(b"test").digest() 170 assert out[8:40] == expected 171 assert out[:8] == b"\x00" * 8 172 assert out[40:] == b"\x00" * 8 173 174 def test_digest_convenience(self): 175 from i2p_crypto.sha256_generator import SHA256Generator 176 assert SHA256Generator.digest(b"hello") == hashlib.sha256(b"hello").digest() 177 178 def test_singleton(self): 179 from i2p_crypto.sha256_generator import SHA256Generator 180 a = SHA256Generator.get_instance() 181 b = SHA256Generator.get_instance() 182 assert a is b 183 184 def test_returns_hash_object(self): 185 from i2p_crypto.sha256_generator import SHA256Generator 186 from i2p_crypto.hash_data import Hash 187 gen = SHA256Generator.get_instance() 188 h = gen.calculate_hash(b"data") 189 assert isinstance(h, Hash) 190 assert len(h.data) == 32 191 192 193# === HMAC256Generator — byte-identical with hmac module === 194 195class TestHMAC256Generator: 196 def test_rfc4231_vector_1(self): 197 """RFC 4231 Test Case 1 for HMAC-SHA256.""" 198 from i2p_crypto.hmac_generator import HMAC256Generator 199 gen = HMAC256Generator.get_instance() 200 key = b"\x0b" * 20 + b"\x00" * 12 # pad to 32 201 data = b"Hi There" 202 out = bytearray(32) 203 gen.calculate(key, data, 0, len(data), out, 0) 204 # HMAC-SHA256 with key=0b*20 (first 32 bytes, padded with zeros) 205 expected = stdlib_hmac.new(key[:32], data, hashlib.sha256).digest() 206 assert bytes(out) == expected 207 208 def test_matches_stdlib_hmac(self): 209 """Byte-identical with Python's hmac.new(key, data, sha256).""" 210 from i2p_crypto.hmac_generator import HMAC256Generator 211 gen = HMAC256Generator.get_instance() 212 for key, data in [ 213 (b"\x00" * 32, b""), 214 (bytes(range(32)), b"hello world"), 215 (b"\xff" * 32, b"x" * 1000), 216 (bytes(range(32)), bytes(range(256))), 217 ]: 218 result = gen.calculate(key, data) 219 expected = stdlib_hmac.new(key[:32], data, hashlib.sha256).digest() 220 assert result == expected 221 222 def test_calculate_with_offset(self): 223 from i2p_crypto.hmac_generator import HMAC256Generator 224 gen = HMAC256Generator.get_instance() 225 key = bytes(range(32)) 226 data = b"XXXhelloXXX" 227 result = gen.calculate(key, data, 3, 5) 228 expected = stdlib_hmac.new(key, b"hello", hashlib.sha256).digest() 229 assert result == expected 230 231 def test_calculate_into_buffer(self): 232 from i2p_crypto.hmac_generator import HMAC256Generator 233 gen = HMAC256Generator.get_instance() 234 key = bytes(range(32)) 235 data = b"test" 236 target = bytearray(48) 237 gen.calculate(key, data, 0, 4, target, 8) 238 expected = stdlib_hmac.new(key, data, hashlib.sha256).digest() 239 assert target[8:40] == expected 240 assert target[:8] == b"\x00" * 8 241 242 def test_verify_correct(self): 243 from i2p_crypto.hmac_generator import HMAC256Generator 244 gen = HMAC256Generator.get_instance() 245 key = bytes(range(32)) 246 data = b"verify me" 247 mac = gen.calculate(key, data) 248 assert gen.verify(key, data, 0, len(data), mac) 249 250 def test_verify_wrong_mac(self): 251 from i2p_crypto.hmac_generator import HMAC256Generator 252 gen = HMAC256Generator.get_instance() 253 key = bytes(range(32)) 254 data = b"verify me" 255 mac = bytearray(gen.calculate(key, data)) 256 mac[0] ^= 0xFF # corrupt 257 assert not gen.verify(key, data, 0, len(data), bytes(mac)) 258 259 def test_verify_partial_length(self): 260 from i2p_crypto.hmac_generator import HMAC256Generator 261 gen = HMAC256Generator.get_instance() 262 key = bytes(range(32)) 263 data = b"partial" 264 mac = gen.calculate(key, data) 265 # Verify only first 16 bytes 266 assert gen.verify(key, data, 0, len(data), mac, 0, 16) 267 268 def test_singleton(self): 269 from i2p_crypto.hmac_generator import HMAC256Generator 270 a = HMAC256Generator.get_instance() 271 b = HMAC256Generator.get_instance() 272 assert a is b 273 274 275# === HKDF — RFC 5869 test vectors === 276 277class TestHKDF: 278 def test_rfc5869_case1(self): 279 """RFC 5869 Test Case 1.""" 280 from i2p_crypto.hkdf import HKDF 281 hkdf = HKDF() 282 ikm = bytes.fromhex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b") 283 salt = bytes.fromhex("000102030405060708090a0b0c") 284 info = bytes.fromhex("f0f1f2f3f4f5f6f7f8f9") 285 expected_okm = bytes.fromhex( 286 "3cb25f25faacd57a90434f64d0362f2a" 287 "2d2d0a90cf1a5a4c5db02d56ecc4c5bf" 288 "34007208d5b887185865" 289 ) 290 okm = hkdf.extract_and_expand(salt, ikm, info, 42) 291 assert okm == expected_okm 292 293 def test_rfc5869_case2(self): 294 """RFC 5869 Test Case 2.""" 295 from i2p_crypto.hkdf import HKDF 296 hkdf = HKDF() 297 ikm = bytes.fromhex( 298 "000102030405060708090a0b0c0d0e0f" 299 "101112131415161718191a1b1c1d1e1f" 300 "202122232425262728292a2b2c2d2e2f" 301 "303132333435363738393a3b3c3d3e3f" 302 "404142434445464748494a4b4c4d4e4f" 303 ) 304 salt = bytes.fromhex( 305 "606162636465666768696a6b6c6d6e6f" 306 "707172737475767778797a7b7c7d7e7f" 307 "808182838485868788898a8b8c8d8e8f" 308 "909192939495969798999a9b9c9d9e9f" 309 "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf" 310 ) 311 info = bytes.fromhex( 312 "b0b1b2b3b4b5b6b7b8b9babbbcbdbebf" 313 "c0c1c2c3c4c5c6c7c8c9cacbcccdcecf" 314 "d0d1d2d3d4d5d6d7d8d9dadbdcdddedf" 315 "e0e1e2e3e4e5e6e7e8e9eaebecedeeef" 316 "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff" 317 ) 318 expected_okm = bytes.fromhex( 319 "b11e398dc80327a1c8e7f78c596a4934" 320 "4f012eda2d4efad8a050cc4c19afa97c" 321 "59045a99cac7827271cb41c65e590e09" 322 "da3275600c2f09b8367793a9aca3db71" 323 "cc30c58179ec3e87c14c01d5c1f3434f" 324 "1d87" 325 ) 326 okm = hkdf.extract_and_expand(salt, ikm, info, 82) 327 assert okm == expected_okm 328 329 def test_rfc5869_case3(self): 330 """RFC 5869 Test Case 3 — zero-length salt and info.""" 331 from i2p_crypto.hkdf import HKDF 332 hkdf = HKDF() 333 ikm = bytes.fromhex("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b") 334 salt = b"" 335 info = b"" 336 expected_okm = bytes.fromhex( 337 "8da4e775a563c18f715f802a063c5a31" 338 "b8a11f5c5ee1879ec3454e5f3c738d2d" 339 "9d201395faa4b61a96c8" 340 ) 341 okm = hkdf.extract_and_expand(salt, ikm, info, 42) 342 assert okm == expected_okm 343 344 def test_i2p_style_one_output(self): 345 """Test I2P-style calculate() with one 32-byte output.""" 346 from i2p_crypto.hkdf import HKDF 347 hkdf = HKDF() 348 key = bytes(range(32)) 349 data = b"test input keying material" 350 out = bytearray(32) 351 result = hkdf.calculate(key, data, out=out) 352 assert len(result) == 32 353 assert bytes(out) == result 354 # Verify against manual HMAC computation 355 prk = stdlib_hmac.new(key, data, hashlib.sha256).digest() 356 t1 = stdlib_hmac.new(prk, b"\x01", hashlib.sha256).digest() 357 assert result == t1 358 359 def test_i2p_style_one_output_with_info(self): 360 """Test I2P-style calculate() with info string.""" 361 from i2p_crypto.hkdf import HKDF 362 hkdf = HKDF() 363 key = bytes(range(32)) 364 data = b"ikm" 365 info = "some info" 366 out = bytearray(32) 367 result = hkdf.calculate(key, data, info=info, out=out) 368 # Verify against manual computation 369 prk = stdlib_hmac.new(key, data, hashlib.sha256).digest() 370 t1 = stdlib_hmac.new(prk, info.encode("ascii") + b"\x01", hashlib.sha256).digest() 371 assert result == t1 372 373 def test_i2p_style_two_outputs(self): 374 """Test I2P-style calculate() with two 32-byte outputs.""" 375 from i2p_crypto.hkdf import HKDF 376 hkdf = HKDF() 377 key = bytes(range(32)) 378 data = b"key material" 379 out = bytearray(32) 380 out2 = bytearray(48) 381 result = hkdf.calculate(key, data, out=out, out2=out2, off2=8) 382 # out should have T1 383 assert bytes(out) == result 384 # out2[8:40] should have T2 385 prk = stdlib_hmac.new(key, data, hashlib.sha256).digest() 386 t1 = stdlib_hmac.new(prk, b"\x01", hashlib.sha256).digest() 387 t2 = stdlib_hmac.new(prk, t1 + b"\x02", hashlib.sha256).digest() 388 assert result == t1 389 assert out2[8:40] == t2 390 assert out2[:8] == b"\x00" * 8 391 392 def test_i2p_style_two_outputs_with_info(self): 393 """Test I2P-style calculate() with two outputs and info.""" 394 from i2p_crypto.hkdf import HKDF 395 hkdf = HKDF() 396 key = bytes(range(32)) 397 data = b"ikm" 398 info = "ctx" 399 out = bytearray(32) 400 out2 = bytearray(32) 401 hkdf.calculate(key, data, info=info, out=out, out2=out2) 402 # Verify 403 prk = stdlib_hmac.new(key, data, hashlib.sha256).digest() 404 info_bytes = info.encode("ascii") 405 t1 = stdlib_hmac.new(prk, info_bytes + b"\x01", hashlib.sha256).digest() 406 t2 = stdlib_hmac.new(prk, t1 + info_bytes + b"\x02", hashlib.sha256).digest() 407 assert bytes(out) == t1 408 assert bytes(out2) == t2 409 410 def test_deterministic(self): 411 from i2p_crypto.hkdf import HKDF 412 hkdf = HKDF() 413 key = b"\xaa" * 32 414 data = b"same input" 415 r1 = hkdf.calculate(key, data) 416 r2 = hkdf.calculate(key, data) 417 assert r1 == r2