Example: side-channel analysis#

This example illustrates how to use Phileas to take measurements for a typical side-channel analysis experiment.

Imports and initial setup

 1from pprint import pprint
 2import logging
 3import sys
 4
 5import matplotlib.pyplot as plt
 6import xarray as xr
 7import numpy as np
 8
 9np.random.seed(int.from_bytes("sca.md".encode("utf-8")) % (1 << 32))
10
11from phileas.factory import ExperimentFactory
12from phileas.iteration import generate_seeds
13import phileas.mock_instruments
14
15logging.basicConfig(level=logging.INFO, stream=sys.stdout)
phileas code contains assertions that reduce its performances. Consider running python with the -O or -OO flags, or setting the PYTHONOPTIMIZE environment variable in production code.

Experimental setup#

A simple current side-channel dataset is acquired, which requires the following experimental setup:

  • the device under test consists of an electronic component with an embedded implementation of the PRESENT cypher.

  • A control computer can communicate with the tested device through a serial link. To establish the connection, it requires a baudrate and a peripheral path.

  • Its current consumption is monitored by plugging a current probe to its power supply.

  • The probe output is sampled by an oscilloscope, which is synchronized with the target algorithm using an external trigger.

Bench description#

The first step to prepare the data acquisition is to describe the instruments available on the bench, and how to establish a connection with them. This is done by the bench configuration file:

 1bench = """
 2simulated_present_dut:
 3  loader: phileas-mock_present-phileas
 4  device: /dev/ttyUSB1
 5  baudrate: 115200
 6  probe: simulated_current_probe
 7
 8simulated_current_probe:
 9  loader: phileas-current_probe-phileas
10
11simulated_oscilloscope:
12  loader: phileas-mock_oscilloscope-phileas
13  probe: generic
14  probe-name: simulated_current_probe
15  width: 32
16"""

Here, we declare that our bench is composed of three bench instruments: the embedded PRESENT implementation simulated_present_dut is connected to a current probe simulated_current_probe. An oscilloscope simulated_oscilloscope records the output of the probe.

Attention

Here, we store the content of the bench configuration file in the bench variable, as it is easier to work with it from a notebook. However, you should usually store this in a YAML file, say bench.yaml. Indeed, it is then easier to share, maintain and modify, and this way it does not pollute the acquisition script.

Experiment description#

We can then describe what instruments are required and how to configure them, which is done by the experiment configuration file. If we want to perform a single encryption, with a unique key and plaintext, we could do it with:

 1experiment = """
 2dut:
 3  interface: present-encryption
 4  key: 12
 5  plaintext: 1
 6
 7oscilloscope:
 8  interface: oscilloscope
 9  amplitude: 10
10"""

This configuration requires two experiment instruments. dut must be a device which supports the present-encryption interface. Its configuration consists of the specified key and plaintext. Similarly, oscilloscope must be an oscilloscope device.

Attention

Here, we store the content of the experiment configuration file in the experiment variable, as it is easier to work with it from a notebook. However, you should usually store this in a YAML file, say experiment.yaml. Indeed, it is then easier to share, maintain and modify, and this way it does not pollute the acquisition script.

Instruments instantiation#

We can finally let Phileas handle the instantiation of the instruments, which is done by ExperimentFactory:

