How to design a new experiment

So you want to design a new experiment, i.e. to define a bunch of actions to be executed in the proper order and what happen if one fails.

An experiment is described in a TOML file and must contain the following sections:

Optionally, a constants section may be added.

We will go through all sections to design a toy experiment that will do the following thing:

  • 3 processes: reset, configure, execute;
  • the goal is to encrypt random plaintexts with AES128 on a STM32 microcontroller and to verify that the output is correct.
  • we save the plaintexts, the experimental ciphertexts and the ciphertexts diffs.

Global data

First, before any section, you can define some data to be used later on in the config file. Let's hardcode our key:

key = "000102030405060708090a0b0c0d0e0f"

Modules

A module is a component that may hold a state and provides commands. The list of modules is present here.

The next step to design an experiment is to define all required modules. If you need to define a new toml_device module, you can read the corresponding how to. We need:

  • a plaintexts generator,
  • a stm32 module for the encrypting device
  • a AES computer
  • a diff to compare experimental and computed ciphertexts
  • savers for plaintexts, experimental ciphertexts and difsfs
  • a module to display the diffs in the console
[modules]
  [modules.stm32]
    type = "toml_device"
    toml_files = ["STM32VLDISCOVERY", "AES128", "CIPHER_PROTOCOL1", "OPENOCD"]

The first module will be named stm32 (arbitrary name that will be used in this experiment). This module is a toml_device module. This module is described by the set of files (do not specify the toml extension) defined above (all files are concatenated as one single TOML file describing what a stm32 is).

[modules.text_gen]
  type = "vec_u8_generator"
  generator_type = "random"
  len = 16
  output_name = "plaintext"

The text_gen module is used to generate random 16-bytes plaintexts (cf vec_u8_generator). The output variable will be named "plaintext".

[modules.aes]
  type = "aes"
  output_name = "cipher_computed"

[modules.diff_ciphers]
  type = "diff"

[modules.console_diff]
  type = "console_display"
  text = "Diff: "

Here we have created modules to compute the AES results on the host computer, to compute the difference (XOR) with the experimental ciphertexts and to display that difference. See the doc for each module given by the type string. We name the output of the aes module "cipher_computed" to differentiate with "ciphertext" which is the variable name of the experimental ciphertext.

Finally the modules to save data to file.

[modules.plaintext_saver]
  type = "text_saver"
  path = "plaintexts.hex"
  creation_mode = "overwrite"

[modules.ciphertext_saver]
  type = "text_saver"
  path = "ciphertexts.hex"
  creation_mode = "overwrite"

[modules.diff_saver]
  type = "text_saver"
  path = "diff.hex"
  creation_mode = "overwrite"

This is rather self-explanatory, see doc of text_saver module if needed.

Actors

The next section is the actors section. Actors are instances of a command for a given module.

Example:

validate = ["stm32", "test"]

The actor name validate is an instance of the test command (defined in STM32VLDISCOVERY.toml) of the stm32 module. Additionally, optional attributes can be attached to actors to modify their behavior. Some attributes are generic, available for all actors, such a pre and post temporisations. Other are specific to some commands of some module, e.g. diff command for diff modules allows a not_zero attribute that prevents outputing a result if the diff is zero.

Actors can be reused across processes be an actor can be used only once in each process (ensure DAG property). If you want to call the same command from the same module twice during one execution, simply create two actors with the same bindings (attributes may be different).

The actors for our experiment are the following:

[actors]
  validate = ["stm32", "test"]
  set_key = ["stm32", "set_key"]
  encrypt = ["stm32", "fast"]#, "post_tempo=100ms"]
  generate_pt = ["text_gen", "gen"]
  stm32_welcome = ["stm32", "welcome"]
  stm32_reset = ["stm32", "reset"]
  save_plaintext = ["plaintext_saver", "save"]
  save_ciphertext = ["ciphertext_saver", "save"]
  save_diff = ["diff_saver", "save", "sync_timeout=10ks"]
  compute_cipher = ["aes", "encrypt"]
  diff_ciphers = ["diff_ciphers", "diff", "not_zero"]
  show_diff = ["console_diff", "display", "sync_timeout=10ks"]

The generic attributes sync_timeout is the duration whithin an actor must be fed a data. The default value is 120s. Since the diff do not produce data if zero, the following actors may not receive any data in that sync_timeout window. To avoid the resulting error, we increase this default value to 10 000s (~3h).

