Module: secbench.api

The secbench.api module contains common definitions used across secbench packages. The primary goal of the package is to define interfaces of all instruments relevant for hardware security characterization such as side-channel analysis and fault injections. But the package other important things:

  • A convenient discovery mechanism for available hardware. This mechanism is configurable and allows loading instruments without hard-coding them in scripts (TTY paths, serial numbers, etc.).

  • A Bench that manages all equipment for experiments.

  • Helpers to implement drivers in few lines of codes for lab equipments, though serial, Ethernet, USB.

Exceptions

Most errors that occur in the internal layers of secbench are subclasses of SecbenchError.

exception SecbenchError

Base exception for secbench related operations.

exception BackendError

An error that occurred in a communication backend.

__init__(msg)
exception NoSuchHardwareError

Exception raised when no device of the requested hardware type is available.

__init__(hw_class, what='')
exception MissingDependency

A dependency (e.g., a Python package) is missing for using a feature.

exception InstrumentError

Base exception for instrument-related operations.

__init__(msg)
exception InstrumentPendingErrors
__init__(errors)
exception UnsupportedFeature

Exception raised when attempting to use a feature that is not supported.

__init__(pkg_name, usage='')
exception InvalidParameter

Invalid parameters requested to an instrument.

exception NoSuchChannelError

Exception raised when accessing a channel that is not available on the instrument.

__init__(channel)

Bench

A Bench allows discovery and instantiation of hardware. You can create a Bench instance by calling the default constructor without arguments:

from secbench.api import Bench
bench = Bench()

In security tests, unless you have to run multiple experiments in parallel in the same scripts, it is recommended to use the get_bench() function to create a bench. It returns a common bench instance, acting as a global variable.

from secbench.api import get_bench
bench = get_bench()

The Bench class provides methods for requesting different type of hardware. Note that by default, a bench will cache the components loaded, so that hardware discovery is not ran every time.

from secbench.api import get_bench
from secbench.api.instrument import Scope

bench = get_bench()
scope = bench.get_scope() # Slow the first time.
scope = bench.get_scope() # Fast: a scope is already in the bench.
scope = bench.get(Scope)  # Fast: same as the previous line.

More details on this topic are available in the tutorial Bench and Device Discovery.

class Bench

A class that manages instruments during experiments.

Parameters:

initial_scan – if True, performs an initial scan of all hardware connected. This is disabled by default to avoid any overhead.

__init__(initial_scan=False)
clear_cache()

Release all cached instruments.

Warning

Calling this function only frees internal references to the instruments. If you keep external references, the destructors will not be called by the garbage collector.

hardware_info()

Return the hardware information used for device discovery.

discover_first(base_cls, policy=DiscoverPolicy.max_weight)

Discover the best matching device given discover policy.

Parameters:
  • base_cls – base class to be discovered.

  • policy – policy used for choosing the best match.

has_hardware(base_cls)

Test if a given hardware is available or not.

Note

Doing this test will not put the hardware in the bench internal cache. The reason is that this function only discovers the hardware and does not construct it (which can have a cost).

If you wish to cache the hardware and test its presence, you should rather use:

instrument = bench.get(HardwareType, required=False)
if instrument:
    # Hardware is available.
    pass
is_loaded(hw_type)

Test if a given hardware type is loaded or not.

This function will work only if you used cache=True when requesting hardware with Bench.get().

get(hw_type, policy=DiscoverPolicy.single, cache=True, required=True)

Discover an instance of a given class.

This method will look for all subclasses of hw_type that implement the Discoverable interface in the current scope.

Parameters:
  • hw_type – class to be discovered.

  • policy – policy for choosing the best match

  • cache – if True (default), will look if the class was already loaded before attempting a true discovery.

  • required – if True and no hardware is found, an error will be raised.

get_all(hw_type)

Return all instances of a given hardware type.

Note

The instances created will not be cached in the current bench.

register(hw)

Register a custom instrument in the bench.

get_scope(**kwargs)

Look for a Scope instance.

get_afg(**kwargs)

Look for a Afg instance.

get_psu(**kwargs)

Look for a PowerSupply instance.

get_pulser(**kwargs)

Look for a Pulser instance.

get_table(**kwargs)

Look for a Table instance.

get_bench()

Return an instance of secbench’s default bench.

The get_bench() function always returns the same object. It is nothing more that a global variable. By using only this method, all hardware loaded will be cached. If you do not want this behavior you have several options:

Device Discovery

Note

Understanding this section is not required for a regular secbench usage. It is indented to developpers and advanced users.

To implement automatic device discovery any class of instrument that is discoverable (Scope, Afg, …) implements the Discoverable interface:

class Discoverable

An interface implemented by hardware that is discoverable.

classmethod is_supported(hardware_info)

Test if this class can be discovered.

This is useful for instruments that are platform-specific.

abstractmethod classmethod discover(hardware_info)