1factory = ExperimentFactory(bench, experiment)
2factory.initiate_connections()
3print("Here are the experiment instruments:")
4pprint(factory.experiment_instruments)
5
6oscilloscope = factory.experiment_instruments["oscilloscope"]
7dut = factory.experiment_instruments["dut"]
INFO:phileas:Bench configuration supplied as a data tree.
INFO:phileas:Bench instrument simulated_present_dut assigned to loader <class 'phileas.mock_instruments.SimulatedPRESENTImplementationLoader'>.
INFO:phileas:Bench instrument simulated_current_probe assigned to loader <class 'phileas.mock_instruments.CurrentProbeLoader'>.
INFO:phileas:Bench instrument simulated_oscilloscope assigned to loader <class 'phileas.mock_instruments.OscilloscopeLoader'>.
INFO:phileas:Experiment configuration supplied as an iteration tree.
INFO:phileas:Matching experiment instrument dut with bench instrument simulated_present_dut.
INFO:phileas:Matching experiment instrument oscilloscope with bench instrument simulated_oscilloscope.
INFO:phileas:Initiating connections to the used instruments.
INFO:phileas.simulated_present_dut:Initiating connection.
INFO:phileas.phileas.mock_instruments:Connected to the PRESENT DUT on /dev/ttyUSB1 with baudrate 115200.
INFO:phileas.simulated_current_probe:Initiating connection.
INFO:phileas.simulated_oscilloscope:Initiating connection.
INFO:phileas.phileas.mock_instruments:[Oscilloscope] Connection initiated.
INFO:phileas:Connections initiation is done.
Here are the experiment instruments:
{'dut': SimulatedPRESENTImplementation(serial_device='/dev/ttyUSB1',
                                       baudrate=115200,
                                       probe=CurrentProbe(last_measurement=None,
                                                          noise_level=1.0,
                                                          gain=1.0),
                                       _key=None,
                                       rounds=32,
                                       snr=1,
                                       points_per_round=20),
 'oscilloscope': Oscilloscope(probe=CurrentProbe(last_measurement=None,
                                                 noise_level=1.0,
                                                 gain=1.0),
                              id='mock-oscilloscope-driver:1',
                              width=32)}

The different log records let us follow what is going on under the hood.

  1. First, loaders are assigned to the different bench instruments. A loader is a sub-class of Loader which handles the instantiation and configuration of an instrument. It is obtained by matching the loader field of the bench instrument entries to the name attribute of the registered loader classes. For more information about loaders, see this page.

  2. Then, the instruments required in the experiment configuration file are matched to the available bench instruments. The interface required by each experiment instrument is compared to the interfaces field of the loaders previously matched with the bench instruments. If only a single bench instrument correspond to the required interface, it is assigned to the experiment instrument. Here, dut (from experiment) is matched with simulated_aes_dut (from bench).

  3. Finally, initiate_connections() instantiates all the required drivers, using the connection information available in the bench configuration file. You can access these instruments in experiment_instruments.

Hint

Phileas exclusively uses the phileas logging handler. However, in Python, logs are by default not displayed. The easiest way to turn this on is to use logging.basicConfig().

import logging

logging.basicConfig(level=logging.INFO)

We can now configure the instruments, and get a side-channel trace:

1factory.configure_experiment()
2cyphertext = dut.encrypt(factory.experiment_config["dut"]["plaintext"].value)
3print(f"Ciphertext: {cyphertext:016x}")
4trace = oscilloscope.get_measurement()
5
6plt.plot(trace)
7plt.show()
INFO:phileas.phileas.mock_instruments:PRESENT DUT key set to 12.
INFO:phileas.simulated_oscilloscope:Amplitude set to 10.
Ciphertext: f3b218fead7fc1ab
../_images/13ef1946d2e07f71d3747b0a4347d6d9754b86949fac27f65ee8a7780e78b306.png

See how the experiment configuration has been parsed and is stored in experiment_config. We can directly access it in order to retrieve the plaintext. However, the plaintext is not directly an int, but it encapsulates one in its value field:

1factory.experiment_config["dut"]["plaintext"]
IterationLiteral(value=1)

Representing multiple configurations#

