270 lines
8.7 KiB
Python
270 lines
8.7 KiB
Python
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from pyaegis import aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4
|
|
|
|
from .util import random_split_bytes
|
|
|
|
# All AEGIS algorithm modules
|
|
ALL_ALGORITHMS = [aegis128l, aegis128x2, aegis128x4, aegis256, aegis256x2, aegis256x4]
|
|
|
|
|
|
def load_mac_test_vectors():
|
|
"""Load MAC test vectors from JSON file."""
|
|
test_vectors_path = (
|
|
Path(__file__).parent / "test-vectors" / "aegismac-test-vectors.json"
|
|
)
|
|
with open(test_vectors_path, "r") as f:
|
|
return json.load(f)
|
|
|
|
|
|
def get_algorithm_module(name):
|
|
"""Map test vector name to algorithm module."""
|
|
if "128L" in name:
|
|
return aegis128l
|
|
elif "128X2" in name:
|
|
return aegis128x2
|
|
elif "128X4" in name:
|
|
return aegis128x4
|
|
elif "256" in name and "256X2" not in name and "256X4" not in name:
|
|
return aegis256
|
|
elif "256X2" in name:
|
|
return aegis256x2
|
|
elif "256X4" in name:
|
|
return aegis256x4
|
|
else:
|
|
raise ValueError(f"Unknown algorithm in test vector name: {name}")
|
|
|
|
|
|
def get_test_id(vector):
|
|
"""Generate a test ID from the vector name."""
|
|
name = vector["name"]
|
|
# Extract algorithm name, e.g., "AEGISMAC-128L Test Vector" -> "128L"
|
|
if "AEGISMAC-" in name:
|
|
return name.split("AEGISMAC-")[1].split(" ")[0]
|
|
return name
|
|
|
|
|
|
@pytest.mark.parametrize("vector", load_mac_test_vectors(), ids=get_test_id)
|
|
def test_mac(vector):
|
|
"""Test MAC computation against test vectors."""
|
|
alg = get_algorithm_module(vector["name"])
|
|
|
|
key = bytes.fromhex(vector["key"])
|
|
nonce = bytes.fromhex(vector["nonce"])
|
|
data = bytes.fromhex(vector["data"])
|
|
|
|
# Test 128-bit MAC if present
|
|
if "tag128" in vector:
|
|
expected_tag128 = bytes.fromhex(vector["tag128"])
|
|
computed_tag128 = alg.mac(key, nonce, data, maclen=16)
|
|
assert computed_tag128 == expected_tag128, (
|
|
f"128-bit MAC mismatch for {vector['name']}"
|
|
)
|
|
|
|
# Test 256-bit MAC if present
|
|
if "tag256" in vector:
|
|
expected_tag256 = bytes.fromhex(vector["tag256"])
|
|
computed_tag256 = alg.mac(key, nonce, data, maclen=32)
|
|
assert computed_tag256 == expected_tag256, (
|
|
f"256-bit MAC mismatch for {vector['name']}"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("vector", load_mac_test_vectors(), ids=get_test_id)
|
|
def test_mac_class(vector):
|
|
"""Test MAC computation using the Mac class against test vectors."""
|
|
alg = get_algorithm_module(vector["name"])
|
|
|
|
key = bytes.fromhex(vector["key"])
|
|
nonce = bytes.fromhex(vector["nonce"])
|
|
data = bytes.fromhex(vector["data"])
|
|
|
|
# Test 128-bit MAC if present
|
|
if "tag128" in vector:
|
|
expected_tag128 = bytes.fromhex(vector["tag128"])
|
|
mac_state = alg.Mac(key, nonce, maclen=16)
|
|
for chunk in random_split_bytes(data):
|
|
mac_state.update(chunk)
|
|
computed_tag128 = mac_state.final()
|
|
assert computed_tag128 == expected_tag128, (
|
|
f"128-bit MAC mismatch for {vector['name']}"
|
|
)
|
|
|
|
# Test 256-bit MAC if present
|
|
if "tag256" in vector:
|
|
expected_tag256 = bytes.fromhex(vector["tag256"])
|
|
mac_state = alg.Mac(key, nonce, maclen=32)
|
|
for chunk in random_split_bytes(data):
|
|
mac_state.update(chunk)
|
|
computed_tag256 = mac_state.final()
|
|
assert computed_tag256 == expected_tag256, (
|
|
f"256-bit MAC mismatch for {vector['name']}"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("vector", load_mac_test_vectors(), ids=get_test_id)
|
|
def test_mac_class_with_digest(vector):
|
|
"""Test MAC computation using digest() and hexdigest() instead of final()."""
|
|
alg = get_algorithm_module(vector["name"])
|
|
|
|
key = bytes.fromhex(vector["key"])
|
|
nonce = bytes.fromhex(vector["nonce"])
|
|
data = bytes.fromhex(vector["data"])
|
|
|
|
# Test 128-bit MAC if present
|
|
if "tag128" in vector:
|
|
expected_tag128 = bytes.fromhex(vector["tag128"])
|
|
|
|
# Test with digest()
|
|
mac_state = alg.Mac(key, nonce, maclen=16)
|
|
for chunk in random_split_bytes(data):
|
|
mac_state.update(chunk)
|
|
computed_tag128 = mac_state.digest()
|
|
assert computed_tag128 == expected_tag128, (
|
|
f"128-bit MAC mismatch for {vector['name']} using digest()"
|
|
)
|
|
|
|
# Test that digest() can be called multiple times
|
|
computed_tag128_again = mac_state.digest()
|
|
assert computed_tag128 == computed_tag128_again, (
|
|
"digest() should return the same value on repeated calls"
|
|
)
|
|
|
|
# Test hexdigest()
|
|
mac_state2 = alg.Mac(key, nonce, maclen=16)
|
|
for chunk in random_split_bytes(data):
|
|
mac_state2.update(chunk)
|
|
hex_tag = mac_state2.hexdigest()
|
|
assert hex_tag == expected_tag128.hex(), (
|
|
f"128-bit MAC hexdigest mismatch for {vector['name']}"
|
|
)
|
|
|
|
# Test that hexdigest() can be called multiple times
|
|
hex_tag_again = mac_state2.hexdigest()
|
|
assert hex_tag == hex_tag_again, (
|
|
"hexdigest() should return the same value on repeated calls"
|
|
)
|
|
|
|
# Test 256-bit MAC if present
|
|
if "tag256" in vector:
|
|
expected_tag256 = bytes.fromhex(vector["tag256"])
|
|
|
|
# Test with digest()
|
|
mac_state = alg.Mac(key, nonce, maclen=32)
|
|
for chunk in random_split_bytes(data):
|
|
mac_state.update(chunk)
|
|
computed_tag256 = mac_state.digest()
|
|
assert computed_tag256 == expected_tag256, (
|
|
f"256-bit MAC mismatch for {vector['name']} using digest()"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("vector", load_mac_test_vectors(), ids=get_test_id)
|
|
def test_mac_clone(vector):
|
|
"""Test that cloning a Mac state works correctly."""
|
|
alg = get_algorithm_module(vector["name"])
|
|
|
|
key = bytes.fromhex(vector["key"])
|
|
nonce = bytes.fromhex(vector["nonce"])
|
|
data = bytes.fromhex(vector["data"])
|
|
|
|
# Test 128-bit MAC if present
|
|
if "tag128" in vector:
|
|
expected_tag128 = bytes.fromhex(vector["tag128"])
|
|
|
|
mac_state = alg.Mac(key, nonce, maclen=16)
|
|
for chunk in random_split_bytes(data):
|
|
mac_state.update(chunk)
|
|
|
|
# Clone the state
|
|
cloned_state = mac_state.clone()
|
|
|
|
# Both should produce the same tag
|
|
tag1 = mac_state.final()
|
|
tag2 = cloned_state.final()
|
|
|
|
assert tag1 == expected_tag128
|
|
assert tag2 == expected_tag128
|
|
assert tag1 == tag2
|
|
|
|
|
|
@pytest.mark.parametrize("vector", load_mac_test_vectors(), ids=get_test_id)
|
|
def test_mac_reset(vector):
|
|
"""Test that resetting a Mac state works correctly."""
|
|
alg = get_algorithm_module(vector["name"])
|
|
|
|
key = bytes.fromhex(vector["key"])
|
|
nonce = bytes.fromhex(vector["nonce"])
|
|
data = bytes.fromhex(vector["data"])
|
|
|
|
# Test 128-bit MAC if present
|
|
if "tag128" in vector:
|
|
expected_tag128 = bytes.fromhex(vector["tag128"])
|
|
|
|
mac_state = alg.Mac(key, nonce, maclen=16)
|
|
for chunk in random_split_bytes(data):
|
|
mac_state.update(chunk)
|
|
tag1 = mac_state.final()
|
|
assert tag1 == expected_tag128
|
|
|
|
# Reset and compute again
|
|
mac_state.reset()
|
|
for chunk in random_split_bytes(data):
|
|
mac_state.update(chunk)
|
|
tag2 = mac_state.final()
|
|
|
|
assert tag2 == expected_tag128
|
|
assert tag1 == tag2
|
|
|
|
|
|
@pytest.mark.parametrize("alg", ALL_ALGORITHMS, ids=lambda x: x.__name__.split(".")[-1])
|
|
def test_mac_reset_after_digest(alg):
|
|
"""Test that reset() clears the cached digest and allows reuse."""
|
|
key = alg.random_key()
|
|
nonce = alg.random_nonce()
|
|
|
|
mac_state = alg.Mac(key, nonce)
|
|
mac_state.update(b"Hello, world!")
|
|
tag1 = mac_state.digest()
|
|
|
|
# After digest(), update should fail
|
|
with pytest.raises(RuntimeError):
|
|
mac_state.update(b"More data")
|
|
|
|
# Reset should clear the cached digest
|
|
mac_state.reset()
|
|
|
|
# Now we should be able to update again
|
|
mac_state.update(b"Different data")
|
|
tag2 = mac_state.digest()
|
|
|
|
# Tags should be different since we used different data
|
|
assert tag1 != tag2
|
|
|
|
|
|
@pytest.mark.parametrize("alg", ALL_ALGORITHMS, ids=lambda x: x.__name__.split(".")[-1])
|
|
def test_mac_clone_preserves_cached_digest(alg):
|
|
"""Test that cloning preserves the cached digest state."""
|
|
key = alg.random_key()
|
|
nonce = alg.random_nonce()
|
|
|
|
mac_state = alg.Mac(key, nonce)
|
|
mac_state.update(b"Hello, world!")
|
|
tag1 = mac_state.digest()
|
|
|
|
# Clone after digest
|
|
cloned_state = mac_state.clone()
|
|
|
|
# Both should return the same cached tag
|
|
tag2 = cloned_state.digest()
|
|
assert tag1 == tag2
|
|
|
|
# Both should be unable to update
|
|
with pytest.raises(RuntimeError):
|
|
mac_state.update(b"More data")
|
|
with pytest.raises(RuntimeError):
|
|
cloned_state.update(b"More data")
|