Return a generator of possible instantiations of the class.

Parameters:

hardware_info – information that can be used by implementors for checking hardware availability.

Returns:

a generator of valid arguments that can be passed to Discoverable.build() for constructing instances.

classmethod discover_weight()

A weight that can be used to select the best match when multiple instances of the same hardware are available (e.g., USB Scope over Ethernet).

A higher value give more priority.

abstractmethod classmethod build(hw_info, args)

Instantiate the hardware using specific parameters returned by discover.

class DiscoverPolicy

Policy to use to pick the hardware when several devices are found during the discovery.

  • first: select the first element (discarding others). This is not deterministic, as we provide no guarantees on the order on which elements appear.

  • single: return the first element and raise an exception (secbench.api.NoSuchHardware) if more than one element exists.

  • max_weight: return the hardware with the highest weight.

__new__(value)

In very specific use cases, you can directly discover a specific class (without passing through a Bench) using the discover() method.

discover(base_cls, hardware_info=None)

A helper for enumerating all subclasses of base_cls.

Returns:

an iterator of (subclass, build_arguments)

Types and Enumerations

All enumerations are available in the secbench.api.enums namespace.

Enumerations used in the control of oscilloscopes:

class Coupling

Common channel coupling modes found on oscilloscopes.

Specific scope implementation may support additional modes.

  • ac: Alternating Coupling

  • dc: Direct Coupling, high resistance termination (typically \(1 M\Omega\))

  • dc_low_impedance: Direct Coupling, low resistance termination (typically \(50\Omega\))

__new__(value)
class Slope

Trigger slope enumeration.

  • rising: trig on rising slope

  • falling: trig on falling slope

  • either: trig on either rising or falling slope

__new__(value)
class Arithmetic

Waveform arithmetic enumeration.

  • off: don’t apply any transformation (default)

  • envelope: compute the waveform envelope

  • average: compute the waveform average

__new__(value)
class Decimation

Decimation method enumeration.

Defines the method to reduce the data stream of the ADC to a stream of waveform points with lower sample rate.

  • sample: one of every N samples

  • peak: minimum and maximum of N samples (peak detection)

  • highres: average of N samples

  • rms: root mean square of N samples

__new__(value)

Enumerations used in the control of arbitrary function generators:

class Function

Different shapes that a waveform from an AFG can take.

  • sinus: Sinusoïdal function

  • square: Square function

  • ramp: Ramp function

  • pulse: Pulse function

  • noise: Noise function

  • dc: continuous offset

__new__(value)
class OutputLoad

This enum allows to define the output load. Infinity is set to > 9E+38

  • ohms_50: sets the output load to 50Ohm.

  • ohms_10k: sets the output load to 10kOhms.

  • inf: sets the output load to infinity.

__new__(value)
class Polarity

Output polarity of a signal.

  • normal: use to set the afg signal polarity to normal.

  • inverted: use to invert the afg signal polarity.

__new__(value)
class TriggerSource

The trigger source settings stands for:

  • external: external trigger is set via this source and allows the afg to detect a TTL pulse. The user must also set the TriggerSlope. (default to rising). Use the external connector on the rear panel.

  • manual: trigger is done via the afg API using afg.trigger().

  • internal: use an internal clock to use as a timer trigger.

__new__(value)

Enumerations used in the control of power supplies (PSU):

class TrackingMode

TrackingMode enumeration for controllable power supply.

This only makes sense for power supplies with multiple channels.

  • autonomous: channels are autonomous.

  • serial: channels are connected in serial.

  • parallel: channels are connected in parallel.

__new__(value)

Scope

In order to use a instrument.Scope you will first obtain it through a Bench and the Bench.get_scope() method. Then, you can use the following interface implemented by all scope.

Although some scope provide some extra functionalities, we do not recommend to use them as it will reduce the portability of your script.

class Scope

Base abstract class for scope hardware.

abstract property description

The self-description of this instrument. This is typically a string representation of the device serial number.

>>> scope.description
"1329.7002K14-100654-Hn"
abstractmethod channels()

Return a mapping of analog channels available.

abstractmethod horizontal_interval()

Duration in seconds between two points.

>>> scope.horizontal_interval()
1e-10
abstractmethod horizontal_duration()

Duration in seconds of the whole acquisition.

>>> scope.horizontal_duration()
1e-07
abstractmethod horizontal_samples()

Number of samples (points) for the whole acquisition.

>>> scope.horizontal_samples()
1000
abstractmethod bit_resolution()

Get the number of bits used to represent a single sample.

Usually this value is 8 or 16 scopes that have high-precision ADCs.

abstractmethod set_bit_resolution(prec)

Set the number of bits to represent a single sample.

The values supported are usually 8, 12 or 16, depending on the scope model.

abstractmethod trigger_count()

When using an acquisition count greater than 1, return the number of triggers that happened since last arming.