It is now easy to transform the simple experiment configuration to one which represents a whole set of parameters. Different custom YAML types, identified by a ! prefix, can be used. For example, to use different plaintext values, we could specify:

 1experiment = """
 2dut:
 3  interface: present-encryption
 4  key: 12
 5  plaintext: !range
 6    start: 1
 7    end: 3
 8    resolution: 1
 9
10oscilloscope:
11  interface: oscilloscope
12  amplitude: 10
13"""
14
15factory = ExperimentFactory(bench, experiment)
16factory.initiate_connections()
17pprint(factory.experiment_instruments)
18
19oscilloscope = factory.experiment_instruments["oscilloscope"]
20dut = factory.experiment_instruments["dut"]
INFO:phileas:Bench configuration supplied as a data tree.
INFO:phileas:Bench instrument simulated_present_dut assigned to loader <class 'phileas.mock_instruments.SimulatedPRESENTImplementationLoader'>.
INFO:phileas:Bench instrument simulated_current_probe assigned to loader <class 'phileas.mock_instruments.CurrentProbeLoader'>.
INFO:phileas:Bench instrument simulated_oscilloscope assigned to loader <class 'phileas.mock_instruments.OscilloscopeLoader'>.
INFO:phileas:Experiment configuration supplied as an iteration tree.
INFO:phileas:Matching experiment instrument dut with bench instrument simulated_present_dut.
INFO:phileas:Matching experiment instrument oscilloscope with bench instrument simulated_oscilloscope.
INFO:phileas:Initiating connections to the used instruments.
INFO:phileas.simulated_present_dut:Initiating connection.
INFO:phileas.phileas.mock_instruments:Connected to the PRESENT DUT on /dev/ttyUSB1 with baudrate 115200.
INFO:phileas.simulated_current_probe:Initiating connection.
INFO:phileas.simulated_oscilloscope:Initiating connection.
INFO:phileas.phileas.mock_instruments:[Oscilloscope] Connection initiated.
INFO:phileas:Connections initiation is done.
{'dut': SimulatedPRESENTImplementation(serial_device='/dev/ttyUSB1',
                                       baudrate=115200,
                                       probe=CurrentProbe(last_measurement=None,
                                                          noise_level=1.0,
                                                          gain=1.0),
                                       _key=None,
                                       rounds=32,
                                       snr=1,
                                       points_per_round=20),
 'oscilloscope': Oscilloscope(probe=CurrentProbe(last_measurement=None,
                                                 noise_level=1.0,
                                                 gain=1.0),
                              id='mock-oscilloscope-driver:1',
                              width=32)}

The experiment configuration still requires the same set of instruments. However, multiple instruments configurations are represented by the experiment configuration. They are available in the experiment_config attribute of the experiment factory, which is an iterable:

1for config in factory.experiment_config:
2  print(config)
3  factory.configure_experiment(config)
{'dut': {'key': 12, 'plaintext': 1}, 'oscilloscope': {'amplitude': 10}}
INFO:phileas.phileas.mock_instruments:PRESENT DUT key set to 12.
INFO:phileas.simulated_oscilloscope:Amplitude set to 10.
{'dut': {'key': 12, 'plaintext': 2}, 'oscilloscope': {'amplitude': 10}}
INFO:phileas.phileas.mock_instruments:PRESENT DUT key set to 12.
INFO:phileas.simulated_oscilloscope:Amplitude set to 10.
{'dut': {'key': 12, 'plaintext': 3}, 'oscilloscope': {'amplitude': 10}}
INFO:phileas.phileas.mock_instruments:PRESENT DUT key set to 12.
INFO:phileas.simulated_oscilloscope:Amplitude set to 10.

However, the factory instantiated some new instruments, which was not required here. We can bypass this by directly parsing the experiment configuration:

1configurations = phileas.parsing.load_iteration_tree_from_yaml_file(experiment)
2pprint(configurations)
CartesianProduct(children={'dut': CartesianProduct(children={'interface': IterationLiteral(value='present-encryption'),
                                                             'key': IterationLiteral(value=12),
                                                             'plaintext': IntegerRange(start=1,
                                                                                       end=3,
                                                                                       default_value=_NoDefault(),
                                                                                       step=1)},
                                                   order=None,
                                                   lazy=False,
                                                   snake=False),
                           'oscilloscope': CartesianProduct(children={'amplitude': IterationLiteral(value=10),
                                                                      'interface': IterationLiteral(value='oscilloscope')},
                                                            order=None,
                                                            lazy=False,
                                                            snake=False)},
                 order=None,
                 lazy=False,
                 snake=False)

