Discovering containers

This tutorial will introduce traces and containers, which are base classes used to store the experimental measurement in Lascar.

Trace

In Lascar, side-channel data are stored within Trace instances. This class is a named tuple with two attributes:

  • The first item is leakage and represents the observable side-channel, such as a power trace.
  • The second item is value and represents the handled values during the observation of the leakage.

Both attributes must always be numpy.ndarray of any shape.

The __str__ method of Trace displays the shape and dtype for both leakage and value.

The following creates a trace with (fake) side channel leakage and associated data. The leakage is a vector of 10 time samples, stored in a numpy.ndarray. The value is an array of 16 bytes, which can represent for instance a key.

from lascar.container import Trace
import numpy as np

leakage = np.random.rand(100)
value = np.random.randint(0, 256, (16,), dtype=np.uint8)
trace = Trace(leakage, value)
print("Trace:", trace)

This will output:

Trace: Trace with leakage:[(100,), float64] value:[(16,), uint8]

Container

Most of time, side-channel analysis requires multiple traces. In Lascar, containers are used to group all the traces as a collection. Side-channel data can arises from:

  • a measurement device coupled to the device under test,
  • an acquisition campaign already saved on hard drive,
  • the simulation of an algorithm on a device,
  • or any other source of leakage information.

The Container class provides an interface for accessing the traces (leakage and values). A class may implement this interface to adapt to any source of leakage and provide the traces when requested by the Lascar processing pipeline.

Lascar already defines multiple Container implementations, and the most important is TraceBatchContainer, which stores in RAM both the leakages and values as numpy.ndarray, sharing the same first dimension: the number of traces in the container.

In the following, a TraceBatchContainer is instanciated from two numpy.ndarray: leakages and values:

leakages = np.random.rand(10, 100)  # 10 leakages, 100 time samples each.
values = np.random.randint(0, 256, (10, 16))  # 10 associated values, 16 bytes each.
batch = TraceBatchContainer(leakages, values)
print("Batch:", batch)

This will output:

Batch container: Container with 10 traces. leakages: [(100,), float64], values: [(16,), int64].

Indexing and iteration

Containers implement the index operator, which returns either a Trace when an index is given, or a TraceBatchContainer with multiple traces when a slice is given. Furthermore, containers are iterable.

print("batch[0]:", batch[0])
print("batch[:5]:", batch[:5])
print("batch[range(3)]:", batch[range(3)])

for trace in batch:
    print("Batch iteration:", trace)

This will output:

batch[0]: Trace with leakage:[(100,), float64] value:[(16,), int64]
batch[:5]: Container with 5 traces. leakages: [(100,), float64], values: [(16,), int64].
batch[range(3)]: Container with 3 traces. leakages: [(100,), float64], values: [(16,), int64].
Batch iteration: Trace with leakage:[(100,), float64] value:[(16,), int64]
Batch iteration: Trace with leakage:[(100,), float64] value:[(16,), int64]
Batch iteration: Trace with leakage:[(100,), float64] value:[(16,), int64]
Batch iteration: Trace with leakage:[(100,), float64] value:[(16,), int64]
Batch iteration: Trace with leakage:[(100,), float64] value:[(16,), int64]
Batch iteration: Trace with leakage:[(100,), float64] value:[(16,), int64]
Batch iteration: Trace with leakage:[(100,), float64] value:[(16,), int64]
Batch iteration: Trace with leakage:[(100,), float64] value:[(16,), int64]
Batch iteration: Trace with leakage:[(100,), float64] value:[(16,), int64]
Batch iteration: Trace with leakage:[(100,), float64] value:[(16,), int64]

Data subsets

Containers offer different mechanisms to limit the data to subsets. leakage_section (resp. value_section) is a Container attribute that will select the specified samples from the original leakage (resp. value). It is supposed to minimize the reading part, by specifying points of interests for instance.

# To work only on leakage sample 10 and 15:
batch.leakage_section = [10, 15]
print(batch)

# To work only with the first 10 samples:
batch.leakage_section = range(10)
print(trace_batch)

# To work with only with one tenth of the sample:
batch.leakage_section = range(0, 100, 10)
print(trace_batch)

# To cancel `leakage_section`:
batch.leakage_section = None  # cancelling leakage_section
print(trace_batch)

This will output:

Container with 10 traces. leakages: [(2,), float64], values: [(16,), int64]. leakage_section set to [10, 15].
Container with 10 traces. leakages: [(10,), float64], values: [(16,), int64]. leakage_section set to range(0, 10).
Container with 10 traces. leakages: [(10,), float64], values: [(16,), int64]. leakage_section set to range(0, 100, 10).
Container with 10 traces. leakages: [(100,), float64], values: [(16,), int64].

Data processing

leakage_processing (resp. value_processing) is a Container attribute which can be a function that will be applied on the leakage (resp. value) after leakage_section (resp. value_section). Leakage processing can be used for instance for side-channel trace resynchronisation, signal filtering, etc. See lascar/tools/processing for a list of existing processing.

from lascar.tools.processing import *

# Any function or callable is accepted, provided it fits with the original
# leakage shape.
batch.leakage_processing = lambda leakage: leakage**2

# Centered product for high-order side-channel attacks: recombine samples
# [0, 1, 2] with [3, 4, 5]
batch.leakage_processing = CenteredProductProcessing(
    batch, [[0, 1, 2], [3, 4, 5]]
)

# Principal component analysis on leakage with 3 components
batch.leakage_processing = PcaProcessing(trace_batch, 3)

# No leakage processing
batch.leakage_processing = None

Logging

All container children implement a logger (from the logging module). By default, the loglevel is set to INFO, but it can be set at any time to display more or less informations.

batch.logger.setLevel("DEBUG")
print(batch[::2])
batch.logger.setLevel("INFO")

This will output:

2023-02-02 11:05:10,032 - lascar.container.container - DEBUG - __getitem__ with key slice(None, None, 2) <class 'slice'>
2023-02-02 11:05:10,032 - lascar.container.container - DEBUG - Setting leakage_section to None
2023-02-02 11:05:10,032 - lascar.container.container - DEBUG - Setting value_section to None
2023-02-02 11:05:10,032 - lascar.container.container - DEBUG - Setting leakage_processing to None
2023-02-02 11:05:10,032 - lascar.container.container - DEBUG - Setting value_processing to None
Container with 5 traces. leakages: [(100,), float64], values: [(16,), int64].

Note

Note: other lascar classes implement a logger as well: Session, Engine, OutputMethod.