abstractmethod reset()

Reset the instrument to factory defaults. This usually resets the timebase and trigger settings to default values and disables all channels. This can be a no-op on some hardware.

>>> scope.reset()
abstractmethod disable_trigger_out()

Disable the trigger out signal.

abstractmethod sync()

Synchronize the class instance with the scope parameters.

set_trigger(channel, level, slope=Slope.rising, delay=0)

Configure the trigger condition.

>>> scope.set_trigger('1', Slope.rising, 0.5)
Parameters:
  • channel – the name of the channel used to trigger

  • slope – the Slope to trigger on

  • level – the trigger level in volts

  • delay – the delay in seconds between trigger and start of acquisition.

enable_trigger_out(slope, length, delay=0)

Enable the trigger out signal.

Parameters:
  • slopeSlope.rising or Slope.falling

  • length – pulse length in seconds

  • delay – pulse delay in seconds

set_horizontal(*, interval=None, duration=None, samples=None)

Configure the horizontal (time) parameters. Call this method with any combination of two of the three available parameters. The third, unspecified one, will be computed according to the other two.

>>> # 10k samples during 2 ms
>>> scope.horizontal(samples=10e3, duration=2e-3)
>>> # 1 µs resolution during 0.5 ms
>>> scope.horizontal(interval=1e-6, duration=.5e-3)
>>> # 5M samples with 1 µs resolution
>>> scope.horizontal(samples=5e6, interval=1e-6)
Parameters:
  • interval – duration in seconds between two points

  • duration – duration in seconds of the whole acquisition

  • samples – number of samples (points) for the whole acquisition

Raises:

ValueError – if zero, one or three parameters are given instead of two.

segmented_acquisition(count)

Enable (or disable) segmented acquisition (aka. Ultra segmentation)

Parameters:

count – If none, disable segmented acquisition, otherwise activate segmented acquisition of count frames.

arm(count=1, iterations=1000, poll_interval=0.001)

Arm the instrument for triggering. This command returns immediately and does not wait for an actual trigger to happen. To this end, you need to call wait() just after arm().

Parameters:
  • count – number of triggers constituting a single acquisition. Some hardware only supports count=1.

  • iterations – number of iterations for polling trigger state.

  • poll_interval – delay between state polling iteration if None, will poll without interruptions.

Returns:

time elapsed.

>>> scope.arm()  # arm to receive a single trigger
>>> scope.arm(count=20)  # arm to receive 20 triggers
wait(iterations=1000, poll_interval=0.001)

Wait for the instrument to complete the latest command sent.

It is particularly useful when called after arm() to guarantee that data was actually acquired.

>>> scope.wait()  # waits 1 second
>>> scope.wait(iterations=10, poll_interval=1) # waits 10 second
Parameters:
  • iterations – Number of polling iteration before assessing timeout.

  • poll_interval – Duration of a polling loop.

Returns:

time elapsed.

wait_auto()

Wait for the instrument to complete the latest command sent. It is particularly useful when called after arm() to guarantee that data was actually acquired.

This function automatically sets up the iteration and polling parameters of the wait function.

Returns:

time elapsed.

set_data_format(bits=8, little_endian=True)

Configure the data format returned by Scope.get_data() method.

Parameters:
  • bits – Number of bits

  • little_endian – When more than one byte is returned per sample (bits > 8) specify the order of bytes (little-endian = least significant bytes first)

get_data(*channels, volts=False)

Retrieve the waveform data for the specified channel(s).

  • If volts is False, no attempt to convert raw samples to volts is made. The returned samples are raw ADC values in the device internal format. Usually this means 8 or 16 bit integer values.

  • If volts is True and the device supports it, returned samples are in volts. Usually this means floating point values.

You can retrieve the actual data type using the numpy dtype property, eg. print(my_array.dtype).

This method returns a list of waveform data, each being a single numpy.array.

>>> c1, = scope.get_data('1')  # note the comma (,)
>>> c1, c4 = scope.get_data('1', '4')
>>> c1_volts, = scope.get_data('1', volts=True)
Parameters:
  • channels – the channel(s) to retrieve data from

  • volts – return volts instead of raw samples (if supported)

config(channels=None, only_enabled=False)

Return a dictionary of scope attributes

EXAMPLES

>>> scope.config(channels=['A', 'B'], only_enabled=True)
{ 'scope_name': Foo,
  'scope_horizontal_samples': 10000,
  ...
}
clear(pop_errors=False)

Clear pending measurements, status flags and optionally pop errors.

Can be used to bring back the scope in a usable mode if something goes wrong.

calibrate(channel, clip_detection_callback, method=None)

Perform automatic calibration of the scope’s vertical range.

Parameters:
  • channel – The scope channel to calibrate

  • clip_detection_callback – A function responsible for detecting whether the scope is clipping or not. This callback takes as input the scope and a channel and returns a boolean.

  • method – The calibration algorithm to use. See instances of the Calibrate abstract method.