Measurements acquisition and storage#

Let us now see how to manage the measurements, and store them conveniently. Phileas provides some interoperability functions to use Pandas and Xarray containers. Xarray is often more convenient to use, as the parameters spaces are often simple cartesian products. iteration_tree_to_xarray_parameters() can be used to prepare the datasets. It reads the experiment configurations, and produces the arguments required to initialize axarray.DataArray or xarray.Dataset:

1coords, dims_name, dims_shape = phileas.iteration.utility.iteration_tree_to_xarray_parameters(configurations)
2traces = xr.DataArray(coords=coords, dims=dims_name)
3traces
<xarray.DataArray (dut.plaintext: 3)> Size: 24B
array([nan, nan, nan])
Coordinates:
    dut.interface           <U18 72B 'present-encryption'
    dut.key                 int64 8B 12
  * dut.plaintext           (dut.plaintext) int64 24B 1 2 3
    oscilloscope.interface  <U12 48B 'oscilloscope'
    oscilloscope.amplitude  int64 8B 10

However, the oscilloscope outputs 1D arrays, so we should add a dimension, say oscilloscope.sample, to traces.

1coords["oscilloscope.sample"] = list(range(trace.shape[0]))
2dims_name.append("oscilloscope.sample")
3dims_shape.append(trace.shape[0])
4traces = xr.DataArray(coords=coords, dims=dims_name)
5traces
<xarray.DataArray (dut.plaintext: 3, oscilloscope.sample: 1248)> Size: 30kB
array([[nan, nan, nan, ..., nan, nan, nan],
       [nan, nan, nan, ..., nan, nan, nan],
       [nan, nan, nan, ..., nan, nan, nan]], shape=(3, 1248))
Coordinates:
    dut.interface           <U18 72B 'present-encryption'
    dut.key                 int64 8B 12
  * dut.plaintext           (dut.plaintext) int64 24B 1 2 3
    oscilloscope.interface  <U12 48B 'oscilloscope'
    oscilloscope.amplitude  int64 8B 10
  * oscilloscope.sample     (oscilloscope.sample) int64 10kB 0 1 2 ... 1246 1247

We can now populate traces with measurements. Converting a configuration to a dictionary that can be used by Xarray for indexing can be done by data_tree_to_xarray_index():

1for config in configurations:
2    index = phileas.iteration.utility.data_tree_to_xarray_index(config, dims_name)
3    factory.configure_experiment(config)
4    dut.encrypt(config["dut"]["plaintext"])
5    trace = oscilloscope.get_measurement()
6    traces.loc[index] = trace
INFO:phileas.phileas.mock_instruments:PRESENT DUT key set to 12.
INFO:phileas.simulated_oscilloscope:Amplitude set to 10.
INFO:phileas.phileas.mock_instruments:PRESENT DUT key set to 12.
INFO:phileas.simulated_oscilloscope:Amplitude set to 10.
INFO:phileas.phileas.mock_instruments:PRESENT DUT key set to 12.
INFO:phileas.simulated_oscilloscope:Amplitude set to 10.
1traces.plot(x="oscilloscope.sample", col="dut.plaintext")
2plt.show()
../_images/4836c17c370e0c519cfdec81ad6f4f4747a75c3eb80855c0abe75c5034498c24.png

We can see that the traces are quite noisy. PRESENT consists of 32 similar rounds, so we could expect to notice 32 similar patterns. Let us average the measurements, to get rid of some noise and hopefully expose these patterns.

Traces de-noising#