These actors are all the one required for all our processes.

Processes

Next we can define our 3 processes.

Reset process

The reset process has 3 steps:

  • use openocd (jtag) to reset the chip.
  • wait for the welcome message
  • test the connection

This gives:

[processes]
   [processes.reset]
    process = ["stm32_reset", "stm32_welcome", "validate"]
    [processes.reset.links]
      reset2welcome = ["stm32_reset", "stm32_welcome"]
      welcome2validate = ["stm32_welcome", "validate"]

The "process" key must hold all actors required for this process (nodes of our process DAG). Then the links are defined (edges of our DAG). Here no data flow between the different actors, yet we need to enforce an actor execution order (reset before welcome and welcome before validate). So we create to synchronization (sync) edges: reset2welcome and welcome2validate.

Normally a link is of the form:

link_name = ["source actor", "source variable", "destination actor", "destination variable"]

But for synchronization, there is a special variable called sync used to denote precedence without data flowing. Effectively,

link_name = ["source actor", "destination actor"]

is syntactic sugar (and strictly equivalent) to

link_name = ["source actor", "sync", "destination actor", "sync"]

Without the links defined here, all actors would have been executed in parallel, thus issuing an error.

Configure process

The configure process is used to set the cryptographic key into the device before the execution.

  [processes.configure]
    process = ["set_key"]

Hard to make it simpler.

Execute process

The execute process is the following:

[processes.execute]
  process = ["generate_pt", "encrypt", "save_plaintext", "save_ciphertext", "compute_cipher", "diff_ciphers", "show_diff", "save_diff"]
  count = 10
  resume_kind = "resume_skip"
  # autolinks = false
  [processes.execute.links]
    plain_save = ["generate_pt", "plaintext", "save_plaintext", "input"]
    ciph_save = ["encrypt", "ciphertext", "save_ciphertext", "input"]
    true_ciph2diff = ["compute_cipher", "cipher_computed", "diff_ciphers", "input1"]
    exp_ciph2diff = ["encrypt", "ciphertext", "diff_ciphers", "input2"]
    diff2display = ["diff_ciphers", "output", "show_diff", "input"]
    diff2save = ["diff_ciphers", "output", "save_diff", "input"]

The most complex process in this experiment. Please write down the resulting DAG as an exercise. Here again, link names have no other purpose than documentation. count says that the described process will be executed 10 times. resume_kind says how to restart the proces (e.g. in case of failure due to a fault).

Links

You may have noticed that some edges are missing. This is because we are using the autolinks feature. Autolinks use variable names to automatically infer (if possible) links between producer actors and consumer actors. In our example, notably, their is no explicit link between the plaintext generator (generate_pt) and the encryption actor (encrypt). It is so because generate_pt produces a plaintext variable, and encrypt requires a plaintext variable: the link has been inferred.

If several actors produce a variable with the same name, no link is automatically created for this variable.

If you want to disable the autolinks feature, you can do so with

autolinks = false

and manually define ALL links.

Constants

The key is a constant defined in our experiment. We do not want to create a module and an actor for that. We can define constants instead. Constants are defined outside the processes section, into their own section. They refer to one actor and apply accross processes if that actor is called several times.

The syntax is:

[constants.actor_name]
  variable_name = "constant_value"
[constants]
  [constants.compute_cipher]
    key = "$(key:hexstring)"

  [constants.set_key]
    key = "$(key:hexstring)"

Feedx

Finally, we would like to always save the exact same amount of plaintexts and ciphertexts even in the case where an error occurs. Since actors in a process are executed asynchronously, we need to constrain the savers with a synchronization trick.

For that purposes we use the feedx feature (feedback, feedfront).

  [processes.execute.feedx]
    #synchronization to correctly save data when there is an error
    save1 = ["save_ciphertext", "front", "save_plaintext", "front"]
    save2 = ["save_plaintext", "front", "save_ciphertext", "front"]

The front point is reached when an actor received has been fed data but command execution has not yet occured. The back point is reached after command execution.

In our example, save1 define a feedx lock for save_plaintext. The front lock is before command execution, so no plaintexts will be saved before the lock is lifted by save_ciphertext (which happens when save_ciphertext has been fed data). Similarly for save2. In the end, saving will occurs only when the 2 actors have data available to save at input.