Returns:

The voltage scale found.

Raises:

ValueError if the maximum number of iteration is reached.

Added in version 5.1.0.

Example:

from secbench.scope.util import is_clipping

def is_clipping_callback(scope, channel):
    scope.arm()
    # send DUT input
    scope.wait()
    d, = scope.get_data(channel)
    return is_clipping(d)

# Latter in your code:
scope.calibrate('1', is_clipping_callback, method=StepSearchCalibration())
class ScopeAnalogChannel

Represents a scope analog channel.

Use the dict syntax on Scope instances to retrieve a channel instance.

disable()

Disable the channel.

abstractmethod setup(range=None, coupling=None, offset=None, decimation=Decimation.sample)

Enable the channel and configure it with the given parameters.

  • For analog channels, range and coupling are required.

Parameters:
  • range – the vertical range, in volt

  • coupling – the coupling

  • offset – the offset in volt (default: 0)

  • decimation – the decimation method (default: sample)

abstractmethod set_arithmetic(arithmetic, reset=1)

Set the channel arithmetic function. The function is reset after reset triggers.

Parameters:
  • arithmetic – the arithmetic function to use

  • reset – the number of triggers after which the function is reset

Scope Vertical Range Calibration

Automatic scope calibration can be performed through the calibrate(). All calibration algorithms implement the interface Calibration.

We currently provide the following methods:

  • instrument.StepSearchCalibration: rescale the range according to a pre-defined scales. This is the default and recommended method.

  • instrument.BinarySearchCalibration: use a binary search to optimize the vertical range.

  • instrument.LinearSearchCalibration: gradually increases the voltage range until the traces do not clip. This method is very slow.

class Calibration

Abstract interface for scope calibration methods.

abstractmethod run(scope, channel, clip_detection_callback)

Launch the calibration on a given channel.

This method has to be reimplemented by the different calibration algorithms.

class StepSearchCalibration

Perform automatic calibration of the scope’s vertical range using a stepwise search.

The algorithm starts from volts_max and reduces the voltage range to reach the selected profile. It also sets updates the voltage offset.

__init__(volts_min=0.001, volts_max=20, profile=None)
run(scope, channel, callback)

Run the calibration of scope’s voltage range and offset following the provided profile.

Parameters:
  • scope – Scope that can be used to perform acquisitions

  • channel – Scope channel on which calibration should be done.

  • callback – A function which returns a tuple (adc_dynamics, adc_offset). adc_dynamics represents the dynamics of the signal when sampled by ADCs (e.g., abs(max - min)). adc_offset represents the offset of the signal when sampled by ADCs (e.g., (min + max) / 2).

class BinarySearchCalibration

Perform automatic calibration of the scope vertical range using a binary search.

The initial search window is the voltage range [volts_min; volts_max]. The search range is shrunk at each iteration by 2. The algorithm stops if the search window size is below volts_prec.

If the algorithm does not find a non-clipping setting after max_iterations a ValueError is raised.

__init__(volts_min=0.001, volts_max=1, volts_prec=0.01, max_iterations=15)
run(scope, channel, clip_detection_callback)

Launch the calibration on a given channel.

This method has to be reimplemented by the different calibration algorithms.

class LinearSearchCalibration

Perform automatic calibration of the scope’s vertical range using a linear search.

The algorithm starts from volts_min and increment the voltage by volt_prec until the signal stops clipping. The algorithm stops if volts_max is reached.

If the algorithm does not find a non-clipping setting after max_iterations a ValueError is raised.

__init__(volts_min=0.001, volts_max=1, volts_prec=0.05, max_iterations=15)
run(scope, channel, clip_detection_callback)

Launch the calibration on a given channel.

This method has to be reimplemented by the different calibration algorithms.

Table

class Table
__init__()
class Carto

Helper object to iterate over a uniform, rectangular grid of given width and height.

Usage for 8 × 11 grid:

from secbench.table import carto
grid = carto.Carto(8, 11)

for step in grid:
    # do acquisition here, for example:
    scope.arm()
    dut.trigger()
    scope.wait()
    data, = scope.get_data('1')
    store.store(data)

On the first run of this program, you’ll be asked to interactively position the probe on the top-left, then bottom-right positions using an external program, typically secbench-axectrl.

On every run of this program, you’ll be asked to interactively set the probe Z position using an external program.

You can set the amount of Z distance the probe is lifted between each position. By default, this is 1mm. For 2.5mm, use:

grid = carto.Carto(8, 11, z_up=2.5)

If you need to start over the top-left/bottom-right positioning after the first run, just delete the .carto.json file.

You may want to store the X, Y coordinates of each step (eg. in store() or for the trace filename). To do so, use the for loop iterator variable that has index and position tuples:

