| @@ -61,6 +61,22 @@ This function will create a repository _dilithium_ with all the complete squelet | |||
| - The file _projectclass.py_ where there is the class of the simulation which will enable you to generate traces of the project in Python scripts; | |||
| - A _Makefile_ ready to be used with a compiler _arm-none-eabi-gcc_. | |||
| Usually a leaking code runs challenges, one challenge giving a power trace. A challenge is the execution of a code with a specific set of data. This set of data is given in the input of the leakage simulation. For example, one can imagine that the leaking code is a symmetric encryption and one wants to study its power leakage according to the message and the secret key. Then, a challenge is the simulation of the leakage for a specific message and for a specific secret key. | |||
| So, the classical form of _project.c_ is the following one: | |||
| - It gets a number of challenges with ```readbyte```. | |||
| - Then, it loops for each challenge. | |||
| - For the challenge, load the specific set of data with ```readbyte```. | |||
| - Start the record of the power leakage (start a power trace) | |||
| - Realise the leaking operations with the loaded set of data | |||
| - Stop the record of the power leakage (end a power trace) | |||
| - Eventually output some data with ```printbyte``` | |||
| - Indicate to ELMO tool that the simulation is finished | |||
| The file _projectclass.py_ contains a subclass of ```SimulationProject```. It contains the description of the _project.c_ file for the ELMO tool, in order to correctly realise the simulation. The classmethod ```get_binary_path(cl)``` must return the relative path of the leakage binary (_project.c_ correctly compiled). The method ```set_input_for_each_challenge(self, input, challenge)``` must write a ```challenge``` in ```input``` using the function ```write```. Many methods of ```SimulationProject``` can be rewritten in the subclass if necessary. For example, in the case where your _project.c_ doesn't run challenges, you can rewrite the method ```set_input(self, input)```. | |||
| Important! Don't forget that _ELMO_ (and so _Python-ELMO_) needs a **compiled** version of _project.c_ (see the "Requirements" section for more details). The provided _Makefile_ is here to help you to compile. | |||
| ### List all the available simulation | |||
| ```python | |||
| @@ -96,7 +112,7 @@ traces = simulation.get_traces() | |||
| Sometimes, it is impossible to run the simulation thanks the simple method ```run``` of the project class. Indeed, sometimes the Python script is executed in the environment where _Python-ELMO_ cannot launch the ELMO tool. For example, it is the case where _Python-ELMO_ is used in SageMath on Windows. On Windows, SageMath installation relies on the Cygwin POSIX emulation system and it can be a problem. | |||
| To offer a solution, _Python-ELMO_ can be used thanks to a client-server link. The idea is you must launch the script ```run_server.py``` which will listen (by default) at port 5000 in localhost. | |||
| To offer a solution, _Python-ELMO_ can be used thanks to a client-server link. The idea is you must launch the following script which will listen (by default) at port 5000 in localhost. | |||
| ```bash | |||
| python -m elmo run-server | |||
| @@ -22,6 +22,7 @@ CURRENT = 1 | |||
| SUBSEQUENT = 2 | |||
| class ELMOEngine: | |||
| ### Initialization | |||
| def __init__(self): | |||
| self.load_coefficients() | |||
| self.reset_points() | |||
| @@ -32,6 +33,7 @@ class ELMOEngine: | |||
| return coeffs | |||
| def load_coefficients(self): | |||
| """ Load the coefficients for the ELMO model about power leakage """ | |||
| filename = os.path.join( | |||
| os.path.dirname(os.path.abspath(__file__)), | |||
| ELMO_TOOL_REPOSITORY, | |||
| @@ -70,13 +72,7 @@ class ELMOEngine: | |||
| self.BitFlip1_bitinteractions = self._extract_data(496) | |||
| self.BitFlip2_bitinteractions = self._extract_data(496) | |||
| def reset_points(self): | |||
| self.points = [] | |||
| self.power = None | |||
| def add_point(self, triplet, previous_ops, current_ops): | |||
| self.points.append((triplet, previous_ops, current_ops)) | |||
| ### Computation core | |||
| def _dot(self, a, b): | |||
| return np.sum(a * b, axis=0) | |||
| @@ -174,7 +170,20 @@ class ELMOEngine: | |||
| BitFlip1_bitinteractions_data, BitFlip2_bitinteractions_data]) | |||
| return power | |||
| ### To manage studied points | |||
| def reset_points(self): | |||
| """ Reset all the points previously added """ | |||
| self.points = [] | |||
| self.power = None | |||
| def add_point(self, triplet, previous_ops, current_ops): | |||
| """ Add a new point to analyse """ | |||
| self.points.append((triplet, previous_ops, current_ops)) | |||
| def run(self): | |||
| """ Compute the power leakage of all the points previously added | |||
| Store the results in 'self.power' | |||
| """ | |||
| nb_points = len(self.points) | |||
| triplet = np.array([p[0] for p in self.points]).T # shape = (3, nb_points) | |||
| previous_ops = np.array([p[1] for p in self.points]).T # shape = (2, nb_points) | |||
| @@ -183,6 +192,9 @@ class ELMOEngine: | |||
| self.power = self.calculate_point(triplet, previous_ops, current_ops) | |||
| def oneshot_point(self, triplet, previous_ops, current_ops): | |||
| """ Compute the power of a single point | |||
| defined by 'triplet', 'previous_ops', and 'current_ops' | |||
| """ | |||
| self.reset_points() | |||
| self.add_point(triplet, previous_ops, current_ops) | |||
| self.run() | |||
| @@ -18,6 +18,8 @@ from .utils import Color | |||
| class Executor(OneShotServiceThread): | |||
| def execute(self): | |||
| """ Answer a request of simulation """ | |||
| # Get simulation data | |||
| data = self.protocol.get_data() | |||
| self.protocol.please_assert(data) | |||
| @@ -85,6 +87,7 @@ class Executor(OneShotServiceThread): | |||
| def launch_executor(host=DEFAULT_HOST, port=DEFAULT_PORT, waiting_function=True): | |||
| """ Launch ELMO server on 'host' listening to the 'port' """ | |||
| from .server.servicethread import ListeningThread | |||
| def do_main_program(): | |||
| @@ -95,12 +98,15 @@ def launch_executor(host=DEFAULT_HOST, port=DEFAULT_PORT, waiting_function=True) | |||
| def program_cleanup(signum, frame): | |||
| thread.stop() | |||
| # Launch the listening server | |||
| thread = do_main_program() | |||
| # Give a way to stop the server | |||
| import signal | |||
| signal.signal(signal.SIGINT, program_cleanup) | |||
| signal.signal(signal.SIGTERM, program_cleanup) | |||
| # Do somthing during the execution of the server | |||
| if waiting_function is True: | |||
| import time | |||
| while thread.is_running(): | |||
| @@ -22,7 +22,7 @@ class SimulationProject: | |||
| ### Define the project | |||
| @classmethod | |||
| def get_project_directory(cl): | |||
| """ """ | |||
| """ Return the project directory of the simulation """ | |||
| if cl._project_directory: | |||
| return cl._project_directory | |||
| else: | |||
| @@ -30,35 +30,32 @@ class SimulationProject: | |||
| @classmethod | |||
| def set_project_directory(cl, project_directory): | |||
| """ Set the project directory of the simulation """ | |||
| cl._project_directory = project_directory | |||
| @classmethod | |||
| def get_project_label(cl): | |||
| return cl.get_project_directory() | |||
| @classmethod | |||
| def get_make_directory(cl): | |||
| return '' | |||
| @classmethod | |||
| def get_binary_path(cl): | |||
| """ Return the path of the leaking binary """ | |||
| raise NotImplementedError() | |||
| @classmethod | |||
| def get_parameters_names(cl): | |||
| return set() | |||
| def get_challenge_format(self): | |||
| """ Return the format of one challenge | |||
| Used by 'set_input_for_each_challenge' if not rewritten | |||
| """ | |||
| raise NotImplementedError() | |||
| ### Tools to realize the simulation of the project | |||
| def __init__(self, challenges=None): | |||
| """ Initialize a simulation project | |||
| :challenge: The list of challenge for the simulation | |||
| """ | |||
| self.elmo_folder = pjoin(MODULE_PATH, ELMO_TOOL_REPOSITORY) | |||
| self.challenges = challenges | |||
| self.reset() | |||
| def reset(self): | |||
| """ Reset the last simulation """ | |||
| self.is_executed = False | |||
| self.has_been_online = False | |||
| @@ -67,26 +64,50 @@ class SimulationProject: | |||
| self._complete_results = None | |||
| self._complete_printed_data = None | |||
| def get_number_of_challenges(self): | |||
| """ Return the number of challenge """ | |||
| return len(self.challenges) if self.challenges else 0 | |||
| def get_test_challenges(self): | |||
| """ Return a fixed list of challenges for test """ | |||
| raise NotImplementedError() | |||
| def get_random_challenges(self, nb_challenges): | |||
| """ Return a list of random challenges | |||
| :nb_challenges: Length of the list | |||
| """ | |||
| raise NotImplementedError() | |||
| def set_challenges(self, challenges): | |||
| """ Reset the simulation and set the challenges of the next simulation | |||
| :challenges: The list of the challenges | |||
| """ | |||
| self.reset() | |||
| self.challenges = challenges | |||
| def get_input_filename(self): | |||
| """ Return (string) the path of the input file | |||
| of the local installation of ELMO tool | |||
| """ | |||
| return pjoin(self.elmo_folder, ELMO_INPUT_FILE_NAME) | |||
| def get_printed_data_filename(self): | |||
| """ Return the path (string) of the file containing the printed data | |||
| of the local installation of ELMO tool | |||
| """ | |||
| return pjoin(self.elmo_folder, 'output', 'printdata.txt') | |||
| def get_asmtrace_filename(self): | |||
| """ Return the path (string) of the file containing the ASM trace | |||
| of the local installation of ELMO tool | |||
| """ | |||
| return pjoin(self.elmo_folder, 'output', 'asmoutput', 'asmtrace00001.txt') | |||
| def set_input_for_each_challenge(self, input, challenge): | |||
| """ Set the input for one challenge for a simulation with ELMO tool | |||
| :input: Descriptor of the input of the ELMO tool (write only) | |||
| :challenge: A challenge for the simulation | |||
| """ | |||
| format = self.get_challenge_format() | |||
| def aux(sizes, data): | |||
| @@ -101,17 +122,30 @@ class SimulationProject: | |||
| aux(format[num_part], challenge[num_part]) | |||
| def set_input(self, input): | |||
| """ Set the input for a simulation with ELMO tool | |||
| First, it writes the number of challenges. | |||
| Then, it writes each challenge one by one thanks to the method 'set_input_for_each_challenge' | |||
| :input: Descriptor of the input of the ELMO tool (write only) | |||
| """ | |||
| if self.challenges: | |||
| assert len(self.challenges) < (1 << self._nb_bits_for_nb_challenges), \ | |||
| nb_challenges = self.get_number_of_challenges() | |||
| assert nb_challenges < (1 << self._nb_bits_for_nb_challenges), \ | |||
| 'The number of challenges must be strictly lower than {}. Currently, there are {} challenges.'.format( | |||
| 1 << self._nb_bits_for_nb_challenges, | |||
| len(self.challenges), | |||
| nb_challenges, | |||
| ) | |||
| write(input, len(self.challenges), nb_bits=self._nb_bits_for_nb_challenges) | |||
| write(input, nb_challenges, nb_bits=self._nb_bits_for_nb_challenges) | |||
| for challenge in self.challenges: | |||
| self.set_input_for_each_challenge(input, challenge) | |||
| def run(self): | |||
| """ Run the simulation thanks the local installation of ELMO tool. | |||
| Using the leaking binary defined thanks to the method 'get_binary_path', | |||
| it will run the ELMO tool to output the leaked power traces. | |||
| The results of the simulation are available via the methods: | |||
| 'get_results', 'get_traces', 'get_asmtrace' and 'get_printed_data' | |||
| Return the raw output of the compiled ELMO tool. | |||
| """ | |||
| self.reset() | |||
| with open(self.get_input_filename(), 'w') as _input: | |||
| self.set_input(_input) | |||
| @@ -125,6 +159,17 @@ class SimulationProject: | |||
| return res | |||
| def run_online(self, host=DEFAULT_HOST, port=DEFAULT_PORT): | |||
| """ Run the simulation thanks to an ELMO server. | |||
| An ELMO server can be launched thanks to the command | |||
| >>> python -m elmo run-server 'host' 'port' | |||
| Using the leaking binary defined thanks to the method 'get_binary_path', | |||
| it will run the ELMO tool to output the leaked power traces. | |||
| The results of the simulation are available via the methods: | |||
| 'get_results', 'get_traces', 'get_asmtrace' and 'get_printed_data' | |||
| Return the raw output of the compiled ELMO tool. | |||
| :host: The host of the ELMO server | |||
| :post! The port where the ELMO server is currently listening | |||
| """ | |||
| from .server.protocol import SocketTool | |||
| import socket | |||
| @@ -172,14 +217,15 @@ class SimulationProject: | |||
| } | |||
| ### Manipulate the results | |||
| def get_number_of_challenges(self): | |||
| return len(self.challenges) | |||
| def get_number_of_traces(self): | |||
| """ Get the number of traces of the last simulation """ | |||
| assert self.is_executed | |||
| return self._nb_traces | |||
| def get_results_filenames(self): | |||
| """ Get the filenames of the results of the last simulation | |||
| Return a list of filenames (strings), each file containing a power trace | |||
| """ | |||
| assert self.is_executed | |||
| assert not self.has_been_online | |||
| nb_traces = self.get_number_of_traces() | |||
| @@ -194,7 +240,8 @@ class SimulationProject: | |||
| return filenames | |||
| def get_results(self): | |||
| """ | |||
| """ Get the raw outputs of the last simulation | |||
| Return a list of power traces (represented by a list of floats) | |||
| Warning: The output list is the same object stored in the instance. | |||
| If you change this object, it will change in the instance too, and the | |||
| next call to 'get_results' will return the changed object. | |||
| @@ -212,7 +259,12 @@ class SimulationProject: | |||
| return self._complete_results | |||
| def get_traces(self, indexes=None): | |||
| """ | |||
| """ Get the power trace of the last simulation | |||
| Return a 2-dimensional numpy array of floats. | |||
| 1st dimension: number of the trace | |||
| 2nd dimension: power point of the trace | |||
| :indexes: if not None, return only the power points contained in :indexes: | |||
| Must be a list of indexes of power points. | |||
| """ | |||
| assert self.is_executed | |||
| results = self.get_results() | |||
| @@ -229,7 +281,7 @@ class SimulationProject: | |||
| else: | |||
| traces = np.zeros((nb_traces, len(indexes))) | |||
| for i in range(nb_traces): | |||
| traces[i,:] = results[i][indexes] | |||
| traces[i,:] = np.array(results[i])[indexes] | |||
| return traces | |||
| ### Manipulate the ASM trace | |||
| @@ -244,7 +296,7 @@ class SimulationProject: | |||
| if self._complete_asmtrace is None: | |||
| with open(self.get_asmtrace_filename(), 'r') as _file: | |||
| self._complete_asmtrace = _file.read() | |||
| if type(self._complete_asmtrace): | |||
| if type(self._complete_asmtrace) is not list: | |||
| self._complete_asmtrace = self._complete_asmtrace.split('\n') | |||
| return self._complete_asmtrace | |||
| @@ -16,6 +16,7 @@ int main(void) { | |||
| read2bytes(&nb_challenges); | |||
| for(num_challenge=0; num_challenge<nb_challenges; num_challenge++) { | |||
| // Set variables for the current challenge | |||
| starttrigger(); // To start a new trace | |||
| // Do the leaking operations here... | |||
| @@ -2,6 +2,7 @@ import numpy as np | |||
| ### Binary operations | |||
| def hweight(n): | |||
| """ Return the Hamming weight of 'n' """ | |||
| c = 0 | |||
| while n>0: | |||
| c += (n & 1) | |||
| @@ -9,9 +10,13 @@ def hweight(n): | |||
| return c | |||
| def hdistance(x,y): | |||
| """ Return the Hamming distance between 'x' and 'y' """ | |||
| return hweight(x^y) | |||
| def binary_writing(n, nb_bits=32, with_hamming=False): | |||
| """ Return the binary writing 'w' of 'n' with 'nb_bits' | |||
| If with_hamming is True, return a couple (w, h) with 'h' the Hamming weight of 'n' | |||
| """ | |||
| n = np.array(n) | |||
| w, h = np.zeros((nb_bits, len(n))), np.zeros((len(n))) | |||
| @@ -26,6 +31,7 @@ def binary_writing(n, nb_bits=32, with_hamming=False): | |||
| ### Conversion | |||
| def to_hex(v, nb_bits=16): | |||
| """ Convert the value 'v' into a hexadecimal string (without the prefix 0x)""" | |||
| try: | |||
| v_hex = v.hex() | |||
| except AttributeError: | |||
| @@ -33,9 +39,15 @@ def to_hex(v, nb_bits=16): | |||
| return '0'*(nb_bits//4-len(v_hex)) + v_hex | |||
| def split_octet(hexstr): | |||
| """ Split a hexadecimal string (without the prefix 0x) | |||
| into a list of bytes described with a 2-length hexadecimal string | |||
| """ | |||
| return [hexstr[i:i+2] for i in range(0, len(hexstr), 2)] | |||
| def to_signed_hex(v, nb_bits=16): | |||
| """ Convert the signed value 'v' | |||
| into a list of bytes described with a 2-length hexadecimal string | |||
| """ | |||
| try: | |||
| return split_octet(to_hex(v & ((1<<nb_bits)-1), nb_bits=nb_bits)) | |||
| except TypeError as err: | |||
| @@ -43,16 +55,23 @@ def to_signed_hex(v, nb_bits=16): | |||
| ### Write function | |||
| def write(_input, uint, nb_bits=16): | |||
| """ Write in '_input' the value 'uint' by writing each byte | |||
| with hexadecimal format in a new line | |||
| """ | |||
| uint = to_signed_hex(uint, nb_bits=nb_bits) | |||
| for i in range(nb_bits//8): | |||
| _input.write(uint[i]+'\n') | |||
| def write_list(_input, uint_list, nb_bits=16): | |||
| """ Write in '_input' the list of values 'uint_list' by writing each byte | |||
| with hexadecimal format in a new line | |||
| """ | |||
| for uint in uint_list: | |||
| write(_input, uint, nb_bits=nb_bits) | |||
| ### Print Utils | |||
| class Color: | |||
| """ Color codes to print colored text in stdout """ | |||
| HEADER = '\033[95m' | |||
| OKBLUE = '\033[94m' | |||
| OKCYAN = '\033[96m' | |||
| @@ -76,6 +76,18 @@ assert not res['error'] | |||
| assert res['nb_traces'] == 10 | |||
| assert res['nb_instructions'] | |||
| # Test the methods for analysis | |||
| multiplication_indexes = simulation.get_indexes_of(lambda instr: 'mul' in instr) | |||
| asmtrace = simulation.get_asmtrace() | |||
| for index, instr in enumerate(asmtrace): | |||
| assert ('mul' in instr) == (index in multiplication_indexes) | |||
| traces = simulation.get_traces() | |||
| traces = simulation.get_traces(multiplication_indexes) | |||
| assert traces.shape == (10, len(multiplication_indexes)) | |||
| printed_data = simulation.get_printed_data() | |||
| print_success(' - Test 3 "Use A Real Simulation": Success!') | |||
| ######################################################### | |||