Before we do it, we should disable outputting the logs to the standard output. Indeed, now that we have checked that the experiment runs properly, we don’t need to verify them anymore. Since averaging requires a lot more configurations, they would totally clutter the output. We can disable those which are less severe than warnings with

1logger = logging.getLogger("phileas")
2logger.setLevel(logging.WARNING)

Note

Please note that in an actual experiment, depending on the surrounding workflow, it might be better to redirect the logs to an external file instead.

Let us now carry out 1000 measurements for each of the DUT configurations. We can do this by adding a new parameter, say iteration, to the top-level experiment configuration:

 1experiment = """
 2dut:
 3  interface: present-encryption
 4  key: 12
 5  plaintext: !sequence [12345, 67890]
 6
 7oscilloscope:
 8  interface: oscilloscope
 9  amplitude: 10
10
11iteration: !range
12  start: 1
13  end: 1000
14  resolution: 1
15"""
16
17configurations = phileas.parsing.load_iteration_tree_from_yaml_file(experiment)
18
19coords, dims_name, dims_shape = phileas.iteration.utility.iteration_tree_to_xarray_parameters(configurations)
20coords["oscilloscope.sample"] = list(range(trace.shape[0]))
21dims_name.append("oscilloscope.sample")
22dims_shape.append(trace.shape[0])
23traces = xr.DataArray(coords=coords, dims=dims_name)
24
25for config in configurations:
26    index = phileas.iteration.utility.data_tree_to_xarray_index(config, dims_name)
27    factory.configure_experiment(config)
28    dut.encrypt(config["dut"]["plaintext"])
29    trace = oscilloscope.get_measurement()
30    traces.loc[index] = trace
31
32traces
<xarray.DataArray (dut.plaintext: 2, iteration: 1000, oscilloscope.sample: 1248)> Size: 20MB
array([[[ 1.12981407, -1.31004511,  1.13646575, ...,  1.04252183,
          1.11672848, -0.5268217 ],
        [ 0.83331125, -0.32536024, -0.98562334, ..., -1.95401334,
          0.19469743,  0.78950437],
        [-2.67044829, -0.81494182,  0.80119415, ...,  0.44398108,
          2.11590857, -0.58147653],
        ...,
        [ 1.23910048, -1.38197299, -1.41166615, ..., -2.73755745,
         -0.47020445,  0.39726221],
        [-0.13345535, -0.65010395, -1.59522895, ...,  0.61728624,
          1.12430002,  0.6720478 ],
        [-1.15392488, -1.13335883,  3.27754404, ...,  1.44999202,
          0.18486143, -1.16912075]],

       [[ 3.25501995,  0.07540935, -2.19303652, ...,  0.51917847,
          0.92892439,  0.81673783],
        [ 0.30093926,  0.31618611, -0.30205585, ..., -1.01324813,
          1.47327104, -1.42762288],
        [-0.91949338,  1.84772908, -1.08585279, ...,  1.52732599,
         -0.47169627,  0.45248895],
        ...,
        [-0.85037223, -0.77776855,  2.56067357, ..., -0.50108016,
          1.41523839,  0.74475309],
        [ 1.32107741, -2.07097402, -1.39934659, ..., -2.21757147,
         -0.68076575,  1.06950877],
        [-0.55380748,  0.12073824, -0.60336877, ..., -0.17019005,
         -0.15281742,  0.23551035]]], shape=(2, 1000, 1248))
Coordinates:
    dut.interface           <U18 72B 'present-encryption'
    dut.key                 int64 8B 12
  * dut.plaintext           (dut.plaintext) int64 16B 12345 67890
    oscilloscope.interface  <U12 48B 'oscilloscope'
    oscilloscope.amplitude  int64 8B 10
  * iteration               (iteration) int64 8kB 1 2 3 4 5 ... 997 998 999 1000
  * oscilloscope.sample     (oscilloscope.sample) int64 10kB 0 1 2 ... 1246 1247