for step in grid:
    x, y = step.index
    x_mm, y_mm = step.position
    print("i am at cell", x, y)
    print("axes are positioned at", x_mm, y_mm)

index is the integer coordinates in \([0, width - 1]\), \([0, height - 1]\).

position is the absolute coordinates of the motorized axes in millimeters, starting from the axis hardware zero (“home”).

You can use various “visitors” controlling the order in which grid positions are visited:

grid = carto.Carto(8, 11, visitor=carto.visit_random)

See the docstring of each visitor for more information:

  • visit_spiral()

  • visit_rows()

  • visit_random()

__init__(table, width, height, state_file=None, visitor=<function visit_spiral>, z_up=1.0)

Arbitrary Function Generators

An arbitrary function generator has one or several channel that allows generating signals. The instrument.Afg interface gives general control on the instrument, while instrument.AfgChannel generating signals.

class Afg

Abstract interface for Arbitrary function generators.

abstract property description

A unique string identifying the instrument.

abstractmethod set_locked(locked)

Lock or unlock the instrument.

Example:

>>> afg.set_locked(True) # Device is locked
>>> afg.set_locked(False) # Device is unlocked
abstractmethod channels()

Return a list of channels available on this instrument.

default_channel()

Return a conventional default channel for the AFG.

The default implementation returns the first channel.

reset()

Reset the instrument.

The default implementation invokes the Afg.clear() method.

clear(pop_errors=True)

Clear pending measurements, status flags and optionally pop errors.

class AfgChannel
abstract property parent

Return the parent AFG.

abstractmethod output_state()

Return the output state for the current channel.

abstractmethod trigger_state()

Return current trigger configuration.

abstractmethod force_trigger()

Force a manual trigger to a specific channel.

abstractmethod set_burst_mode(cycles, mode)

Enable burst mode (generation of a finite number of shapes).

Example:

>>> afg_ch1.set_burst_mode(1, BurstMode.triggered)
abstractmethod burst_count()

Gets the number of burst for a specific channel.

Example:

>>> ncycles = afg_ch1.burst_count()
1
abstractmethod set_voltage(amplitude, offset=None)

Set voltage parameters.

Example:

>>> afg_ch1.set_voltage(1.5, 0)
abstractmethod voltage()

Return the voltage parameters of the AFG.

Example:

>>> volt, _ = afg_ch1.voltage()
>>> volt, offset = afg_ch1.voltage()
abstractmethod set_frequency(frequency)

Set the signal frequency (in Hz).

Example:

>>> afg_ch1.set_frequency(1E+5)
abstractmethod frequency()

Return the frequency for the current function (in Hz).

Example:

>>> frequency = afg_ch1.frequency()
1E+6
abstractmethod set_function(function)

Set the function of the arbitrary function generator.

Example:

>>> afg_ch1.set_function(Function.sinus)
>>> afg_ch1.set_function(Function.pulse)
abstractmethod function()

Return the active function of the arbitrary function generator.

abstractmethod set_duty_cycle(w)

Set the duty cycle in percentage (i.e., from 0 to 100) for the square wave.

abstractmethod duty_cycle()

Returns the duty cycle in percentage (i.e., from 0 to 100) of the square wave.

abstractmethod set_ratio(w)

Sets the ratio for functions that support it (pulse, ramp, etc.).

abstractmethod ratio()

Return the ratio of the ramp function in percentage.

abstractmethod pulse_width()

Returns the pulse width in nanoseconds for the pulse function.

abstractmethod set_pulse_width(width_ns)

Set the pulse width in nanoseconds for the pulse function.

abstractmethod pulse_edge_time()

Return the rising edge time in nanoseconds for pulse function.

abstractmethod set_pulse_edge_time(edge_time_ns)

Set the rising edge time in nanoseconds for pulse function.

abstractmethod pulse_delay()

Retrieve the pulse delay in nanoseconds.

abstractmethod set_pulse_delay(delay_ns)

Set the pulse delay in nanoseconds.

abstractmethod set_trigger_delay(delay_ns)

Set the delay from the trigger signal.

This delay should not be used for precise offset. Usually Afg.set_pulse_delay() provides better precision.

set_combined_delay(delay_ns)

Configure an arbitrary delay, keeping a good precision.

This method combines Afg.set_trigger_delay() and Afg.set_pulse_delay(). This is the recommended method to use if you need to set long delays.

generate_square(frequency, voltage, offset=None, duty_cycle=50)

Configure a square waveform in a single call.

generate_ramp(frequency, voltage, offset=None, ratio=100)

Configure a ramp waveform in a single call.

generate_pulse(frequency, voltage, offset=None, edge_time_ns=5, width_ns=20)

Configure a pulse waveform in a single call.

generate_noise(voltage, offset=None)

Configure a noise waveform in a single call.

Controllable Power Supply

