Tutorial

In general, there are two steps for evaluating a device’s leakage using TVLA. The first is capturing data and the second is evaluating that data.

Acquiring Data

Non-Specific Tests

For non-specific TVLA tests, capture is broken into two campaigns. During analysis, the data from these two campaigns will be used in a t-test. It’s recommended that data acquisistion be done simultaneously for these data sets - aka swap back and forth between campaigns instead of doing them in sequence.

Begin by creating a key text pair object:

import cwtvla
ktp = cwtvla.FixedVRandomText(key_len=16)

The dataset for each campaign can be accessed individually:

key, text = ktp.next_group_A() # fixed key, fixed text for FixedVRandomText
key, text = ktp.next_group_B() # fixed key, random text for FixedVRandomText

From there, it’s as simple as a normal capture campaign. Record the trace wave data from each campaign and keep them separate. There’s no need to record plaintext, key, or ciphertext, but it’s recommended that you validate the ciphertext that you get back from the target device every time you capture a trace:

cwtvla.verify_AES(plaintext, key, ciphertext)

If you’re using a ChipWhisperer device, a full capture campaign (minus error checking) might look like:

import cwtvla
import chipwhisperer as cw
import numpy as np
ktp = cwtvla.FixedVRandomText(key_len=16)

scope = cw.scope()
target = cw.scope(target)
scope.default_setup()
cw.program_target(scope, cw.programmers.STM32FProgrammer, "fw.hex")

N = 10000 # capture 10000 traces for each group
groupA = np.zeros((N, scope.adc.samples), dtype='float64')
groupB = np.zeros((N, scope.adc.samples), dtype='float64')

for i in range(N):
    key, text = ktp.next_group_A() # fixed key, fixed text for FixedVRandomText
    trace = cw.capture_trace(scope, target, text, key)
    groupA[i,:] = trace.wave[:]

    key, text = ktp.next_group_B() # fixed key, random text for FixedVRandomText
    trace = cw.capture_trace(scope, target, text, key)
    groupB[i,:] = trace.wave[:]

Adapt the above to your capture setup.

Specific Tests

Data acquisistion for specific tests is similar except only a single campaign is needed. For this data, use the group_B key/text pair from the FixedVRandomText ktp and record both trace wave data and plaintext.:

import cwtvla
import chipwhisperer as cw
import numpy as np
ktp = cwtvla.FixedVRandomText(key_len=16)

scope = cw.scope()
target = cw.scope(target)
scope.default_setup()
cw.program_target(scope, cw.programmers.STM32FProgrammer, "fw.hex")

N = 10000 # capture 10000 traces for each group
waves = np.zeros((N, scope.adc.samples), dtype='float64')
textins = np.zeros((N, 16), dtype='uint8')

for i in range(N):
    key, text = ktp.next_group_B() # fixed key, fixed text for FixedVRandomText
    trace = cw.capture_trace(scope, target, text, key)
    waves[i,:] = trace.wave[:]
    textins[i,:] = np.array(text)[:]

Evaluating Data

cwtvla expects numpy arrays in the following formats:

trace_data = np.array(shape=(num_traces, trace_len), dtype='float64')
textin_data = np.array(shape=(num_traces, 16), dtype='uint8')

Non-Specific Tests

Evaluating data from non-specific tests is simple, provided your trace data is in numpy arrays:

t_val = cwtvla.t_test(groupA, groupB)
fail_points = cwtvla.check_t_test(t_val)
if len(fail_points) > 0:
    print("Test failed at: {}".format(fail_points))

Specific Tests

As a part of the evaluation process for specific tests, trace data must be separated by the value of intermediates in the AES state. Two common ways on doing this are via bit values, and via byte values. cwtvla includes a generic function for building your own separator by both bit and byte. For example, to separate by the value of the 0th bit, 0th byte of the second SBox output:

func = lambda text: cwtvla.leakage_func_bit(text, 0, 0, ktp._dev_cipher, cwtvla.leakage_lookup("subbytes", 2), 0)
truth_array = np.array([func(textins[i], ktp._cipher_dev) for i in range(len(waves))])
groupA = waves[truth_array != 0]
groupB = waves[truth_array == 0]

From there, you can do a t-test as normal:

t_val = cwtvla.t_test(groupA, groupB)
fail_points = cwtvla.check_t_test(t_val)
if len(fail_points) > 0:
    print("Test failed at: {}".format(fail_points))

cwtvla has a generic specific evaluation function to automate scanning over a range of AES rounds, bytes, and bits, as well as some common leakage points to evaluate:

eval_rand_v_rand(waves, textins, func=cwtvla.sbox_hw)

By default, eval_rand_v_rand() tests over the full leakage search space (from round 2 to the last round, 16 bytes, 8 bits). You can customize the search space as follows, attacking rounds 2-4, bytes 5 and 6, bits 2 and 7:

eval_rand_v_rand(waves, textins, func, round_range=[2,3,4], byte_range=[5,6], bit_range=[2,7])

Here func has the following function prototype:

func(text: list, byte: uint8, bit: uint8, cipher: AESCipher, rnd: uint8) -> bool

To make it easier to generate leakage functions, you can use the function constructors construct_leakage_bit and construct_leakage_byte:

func = cwtvla.construct_leakage_bit("addroundkey", "subbytes")

ChipWhisperer Convenience

cwtvla, includes an additional submodule to automate data collection with ChipWhisperer scopes and targets. To setup and program a scope and target:

scope, target = setup_device("STM32F3") # STM32F3 TINYAES

You can then either do a full capture run, putting the data in a ChipWhisperer zarr:

import cwtvla.cw_convenience as conv
z = conv.capture_all(scope, target, "STM32F3")

Or do tests individually, which return numpy arrays:

group1, group2 = conv.capture_non_specific(scope, target, cwtvla.FixedVRandomText)
waves, textins = conv.capture_rand(scope, target)

ChipWhisperer zarr containers have a tree in the following format:

/
├── PLATFORM_A
|   ├── FixedVRandomKey-KEY_LEN
|   │   ├── results
|   │   │   └── tvla (2, scope.adc.samples) float64
|   │   └── traces
|   │       ├── group1 (N, scope.adc.samples) float64
|   │       └── group2 (N, scope.adc.samples) float64
|   ├── FixedVRandomText-KEY_LEN
|   │   ├── results
|   │   │   └── tvla (2, scope.adc.samples) float64
|   │   └── traces
|   │       ├── group1 (N, scope.adc.samples) float64
|   │       └── group2 (N, scope.adc.samples) float64
|   ├── RandVRand-KEY_LEN
|   │   └── traces
|   │       ├── textins (N, 16) uint8
|   │       └── waves (N, scope.adc.samples) float64
|   └── SemiFixedVRandomText-KEY_LEN
|       ├── results
|       │   └── tvla (2, scope.adc.samples) float64
|       └── traces
|           ├── group1 (N, scope.adc.samples) float64
|           └── group2 (N, scope.adc.samples) float64
|
├── PLATFORM_B
.
.
.