See how a new coordinate, iteration, has been automatically added to the Xarray container. We can then display the averaged traces with:

1traces.mean("iteration").plot(x="oscilloscope.sample", col="dut.plaintext")
2plt.show()
../_images/6de285106f79074bfb5849f4bc754bf9bbc9097c9d5eb3c74523d9b38dc68d08.png

It’s better, the rounds are now visible. However, we are only using two different plaintexts, which is not enough for most side-channel attacks. Let’s see how to generate a lot more !

Generating random data#

We could generate each of the 64-bit plaintexts supported by PRESENT. However, this would be too long to be practically feasible. Let us see how to randomly sample them uniformly in the message space.

The !random node represents values generated by the Numpy random number generators. We can generate random 64-bit integers with this sample:

 1pt_yaml = """
 2!random
 3  distribution: integers
 4  parameters:
 5    dtype: uint64
 6    low: 0
 7    high: 0x10000000000000000
 8  size: 10
 9"""
10
11pt = phileas.parsing.load_iteration_tree_from_yaml_file(pt_yaml)

The distribution refers to numpy.random.Generator.integers(), whose arguments are taken from parameters. size specifies the number of generated values, which will be infinitely many if it is none or not given.

Let us see what values this contains:

1list(pt)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[18], line 1
----> 1 list(pt)

File ~/checkouts/readthedocs.org/user_builds/phileas/checkouts/stable/phileas/iteration/base.py:463, in IterationTree.__iter__(self)
    459 """
    460 Return a two-way resetable tree iterator.
    461 """
    462 if len(self.configurations) == 0:
--> 463     return self._iter()
    465 logger.warning(
    466     "Iterating through a configurable tree. It is automatically "
    467     "unrolled, but it is advised to explicitly call "
    468     "IterationTree.unroll_configurations before iteration."
    469 )
    471 return iter(self.unroll_configurations())

File ~/checkouts/readthedocs.org/user_builds/phileas/checkouts/stable/phileas/iteration/leaf.py:383, in NumpyRNG._iter(self)
    382 def _iter(self) -> TreeIterator:
--> 383     return NumpyRNGIterator(self)

File ~/checkouts/readthedocs.org/user_builds/phileas/checkouts/stable/phileas/iteration/leaf.py:405, in NumpyRNGIterator.__init__(self, tree)
    402 super().__init__(tree)
    404 if tree.seed is None:
--> 405     raise ValueError("Cannot iterate over a non seeded random leaf.")
    407 self.seed = np.random.SeedSequence(list(tree.seed.to_bytes()))

ValueError: Cannot iterate over a non seeded random leaf.

With Phileas, iteration over all configurations must be deterministic, so true randomness cannot be achieved. To circumvent this, deterministic pseudo-random number generators are used, which generate seemingly random numbers in a repeatable way. However, they must be seeded before use: you can use generate_seeds(), which populate the random nodes of a tree with seeds that depend on their location, and a user-supplied salt. This salt can then be used to generate multiple different experiments with the same configuration file.

1print("With salt='first'")
2pprint(list(phileas.iteration.random.generate_seeds(pt, salt="first")))
3print("\nWith salt='second'")
4pprint(list(phileas.iteration.random.generate_seeds(pt, salt="second")))
With salt='first'
[np.uint64(4763668544205790273),
 np.uint64(2335858191021624770),
 np.uint64(6353463480138259074),
 np.uint64(8025998550422391791),
 np.uint64(5590162691224468292),
 np.uint64(3688076759417327104),
 np.uint64(15424738477626186428),
 np.uint64(4629005567611139026),
 np.uint64(95670261871745950),
 np.uint64(8761369241975504774)]