The secbench framework provides support for remote-controllable power supplies. Such devices implement the abstract abstract interface instrument.PowerSupply. The latter class provide general instrument control. For doing useful operations like setting voltage, you will need to query a channel and use the instrument.PowerSupplyChannel interface.

class PowerSupply

Base abstract class for a Power Supply hardware.

abstractmethod description()

The self-description of this instrument.

This is typically a string representation of the device serial number.

Example:

>>> alim.description
'SPD3XIDD4R5542,1.01.01.02.05,V3.0'
abstractmethod get_channel(channel)

Return a specific power channel output.

default_channel()

Return a conventional default channel for the AFG.

The default implementation returns the first channel.

clear(pop_errors=True)

Clear pending measurements, status flags and optionally pop errors.

Can be used to bring back the scope in a usable mode if something goes wrong.

class PowerSupplyChannel

Base abstract class for a power supply channel.

abstractmethod set_output_enabled(enabled)

Enable the output of this channel.

abstractmethod set_voltage(voltage)

Set the channel output voltage (in volts).

abstractmethod voltage()

Return the current voltage value (in volts).

abstractmethod set_current_limit(current)

Maximum current limit for the channel in Ampere.

abstractmethod current_limit()

Return the current value in Ampere.

Pulser

A pulser is an abstraction for fault injection campaigns. Different type of injector support different parameters.

class GlitchParams

Parameter of a voltage glitch fault injection.

  • delay_ns: delay of the pulse from the trigger signal.

  • width_ns: width of the pulse.

__init__(*, delay_ns, width_ns)
class EMPulseParams

Parameters for electro-magnetic fault injection.

  • delay_ns: delay of the pulse from the trigger signal.

  • width_ns: width of the pulse.

  • rise_time_ns: delay to reach the maximum voltage.

  • amplitude: amplitude of the EM pulse.

__init__(*, delay_ns, width_ns, rise_time_ns, amplitude)
class LaserParams

Parameters for laser fault injection.

  • delay_ns: delay of the pulse from the trigger signal.

  • width_ns: width of the pulse.

  • current: current of the laser source in Ampere.

__init__(*, delay_ns, width_ns, current)

The pulser interface is defined as follows:

class Pulser

Base abstract class for fault injection hardware.

abstract property description

Self-description of this instrument.

abstractmethod channels()

Get the list of channels available on the instrument.

abstractmethod output_enabled()

Is the output enabled?

abstractmethod set_output_enabled(enabled)

Enable or disable the pulser output.

abstractmethod setup_trigger(source=TriggerSource.external, slope=Slope.rising)

Configure the trigger of the pulser.

disable()

Disable the pulser.

This method should ensure that it will be safe to touch the device, typically used in destructors. Default implementation calls set_output_enabled(False).

channel_names()

Get the list of channel names.

channel_list()

Get the list of channels.

default_channel()

Return a conventional default channel for the pulser.

The default implementation returns the first channel.

clear(pop_errors=True)

Clear pending errors on the instrument.

class PulserChannel
__init__()
abstractmethod classmethod param_type()

Type of parameters for this channel.

Must be a dataclass (e.g., GlitchParams).

abstract property parent

Return the parent pulser of this channel.

abstract property name

Name of the channel.

abstractmethod set_enabled(enabled)

Enable or disable the channel.

abstractmethod enabled()

Test if a channel is enabled or not.

abstractmethod setup(**kwargs)

Modify one or more parameters of the channel.

Specific parameters are changed through kwargs, the arguments here must be valid fields of PulserChannel.param_type().

params()

Get all parameters of the channel.

set_params(params)

Set all parameters of the channel.

Then, we have some specialized injectors. Those class are just markers to discover specific injectors. For example:

class EMPulser

Inherit this class to mark your class as an EM injector.

class VGlitchPulser

Inherit this class to mark your class as a voltage glitch injector.

__init__(*args, **kwargs)
class LaserPulser

Inherit this class to mark your class as a laser injector.

__init__(*args, **kwargs)

Backends

Most lab instrument implement some sort of SCPI (Standard Commands for Programmable Instruments) commands. This is commands that look like:

idn = self.query("*IDN?")
self.write("TEMPERATURE:VALUE 100")
temperature = self.query("TEMPERATURE:VALUE?")

Secbench as a set of abstractions that help in writing drivers for SCPI-like instruments. We rely heavily on the Mixin design pattern. When possible instruments are written using an abstract Backend. We generally inherit the InstrumentMixin class to have query and write methods in the instrument class. Then, we create subclasses that instanciate the hardware with specific backend.

../_images/backend_class_diagram.png

Typical class diagram for instrument drivers.

The abstract interface of backends is the following:

class Backend

Interface of a communication backend for SCPI instruments.

set_timeout(secs)

Set timeout for blocking operations. (optional)

flush()

Ensure all pending commands were sent to the instrument.

abstractmethod close()

Force closing of the backend.

abstractmethod query(cmds)

Perform a query.