Experiment

Now that all 3 processes have been defined, we can describe how they are ordered.

[experiment]
  entry = ["reset", ""]#special proc name: say where to start experiment
  reset = ["configure", ""]
  configure = ["execute", ""]
  execute = ["", "reset"]

The syntax for a process choice is:

process_name = ["next proc if success", "next proc if failure"]

entry is the virtual process name the experiment starts with. If a "" process is reached, the experiment terminates. We have defined the following experiment:

  • start with reset process,
  • if succeed, go to configure process, if not terminates. If reset is called again, start this process over.
  • if configure succeeds, go to execute, if not terminates.
  • if execute succeeds, terminates, if not go to reset. If execute is called again (e.g. after an error), start from where it was last time.

Whole experiment file

Finally, the whole experiment file is:

key = "000102030405060708090a0b0c0d0e0f"
[modules]
  [modules.stm32]
    type = "toml_device"
    toml_files = ["STM32VLDISCOVERY", "AES128", "CIPHER_PROTOCOL1", "OPENOCD"]

  [modules.text_gen]
    type = "vec_u8_generator"
    generator_type = "random"
    len = 16
    output_name = "plaintext"

  [modules.plaintext_saver]
    type = "text_saver"
    path = "plaintexts.hex"
    creation_mode = "overwrite"

  [modules.ciphertext_saver]
    type = "text_saver"
    path = "ciphertexts.hex"
    creation_mode = "overwrite"

  [modules.diff_saver]
    type = "text_saver"
    path = "diff.hex"
    creation_mode = "overwrite"

  [modules.aes]
    type = "aes"
    output_name = "cipher_computed"

  [modules.diff_ciphers]
    type = "diff"

  [modules.console_diff]
    type = "console_display"
    text = "Diff: "

[actors]
  validate = ["stm32", "test"]
  set_key = ["stm32", "set_key"]
  encrypt = ["stm32", "fast"]#, "post_tempo=100ms"]
  generate_pt = ["text_gen", "gen"]
  stm32_welcome = ["stm32", "welcome"]
  stm32_reset = ["stm32", "reset"]
  save_plaintext = ["plaintext_saver", "save"]
  save_ciphertext = ["ciphertext_saver", "save"]
  save_diff = ["diff_saver", "save", "sync_timeout=10ks"]
  compute_cipher = ["aes", "encrypt"]
  diff_ciphers = ["diff_ciphers", "diff", "not_zero"]
  show_diff = ["console_diff", "display", "sync_timeout=10ks"]

[constants]
  [constants.compute_cipher]
    key = "$(key:hexstring)"

  [constants.set_key]
    key = "$(key:hexstring)"


[processes]
   [processes.reset]
    process = ["stm32_reset", "stm32_welcome", "validate"]
    [processes.reset.links]
      reset2welcome = ["stm32_reset", "stm32_welcome"]
      welcome2validate = ["stm32_welcome", "validate"]

  [processes.configure]
    process = ["set_key"]
    [processes.configure.links]

  [processes.execute]
    process = ["generate_pt", "encrypt", "save_plaintext", "save_ciphertext", "compute_cipher", "diff_ciphers", "show_diff", "save_diff"]
    count = 10
    resume_kind = "resume_skip"
    # autolinks = false
    [processes.execute.links]
      plain_save = ["generate_pt", "plaintext", "save_plaintext", "input"]
      ciph_save = ["encrypt", "ciphertext", "save_ciphertext", "input"]
      true_ciph2diff = ["compute_cipher", "cipher_computed", "diff_ciphers", "input1"]
      exp_ciph2diff = ["encrypt", "ciphertext", "diff_ciphers", "input2"]
      diff2display = ["diff_ciphers", "output", "show_diff", "input"]
      diff2save = ["diff_ciphers", "output", "save_diff", "input"]
    [processes.execute.feedx]
      #synchronization to correctly save data when there is an error
      save1 = ["save_ciphertext", "front", "save_plaintext", "front"]
      save2 = ["save_plaintext", "front", "save_ciphertext", "front"]

[experiment]
  entry = ["reset", ""]#special proc name: say where to start experiment
  reset = ["configure", ""]
  configure = ["execute", ""]
  execute = ["", "reset"]