With salt='second'
[np.uint64(12698494764536959150),
 np.uint64(292925724620152663),
 np.uint64(3731292948904510256),
 np.uint64(4149468841753168599),
 np.uint64(15948806649630493723),
 np.uint64(9171799223842692112),
 np.uint64(49911355859319447),
 np.uint64(10687623950509578999),
 np.uint64(4160189423562294913),
 np.uint64(313738405728289146)]

Note

The RNG from Numpy can only work with at most 64-bit wide integers. If you want to generate arbitrarily big ones, you can use !random_uniform_bigint instead.

Randomizing the iteration order#

Let’s suppose that there is a low-frequency noise source in the experimental setup. It affects the measurements, and its value slowly changes in time. Thus, the measurements related to the same plaintext will likely suffer from the same noise value. This will bias the statistical algorithms used to carry out the side-channel attack.

A solution against this is to randomize the iteration order. Instead of recording 1000 traces related with the first plaintext, then 1000 linked to the second, and so on, we can pick the plaintexts randomly, and ensure that each one is picked 1000 times.

With Phileas, this is done by the !shuffle node:

 1pt_yaml = """
 2!shuffle
 3  child: !product
 4    plaintext: !sequence [111, 222, 333]
 5    iteration: !range
 6      start: 1
 7      end: 3
 8      resolution: 1
 9"""
10
11pt = phileas.iteration.random.generate_seeds(phileas.parsing.load_iteration_tree_from_yaml_file(pt_yaml))
12pprint(list(pt))
[{'iteration': 2, 'plaintext': 111},
 {'iteration': 3, 'plaintext': 222},
 {'iteration': 3, 'plaintext': 111},
 {'iteration': 2, 'plaintext': 222},
 {'iteration': 3, 'plaintext': 333},
 {'iteration': 2, 'plaintext': 333},
 {'iteration': 1, 'plaintext': 222},
 {'iteration': 1, 'plaintext': 333},
 {'iteration': 1, 'plaintext': 111}]

Wrapping up and getting further#

We have seen how to use Phileas to:

  • automatically instantiate and configure the instruments of the experiment bench;

  • represent simple experiments with configuration files;

  • create storage containers for the acquired data;

  • generate random data values and

  • shuffle the configurations iteration order.

We can wrap all of this in a single experiment file and script:

 1from phileas.parsing import load_iteration_tree_from_yaml_file
 2from phileas.iteration.utility import (
 3    iteration_tree_to_xarray_parameters,
 4    data_tree_to_xarray_index
 5)
 6from phileas.iteration.random import generate_seeds
 7
 8experiment = """!shuffle
 9  child:
10    dut:
11      interface: present-encryption
12      key: !random
13        distribution: integers
14        parameters:
15          dtype: uint64
16          low: 0
17          high: 0x10000000000000000
18        size: 10
19      plaintext: !random
20        distribution: integers
21        parameters:
22          dtype: uint64
23          low: 0
24          high: 0x10000000000000000
25        size: 10
26
27    oscilloscope:
28      interface: oscilloscope
29      amplitude: 10
30
31    iteration: !range
32      start: 1
33      end: 10
34      resolution: 1
35"""
36
37configurations = generate_seeds(load_iteration_tree_from_yaml_file(experiment))
38
39coords, dims_name, dims_shape = iteration_tree_to_xarray_parameters(configurations)
40dut.key = 12
41dut.encrypt(0)
42trace = oscilloscope.get_measurement()
43coords["oscilloscope.sample"] = list(range(trace.shape[0]))
44dims_name.append("oscilloscope.sample")
45dims_shape.append(trace.shape[0])
46traces = xr.DataArray(coords=coords, dims=dims_name)
47
48for config in configurations:
49    factory.configure_experiment(config)
50
51    dut.encrypt(config["dut"]["plaintext"])
52    trace = oscilloscope.get_measurement()
53
54    index = data_tree_to_xarray_index(config, dims_name)
55    traces.loc[index] = trace
56
57traces
<xarray.DataArray (dut.key: 10, dut.plaintext: 10, iteration: 10,
                   oscilloscope.sample: 1248)> Size: 10MB