Return a stripped string.

abstractmethod query_raw(cmds, size)

Perform a device query.

Return the data returned by the device “as this”.

We currently provide the following concrete backends.

class USBTMCBackend

The USBTMC backend allows communicating with instruments through USB.

__init__(path, buffering=None)
set_timeout(secs)

Set timeout for blocking operations. (optional)

close()

Force closing of the backend.

query(cmds)

Perform a query.

Return a stripped string.

query_raw(cmds, size)

Perform a device query.

Return the data returned by the device “as this”.

class VXIBackend
__init__(host)
set_timeout(secs)

Set timeout for blocking operations. (optional)

close()

Force closing of the backend.

query(cmds)

Perform a query.

Return a stripped string.

query_raw(cmds, size)

Perform a device query.

Return the data returned by the device “as this”.

class PyVisaBackend

Implementation of the Backend interface through PyVisa.

This backend allows interacting with any instrument supported by the pyvisa framework.

__init__(instr)
set_timeout(secs)

Set timeout for blocking operations. (optional)

close()

Force closing of the backend.

query(cmds)

Perform a query.

Return a stripped string.

query_raw(cmds, size)

Perform a device query.

Return the data returned by the device “as this”.

class SerialBackend

An instrument backend over serial port.

This backend uses pyserial under the hood for serial communications.

__init__(path, serial_number=None, **kwargs)
flush()

Ensure all pending commands were sent to the instrument.

close()

Force closing of the backend.

query(cmds)

Perform a query.

Return a stripped string.

query_raw(cmds, size)

Perform a device query.

Return the data returned by the device “as this”.

set_timeout(secs)

Set timeout for blocking operations. (optional)

Then, each backend has an associated mixin class that implements the Discoverable interface and construct a class with a concrete backend. When you inherit one of those mixin, you have to implement some matching function to detect you device. For example, if you inherit the SerialDiscoverableMixin, you need to implement _match_serial() that should return if the serial number matches the current hardware.

class USBTMCDiscoverableMixin

Make your hardware an instance of Discoverable[USBTMCBuildArgs]

abstractmethod classmethod _usbtmc_match(entry, cfg)

A predicate that must be implemented to detect if an USB entry matches the current hardware.

classmethod _usbtmc_configure(backend)

A hook called after a USBTMCBackend is constructed.

This allows applying hardware-specific configuration.

classmethod _usbtmc_buffering()

Buffering mode to use when opening USB file descriptors.

class VXIDiscoverableMixin

Make your hardware an instance of Discoverable[VXIBuildArgs]

You must implement VXIDiscoverableMixin._vxi_match_idn().

abstractmethod classmethod _vxi_match_idn(idn)

A predicate to detect if an entry matches the current hardware.

Parameters:

idn – the device description returned by the *IDN? SCPI command.

classmethod _vxi_configure(backend)

A hook called after a VxiBackend is constructed.

This allows applying hardware-specific configuration.

class PyVisaDiscoverableMixin

Make your hardware discoverable through PyVisa.

You must define PyVisaDiscoverableMixin._pyvisa_match_id().

abstractmethod classmethod _pyvisa_match_id(rm, path)

A predicate that matches the device.

Parameters:
  • rm – a pyvisa ressource manager instance

  • path – pyvisa device descriptor

Returns:

True is the given instance is

classmethod _pyvisa_configure(backend)

An optional hook called after the pyvisa backend is created.

Overriding this method allows customizing the backend properties.

__init__(*args, **kwargs)
class SerialDiscoverableMixin

Make your hardware discoverable through a SerialBackend.

You must define SerialDiscoverableMixin._match_serial().

abstractmethod classmethod _match_serial(idn)

Predicate to detect if the serial identifier of a serial port matches the current hardware.

classmethod _serial_options()

This method can be overwritten to pass additional arguments to the serial port constructor.

The dictionary returned is passed to pyserial’s Serial class constructor.

Mixins and Interfaces For Instruments

In this section, we list some of the mixins available implementing instruments.

class InstrumentMixin

Defines convenience methods in instruments.

__init__(*args, **kwargs)
classmethod from_backend(backend, cfg)

Create an instance from a Backend.

has_error()

Return True if the instrument has an error pending.

pop_next_error()

Pop the next pending error from the instrument.

class WriteManyMixin

Provide a write_many method for Instruments that support chained commands.

In addition, instruments can implement several additional interfaces depending on their functionalities. You can test if an interface is implemented by a given hardware with:

isinstance(obj, HasSetupStorage)
isinstance(obj, HasWaveformStorage)
class HasSetupStorage
abstractmethod setup_load(name)

Load instrument settings from the specified file preset on the device. Can be used instead of setting up the various channels and parameters manually from Python.

To create such a preset, use the instrument interface or call setup_save().

>>> instr.setup_load('my_preset')
Parameters:

name – file name to load (absolute or relative to default preset location)

