"""
Library for the AsconModel class.
This module contains the Python model used to verify the Ascon module.
Author: Timothée Charrier
"""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from cocotb.handle import HierarchyObject
from cocotb.logging import SimBaseLog
[docs]
def convert_output_to_str(
dut: HierarchyObject,
cipher: list[int],
) -> dict[str, str]:
"""
Convert the DUT output to a string.
Parameters
----------
dut : HierarchyObject
The device under test (DUT).
cipher : list[int]
The cipher output.
Returns
-------
dict
The DUT output as a string.
"""
# Get the DUT outputs as integers
o_tag: int = int(dut.o_tag.value)
o_state: list[int] = [int(x) for x in dut.o_state.value]
# Convert the DUT outputs to strings
return {
"o_tag": f"{o_tag:032X}",
"o_state": " ".join([f"{x:016X}" for x in o_state]),
"o_cipher": "".join([f"{x:016X}" for x in cipher]),
}
[docs]
class AsconModel:
"""
Model for the Ascon module.
This class defines the model used to verify the Ascon module.
"""
def __init__(
self,
dut: HierarchyObject,
*,
inputs: dict[str, int] | None = None,
plaintext: list[int] | None = None,
) -> None:
"""
Initialize the model.
Parameters
----------
dut : HierarchyObject
The device under test (DUT).
inputs : dict, optional
The initial input dictionary. Default is None.
plaintext : list, optional
The plaintext data. Default is None.
"""
self.dut: HierarchyObject = dut
self.log: SimBaseLog = dut._log
inputs = inputs or {}
# Input parameters with default values
self.i_data: int = inputs.get("i_data", 0)
self.i_key: int = inputs.get("i_key", 0)
self.i_nonce: int = inputs.get("i_nonce", 0)
# Output states
self.o_cipher: list[int] = [0] * 4
self.o_tag: list[int] = [0] * 2
# Create the input state
self.i_state: list[int] = [
self.i_data,
self.i_key >> 64,
self.i_key & 0xFFFFFFFFFFFFFFFF,
self.i_nonce >> 64,
self.i_nonce & 0xFFFFFFFFFFFFFFFF,
]
# Fixed data values
self.plaintext: list[int] = plaintext or [0] * 5
# Check if the plaintext list contains only zeros
if all(x == 0 for x in self.plaintext):
self.log.warning("The plaintext list contains only zeros.")
[docs]
@staticmethod
def rotate_right(value: int, num_bits: int) -> int:
"""
Rotate the bits of a 64-bit integer to the right.
Parameters
----------
value : int
The input value.
num_bits : int
The number of bits to rotate.
Returns
-------
int
The rotated value.
"""
return (value >> num_bits) | ((value & (1 << num_bits) - 1) << (64 - num_bits))
[docs]
def permutation(self, i_round: int = 0, *, is_first: bool = False) -> None:
"""
Compute the output state based on the current input state.
Parameters
----------
i_round : int, optional
The current round number. Default is 0.
is_first : bool, optional
True if it is the first permutation, False otherwise. Default is False.
"""
# Create a copy of the input state
state: list[int] = self.i_state.copy() if is_first else self.o_state.copy()
for r in range(12 - i_round, 12):
self.log.info("-- Permutation (r=%d) --", r)
# Perform the Round Constants addition
state[2] ^= 0xF0 - r * 0x10 + r * 0x1
self.log.info("Constant addition : %016X %016X %016X %016X %016X", *state)
# Perform the Substitution Layer
state = self._substitution_layer(state=state)
self.log.info("Substitution S-box : %016X %016X %016X %016X %016X", *state)
# Perform the Linear Diffusion Layer
state = self._linear_diffusion_layer(state=state)
self.log.info("Linear diffusion : %016X %016X %016X %016X %016X", *state)
# Set the output state
self.o_state = state
def _substitution_layer(self, state: list[int]) -> list[int]:
"""
Apply the substitution layer (S-box).
Parameters
----------
state : List[int]
The current state.
Returns
-------
List[int]
The updated state after the substitution layer.
"""
state[0] ^= state[4]
state[4] ^= state[3]
state[2] ^= state[1]
temp = [(state[i] ^ 0xFFFFFFFFFFFFFFFF) & state[(i + 1) % 5] for i in range(5)]
state = [state[i] ^ temp[(i + 1) % 5] for i in range(5)]
state[1] ^= state[0]
state[0] ^= state[4]
state[3] ^= state[2]
state[2] ^= 0xFFFFFFFFFFFFFFFF
return state
def _linear_diffusion_layer(self, state: list[int]) -> list[int]:
"""
Apply the linear diffusion layer.
Parameters
----------
state : List[int]
The current state.
Returns
-------
List[int]
The updated state after the linear diffusion layer.
"""
rotations: list[tuple[int, list[int]]] = [
(state[0], [19, 28]),
(state[1], [61, 39]),
(state[2], [1, 6]),
(state[3], [10, 17]),
(state[4], [7, 41]),
]
return [
s
^ self.rotate_right(
value=s,
num_bits=r1,
)
^ self.rotate_right(
value=s,
num_bits=r2,
)
for s, (r1, r2) in rotations
]
[docs]
def xor_data_begin(self, data: int) -> None:
"""
Perform XOR operation at the beginning with the data.
Parameters
----------
data : int
The data to XOR with the state.
"""
self.o_state[0] ^= data
self.log.info(
"State ^ Data : %016X %016X %016X %016X %016X",
*self.o_state,
)
[docs]
def xor_key_begin(self, key: int) -> None:
"""
Perform XOR operation at the beginning with the key.
Parameters
----------
key : int
The key to XOR with the state.
"""
self.o_state[1] ^= (key >> 64) & 0xFFFFFFFFFFFFFFFF
self.o_state[2] ^= key & 0xFFFFFFFFFFFFFFFF
[docs]
def xor_key_end(self) -> None:
"""Perform XOR operation at the end with the key."""
self.o_state[3] ^= self.i_key >> 64
self.o_state[4] ^= self.i_key & 0xFFFFFFFFFFFFFFFF
self.log.info(
"State ^ Key : %016X %016X %016X %016X %016X",
*self.o_state,
)
[docs]
def xor_lsb_end(self) -> None:
"""Perform XOR operation at the end with the least significant bit."""
self.o_state[4] ^= 0x0000000000000001
self.log.info(
"State ^ LSB : %016X %016X %016X %016X %016X",
*self.o_state,
)
def _process_finalization_phase(self) -> None:
"""Process the finalization phase."""
self.xor_data_begin(data=self.plaintext[4])
self._log_state(phase="Finalization ")
# Final Permutation
self.xor_key_begin(key=self.i_key)
self.o_cipher[3] = self.o_state[0]
self.permutation(i_round=12)
# Final XOR
self.xor_key_end()
self._log_state(phase="Final ")
# Extract the tag
self.o_tag[0] = self.o_state[3]
self.o_tag[1] = self.o_state[4]
[docs]
def ascon128(self, inputs: dict[str, int]) -> dict[str, str]:
"""
Compute the output state based on the current input state.
Parameters
----------
inputs : dict
The input dictionary containing the data, key, and nonce.
Returns
-------
dict
The output state, tag, and cipher.
"""
# Update the input state
self.update_inputs(inputs=inputs)
# Initialization Phase
self._log_initial_state()
self.permutation(i_round=12, is_first=True)
self.xor_key_end()
self._log_state(phase="Initialisation")
# Associated Data Phase
self.xor_data_begin(data=self.plaintext[0])
self.permutation(i_round=6)
self.xor_lsb_end()
# Plaintext Phase
for i in range(3):
self._log_state(phase=f"Data A{i + 1} ")
# Start with Xor Begin
self.xor_data_begin(data=self.plaintext[i + 1])
# Get the cipher
self.o_cipher[i] = self.o_state[0]
# Process the data phase
self.permutation(
i_round=6,
)
# Finalization Phase
self._process_finalization_phase()
# Return the output state, tag, and cipher
return self._get_output()
def _log_initial_state(self) -> None:
"""Log the initial state."""
self.log.info(
"%s\nInitial State : %016X %016X %016X %016X %016X\n%s",
"*" * 105,
*self.i_state,
"*" * 105,
)
def _log_state(self, phase: str) -> None:
"""Log the current state."""
self.log.info(
"%s\n%s : %016X %016X %016X %016X %016X\n%s",
"*" * 105,
phase,
*self.o_state,
"*" * 105,
)
def _get_output(self) -> dict[str, str]:
"""
Get the output state, tag, and cipher.
Returns
-------
dict
The output state, tag, and cipher.
"""
output_tag_str: str = f"{self.o_tag[0]:016X}{self.o_tag[1]:016X}"
output_cipher_str: str = (
f"{self.o_cipher[0]:016X}{self.o_cipher[1]:016X}"
f"{self.o_cipher[2]:016X}{self.o_cipher[3]:016X}"
)
output_state_str: str = (
f"{self.o_state[0]:016X} {self.o_state[1]:016X} {self.o_state[2]:016X} "
f"{self.o_state[3]:016X} {self.o_state[4]:016X}"
)
return {
"o_state": output_state_str,
"o_tag": output_tag_str,
"o_cipher": output_cipher_str,
}