array([[[[-4.38037485e-01,  7.59864730e-01,  4.34917922e-01, ...,
          -7.63242481e-01, -1.85266377e-01, -5.73231827e-01],
         [ 1.95843817e-01,  1.15172872e-01, -5.19674140e-01, ...,
           4.55491228e-01, -6.24815001e-01,  1.81674314e-01],
         [-6.39479502e-01, -3.79184231e-01,  6.67895217e-01, ...,
          -7.29807150e-02, -2.15769045e+00,  1.42720470e+00],
         ...,
         [-4.07869726e-01,  2.03745978e+00,  3.17547494e+00, ...,
          -5.63567181e-01,  2.18618918e+00, -1.63989315e+00],
         [-1.07745090e-01,  2.54755276e+00, -7.94872625e-01, ...,
          -5.24253331e-02, -4.44919503e-01, -3.25951679e-01],
         [-2.01154823e-01,  1.58461004e+00,  1.39273569e+00, ...,
          -1.07622860e+00, -2.24473202e-01,  7.29289268e-01]],

        [[ 1.65353122e+00, -1.23312651e+00,  4.94792955e-01, ...,
          -1.87494542e+00,  1.11669571e+00, -7.11849235e-01],
         [ 2.08124441e+00,  4.66458036e-01, -5.88411205e-01, ...,
          -3.55638325e-01,  1.62217253e-01, -1.36643297e+00],
         [-4.58209279e-01, -9.22351567e-01, -3.17993717e-01, ...,
           1.40843359e+00,  2.14434644e+00, -7.64408468e-02],
...
          -9.81897107e-01,  2.39308470e+00,  5.77205982e-01],
         [ 2.94931556e-01, -3.26146547e+00,  4.14906337e-01, ...,
           4.24508740e-01,  2.08954141e-03, -1.55362334e+00],
         [ 2.04261706e+00,  3.14283330e-01, -1.47084646e+00, ...,
           5.51592797e-01,  3.70081439e-01, -1.74549938e+00]],

        [[ 2.51378216e+00, -2.81829589e-01,  3.69165305e-01, ...,
          -2.70567038e-01, -2.03270381e+00, -7.87076534e-01],
         [-8.74466629e-01, -1.61569449e+00, -6.82109971e-01, ...,
           2.15778115e-01,  1.96686075e+00, -7.29692215e-01],
         [ 2.65752987e-01, -1.07205169e+00,  2.30192944e+00, ...,
          -1.29054157e+00,  1.73785637e+00,  1.02969149e-01],
         ...,
         [-5.82776526e-02,  1.32377612e+00,  3.28596801e-01, ...,
          -1.67724468e+00, -1.88363951e+00,  6.06247757e-01],
         [-3.63233362e-01,  8.37462336e-01, -1.01239868e-01, ...,
           1.32751223e-01,  4.51598555e-01, -6.06485156e-01],
         [ 1.94573595e+00,  8.38442203e-02,  1.32442673e-01, ...,
          -2.10053846e+00,  2.01149960e+00,  1.99224387e-01]]]],
      shape=(10, 10, 10, 1248))
Coordinates:
    dut.interface           <U18 72B 'present-encryption'
  * dut.key                 (dut.key) uint64 80B 11740344685095552390 ... 177...
  * dut.plaintext           (dut.plaintext) uint64 80B 3849265755518883408 .....
    oscilloscope.interface  <U12 48B 'oscilloscope'
    oscilloscope.amplitude  int64 8B 10
  * iteration               (iteration) int64 80B 1 2 3 4 5 6 7 8 9 10
  * oscilloscope.sample     (oscilloscope.sample) int64 10kB 0 1 2 ... 1246 1247

You can discover new features of Phileas by browsing through the [user guide] (/user_guide/index), or by exploring other examples.