@@ -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!') | |||
######################################################### |