Example Side-Channel Acquisition

In this notebook we show how to do an acquisition using the secbench framework, more precisely, the secbench.api and secbench.storage modules.

Bench and Hardware Setup

import logging

import matplotlib.pyplot as plt
import numpy as np

from secbench.api import get_bench
from secbench.api.simulation import SimulatedScope
from secbench.storage import Dataset, Store

First, we enable the logging and create a Bench.

logging.basicConfig(level=logging.INFO)
bench = get_bench()
INFO:secbench.api.bench:SECBENCH_USER_CONFIG not defined, no user configuration loaded
INFO:secbench.api.bench:SECBENCH_SCOPENET not defined, skipping registration of VxiScanner
INFO:secbench.api.bench:SECBENCH_PYVISA_BACKEND not defined, skipping registration of PyVisaScanner

We now grab a scope.

Note

To make the notebook executable without real hardware, we create a simulated scope and register it manually.

scope = SimulatedScope(channel_names=["1", "2"])
bench.register(scope)

In a real experiment, the previous cell is not needed, you only need to do:

scope = bench.get_scope()

Here we define the parameters of the acquisition:

total_size = 300  # How many traces to acquire.
batch_size = 100   # Size of batches acquisired for segmented acquisition.
n_batches = total_size // batch_size

Then, we need to define some device under test (DUT). It is specific to each experiment. The goal of the device under test is to perform some computation (AES, RSA, ECC or other) and ideally generate a trigger signal.

For this example, we suppose that the DUT processes a 16 byte plaintext. Our implementation below will force a trigger on the scope. In a real side-channel experiment, you will probably need to open a serial port, send the plaintext to the target, etc.

class Dut:
    def process(self, plaintext):
        scope.force_trigger()

Now, we instanciate a DUT:

dut = Dut()

We generate some inputs for the acquisitions. Remember that the DUT processes 16-bytes plaintexts in our example.

pts = np.random.randint(0, 256, size=(total_size, 16), dtype=np.uint8)

Dataset Initialization

Now, we can create a Store and a Dataset inside it for our acquisition. If we were to do multiple acquisitions, we could create multiple Dataset with different names.

store = Store('example_campaign.hdf5', mode='w')
ds = store.create_dataset('my_acquisition', total_size, 'data', 'pts')

Acquisition

To do an acquisition, we should first setup the scope and configure the trigger. This can be done manually or via the Scope API. We have a whole tutorial dedicated on this topic in Using Scopes.

Here is an example configuration. Do not forget to update it if you do a real experiment.

scope["1"].setup(range=3, offset=0)
scope["2"].setup(range=10e-3, offset=0)
scope.set_horizontal(samples=200, interval=1e-8)
scope.set_trigger(channel="1", level=1.2)

A good practice is to dump the scope configuration and save it in the dataset.

This is how you get the scope configuration (we only query channels we use in our acquisition):

scope_config = scope.config(channels=["1", "2"])
scope_config
{'scope_name': 'SimulatedScope',
 'scope_description': 'simulated scope',
 'scope_horizontal_samples': 200,
 'scope_horizontal_interval': 1e-08,
 'scope_horizontal_duration': 2e-06,
 'scope_channel_1': {'coupling': <Coupling.dc: 'dc'>,
  'offset': 0,
  'range': 3,
  'decimation': <Decimation.sample: 'sample'>,
  'enabled': True},
 'scope_channel_2': {'coupling': <Coupling.dc: 'dc'>,
  'offset': 0,
  'range': 0.01,
  'decimation': <Decimation.sample: 'sample'>,
  'enabled': True}}

This is how you save the scope configuration in a dataset. By convention, we call the asset “scope_config.json”.

ds.add_json_asset("scope_config.json", scope_config)

If your scope supports it, segmented acquisitions can drastically speed-up the acquisition process.

scope.segmented_acquisition(batch_size)

Now let’s get some traces! Here is a generic acquisition loop:

for batch_number in range(n_batches):
    # We retrieve 10 batches of 100 traces each
    start, end = batch_number * batch_size, (batch_number + 1) * batch_size
    scope.arm()
    for exec_number in range(start, end):
        dut.process(pts[exec_number])
    scope.wait()
    # Query traces from the scope.
    traces, = scope.get_data("1")
    # Add the batch of traces and pts to the dataset.
    ds.extend(traces, pts[start:end])
del ds
store.close()
del store

Reloading the Dataset

This section shows how to reload the dataset. First, it is a good idea to inspect the data using the secbench-db status command. We can see for the “my_acquisition” dataset, we have size=300, which means the dataset was fully populated.

!secbench-db status example_campaign.hdf5
ROOT
 +-- my_acquisition
     +-- capacity: 300
     +-- size: 300
     +-- fields
     |   +-- data: shape=(300, 200), dtype=int8
     |   +-- pts: shape=(300, 16), dtype=uint8
     +-- assets
         +-- scope_config.json: shape=(390,), dtype=uint8

Now, we open the store in read-only mode. For analysis, we have no reason to open the store in write mode.

store_reload = Store("example_campaign.hdf5", mode="r")
ds = store_reload["my_acquisition"]
ds_data = ds["data"]
ds_pts = ds["pts"]

When you reload data, as shown in the previous cell, the data returned are not numpy arrays. At this point the data are not loaded in RAM, which is a good thing when you have huge datasets.

print(type(ds_data))
<class 'h5py._hl.dataset.Dataset'>

Luckily, the ds_data and ds_pts support almost all numpy array methods.

print(ds_data.dtype)
print(ds_data.shape)
int8
(300, 200)
ds_data_dram = ds_data[:]
ds_pts_dram = ds_pts[:]
print(type(ds_data_dram))
<class 'numpy.ndarray'>

At this point, you can do all side-channel processing you want. Our simulated data are not very interesting, but we can plot the mean trace for example:

plt.plot(np.mean(ds_data, axis=0))
plt.xlabel("Sample Index")
plt.ylabel("ADC Sample Value")
plt.show()
../_images/10c320a41cb8bebb621ffbe6c7956ff27b5b29e3dfa4570e42cc6998b04862f3.png

You can also recover the configuration from the scope:

ds_scope_config = ds.get_json_asset("scope_config.json")
ds_scope_config
{'scope_name': 'SimulatedScope',
 'scope_description': 'simulated scope',
 'scope_horizontal_samples': 200,
 'scope_horizontal_interval': 1e-08,
 'scope_horizontal_duration': 2e-06,
 'scope_channel_1': {'coupling': 'dc',
  'offset': 0,
  'range': 3,
  'decimation': 'sample',
  'enabled': True},
 'scope_channel_2': {'coupling': 'dc',
  'offset': 0,
  'range': 0.01,
  'decimation': 'sample',
  'enabled': True}}

One nice thing is that we can recompute the real time scale of traces.

time_ns = 1e9 * scope_config["scope_horizontal_interval"] * np.arange(ds_data.shape[1])

plt.plot(time_ns, np.mean(ds_data, axis=0))
plt.xlabel("Time (ns)")
plt.ylabel("ADC Sample Value")
plt.show()
../_images/af9f29c941a6abf4ea47b7229ebabf729fa1d9075d3d742f26ff4ca1ecd0e1c8.png