abstractmethod setup_save(name)

Save the current instrument settings to the specified file on the device.

To later load this preset, use the instrument interface or call setup_load().

>>> instr.setup_save('my_preset')
Parameters:

name – file name to save (absolute or relative to default preset location)

class HasWaveformStorage
abstractmethod write_waveform(channel, path, temporary=False)

Write waveform of a channel on the scope local storage.

Parameters:
  • channel – Channel to be saved

  • path – Path to the waveform on the device.

  • temporary – if True, will store the waveform in a temporary directory, thus you can only pass the file name.

abstractmethod read_waveform(path, temporary=False, reshape=False)

Read a binary file at a given location on the scope.

Parameters:
  • path – Path to the waveform on the device.

  • temporary – If True, will look into the scope internal waveform directory, thus you can only pass the file name.

  • reshape – If true, will reshape the data according to the current segmentation policy and scope acquisition.

Returns:

the data read.

class HasScreenShot
abstractmethod get_screenshot()

Get a screenshot of the instrument screen. Obviously this is not supported by screen-less devices. See PIL.Image for available methods.

>>> scope.get_screenshot().save('screenshot.jpg')
Return type:

PIL.Image

Helpers

Secbench Wrapper and Hooks

The hooks.secbench_main() function allows nice error wrapping and support for post-experiment notifications.

secbench_main(on_success=None, on_failure=None, exit_on_failure=True)

A Decorator for secbench experiments.

Decorating running your experiment with secbench_main, has a few advantages:

  • It will automatically create a secbench.core.bench.Bench and pass it as first argument to your function.

  • secbench_main performs some clean-up and allows to supply post-experiment actions, like sending you an e-mail or copying results (see the example below).

Parameters:
  • on_success – An optional hook called if the experiment runs successfully

  • on_failure – An optional hook called if the experiment failed

  • exit_on_failure – If true, will invoke sys.exit(1) on error

Examples:

Here is a simple example of using the secbench_main decorator.

from secbench.api.hooks import secbench_main, NotifySendHook

# A custom hook to copy data into a shared directory. You can do
# anything there.
def backup_results(results):
    subprocess.check_call(['cp', 'my-store', '/home/...'])

# Apply the secbench_main decorator to your function -> the argument
# 'bench' will be passed automatically.
@secbench_main(on_success=backup_results,
               on_failure=NotifySendHook("Something went wrong..."))
def my_experiment(bench):
    # ...
    return results


if __name__ == '__main__':
    my_experiment()

The hooks are defined in the namespace secbench.api.hooks, they are shorhands to be used with the secbench_main() decorator.

class NotifySendHook

Open a notification pop-up with a message on your windows manager.

See man notify-send.

__init__(message, urgency='critical')

Version

You can obtain the current version of secbench.api with the version() function.

version()

Current version of the secbench.api package

Other helpers

Some general helpers are available in secbench.api.helpers.

class OrderedFrozenSet
__init__(data)
bytes_to_hex(data)

Transform bytes to the corresponding hexadecimal text.

hex_to_bytes(hex)

Transform hexadecimal text to the corresponding bytes.

find_serial_device(expr)

Return the device path of a serial device given its unique vendor id, product id, serial number, etc.

Parameters:

expr – the expression to look for, eg. abcd:1234

Returns:

the device path, eg. /dev/ttyUSB0

Raises:

SecbenchError if no such device is found

find_device_serial_number(expr)

Return the device serial number of a serial device given its unique vendor id, product id, path, etc.

Parameters:

expr – the expression to look for, eg. ttyUSB3

Returns:

the device serial number, eg. DA32XM4Q

Raises:

SecbenchError if no such device is found

find_usbtmc_device(expr)

Return the device path of an USBTMC device from its vendor ID.

Parameters:

expr – vendor ID look for (case is ignored), eg. Tektronix

Returns:

an iterator on matching devices, eg. /dev/usbtmc2

Raises:

SecbenchError if no such device is found

find_usb_device(expr)

Return the device path of an USBTMC device from its vendor ID.

Parameters:

expr – vendor ID look for (case is ignored), eg. Tektronix

Returns:

an iterator on matching devices, eg. /dev/usbtmc2

Raises:

SecbenchError if no such device is found

is_clipping(data, ymin=None, ymax=None, ratio=None)

Determine if the data is clipping based on some rules.

create_cartography_points(tl, br, nx, ny, z=None, shuffle_lines=False)

Create grid coordinates.

Parameters:
  • tl – top left x and y coordinates.

  • br – bottom right x and y coordinates.

  • nx – number of x positions to explore.

  • ny – number of y positions to explore.

  • z – z position of the probe.

  • shuffle_lines – shuffles the lines’ order

grid_around(p, width, nx, ny)

Grid coordinates around a given center

now_str()

Return current date in string format.

This is the conventional datetime representation used across this library.