Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[capture] Add drastically simplified capture script as template for post-si testing #175

Merged
merged 1 commit into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions cw/simple_capture_aes_sca.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
#!/usr/bin/env python3
# Copyright lowRISC contributors.
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0
import binascii
import random
import signal
import sys
import time
from datetime import datetime
from functools import partial
from pathlib import Path

import chipwhisperer as cw
import numpy as np
import yaml
from Crypto.Cipher import AES
from tqdm import tqdm

from util import device, plot, trace_util


def abort_handler_during_loop(project, sig, frame):
# Handler for ctrl-c keyboard interrupts
# TODO: Has to be modified according to database (i.e. CW project atm) used
if project is not None:
print("\nHandling keyboard interrupt")
project.close(save=True)
sys.exit(0)


if __name__ == '__main__':
# Load configuration from file
with open('simple_capture_aes_sca.yaml') as f:
cfg = yaml.load(f, Loader=yaml.FullLoader)

# Key and plaintext generation building blocks
# Generate key at random based on test_random_seed. TODO: not used atm
random.seed(cfg["test"]["test_random_seed"])
key = bytearray(cfg["test"]["key_len_bytes"])
for i in range(0, cfg["test"]["key_len_bytes"]):
key[i] = random.randint(0, 255)
# Load initial values for key and text from cfg
key = bytearray(cfg["test"]["key"])
print(f'Using key: {binascii.b2a_hex(bytes(key))}')
text = bytearray(cfg["test"]["text"])
# Generating new texts/keys by encrypting using key_for_generation
key_for_gen = bytearray(cfg["test"]["key_for_gen"])
cipher_gen = AES.new(bytes(key_for_gen), AES.MODE_ECB)

# Cipher to generate expected responses
cipher = AES.new(bytes(key), AES.MODE_ECB)

# Create OpenTitan encapsulating ChipWhisperer Husky and FPGA
# NOTE: Johann tried to split them up into classes,
# BUT scope needs FPGA (PLL?) to be configured
# and target constructor needs scope as input.
# A clean separation seems infeasible.
cwfpgahusky = device.OpenTitan(cfg["cwfpgahusky"]["fpga_bitstream"],
cfg["cwfpgahusky"]["force_program_bitstream"],
cfg["cwfpgahusky"]["fw_bin"],
cfg["cwfpgahusky"]["pll_frequency"],
cfg["cwfpgahusky"]["baudrate"],
cfg["cwfpgahusky"]["scope_gain"],
cfg["cwfpgahusky"]["num_samples"],
cfg["cwfpgahusky"]["offset"],
cfg["cwfpgahusky"]["output_len_bytes"])

# Create ChipWhisperer project for storage of traces and metadata
project = cw.create_project(cfg["capture"]["project_name"], overwrite=True)

# Register ctrl-c handler to store traces on abort
signal.signal(signal.SIGINT, partial(abort_handler_during_loop, project))

# Set key
cwfpgahusky.target.simpleserial_write("k", key)
# TODO: Alternative line: cwfpgahusky.target.set_key(key, ack=False)

# Main loop for measurements with progress bar
for _ in tqdm(range(cfg["capture"]["num_traces"]), desc='Capturing', ncols=80):

# Generate and load new text
text = bytearray(cipher_gen.encrypt(text))
cwfpgahusky.target.simpleserial_write('p', text)

# TODO: Useful code line for batch capture
# cwfpgahusky..simpleserial_write("s", capture_cfg["batch_prng_seed"].to_bytes(4, "little"))

# Arm scope
cwfpgahusky.scope.arm()

# Capture trace
ret = cwfpgahusky.scope.capture(poll_done=False)
i = 0
while not cwfpgahusky.target.is_done():
i += 1
time.sleep(0.05)
if i > 100:
print("Warning: Target did not finish operation")
if ret:
print("Warning: Timeout happened during capture")

# Get response and verify
response = cwfpgahusky.target.simpleserial_read('r',
cwfpgahusky.target.output_len, ack=False)
if binascii.b2a_hex(response) != binascii.b2a_hex(cipher.encrypt(bytes(text))):
raise RuntimeError(f'Bad ciphertext: {response} != {cipher.encrypt(bytes(text))}.')

# Get trace
wave = cwfpgahusky.scope.get_last_trace(as_int=True)
if len(wave) >= 1:
trace = cw.Trace(wave, text, response, key)
else:
raise RuntimeError('Capture failed.')

# TODO: Useful code line for batch capture
# waves = scope.capture_and_transfer_waves()

# Check if ADC range has been exceeded
trace_util.check_range(trace.wave, cwfpgahusky.scope.adc.bits_per_sample)

# Append traces to storage
project.traces.append(trace, dtype=np.uint16)

# Save metadata and entire configuration cfg to project file
project.settingsDict['datetime'] = datetime.now().strftime("%m/%d/%Y, %H:%M:%S")
project.settingsDict['cfg'] = cfg
sample_rate = int(round(cwfpgahusky.scope.clock.adc_freq, -6))
project.settingsDict['sample_rate'] = sample_rate
project.save()

# Create and show test plot
if cfg["capture"]["show_plot"]:
plot.save_plot_to_file(project.waves, None, cfg["capture"]["plot_traces"],
cfg["capture"]["trace_image_filename"])
print(f'Created plot with {cfg["capture"]["plot_traces"]} traces: '
f'{Path(cfg["capture"]["trace_image_filename"]).resolve()}')
23 changes: 23 additions & 0 deletions cw/simple_capture_aes_sca.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
cwfpgahusky:
fpga_bitstream: "objs/lowrisc_systems_chip_earlgrey_cw310_0.1.bit"
force_program_bitstream: False
fw_bin: "objs/aes_serial_fpga_cw310.bin"
baudrate: 115200
output_len_bytes: 16
pll_frequency: 100000000
num_samples: 1200
offset: -40
scope_gain: 31.5
test:
key_len_bytes: 16
test_random_seed: 0
key: [0x81, 0x1E, 0x37, 0x31, 0xB0, 0x12, 0x0A, 0x78, 0x42, 0x78, 0x1E, 0x22, 0xB2, 0x5C, 0xDD, 0xF9]
text: [0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA]
key_for_gen: [0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF1, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xE0, 0xF0]
capture:
num_traces: 1000
project_name: "projects/simple_capture_aes_sca"
show_plot: True
plot_traces: 100
trace_image_filename: "projects/simple_capture_aes_sca_sample_traces.html"

18 changes: 18 additions & 0 deletions cw/util/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,16 @@ def __init__(self, bitstream, force_programming, firmware, pll_frequency,
# TODO: Remove these comments after discussion
self.fpga = self.initialize_fpga(fpga, bitstream, force_programming,
pll_frequency)

self.scope = self.initialize_scope(scope_gain, num_samples, offset,
pll_frequency)
print(f'Scope setup with sampling rate {self.scope.clock.adc_freq} S/s')

self.target = self.initialize_target(programmer, firmware, baudrate,
output_len, pll_frequency)

self._test_read_version_from_target()

def initialize_fpga(self, fpga, bitstream, force_programming,
pll_frequency):
"""Initializes FPGA bitstream and sets PLL frequency."""
Expand Down Expand Up @@ -195,6 +200,19 @@ def initialize_target(self, programmer, firmware, baudrate, output_len,

return target

def _test_read_version_from_target(self):
version = None
ping_cnt = 0
while not version:
if ping_cnt == 3:
raise RuntimeError(
f'No response from the target (attempts: {ping_cnt}).')
self.target.write('v' + '\n')
ping_cnt += 1
time.sleep(0.5)
version = self.target.read().strip()
print(f'Target simpleserial version: {version} (attempts: {ping_cnt}).')

def program_target(self, fw, pll_frequency=100e6):
"""Loads firmware image """
programmer1 = SpiProgrammer(self.fpga)
Expand Down
18 changes: 18 additions & 0 deletions cw/util/trace_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright lowRISC contributors.
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0

import numpy as np


def check_range(waves, bits_per_sample):
""" The ADC output is in the interval [0, 2**bits_per_sample-1]. Check that the recorded
traces are within [1, 2**bits_per_sample-2] to ensure the ADC doesn't saturate. """
adc_range = np.array([0, 2**bits_per_sample])
if not (np.all(np.greater(waves[:], adc_range[0])) and
np.all(np.less(waves[:], adc_range[1] - 1))):
print('\nWARNING: Some samples are outside the range [' +
str(adc_range[0] + 1) + ', ' + str(adc_range[1] - 2) + '].')
print('The ADC has a max range of [' +
str(adc_range[0]) + ', ' + str(adc_range[1] - 1) + '] and might saturate.')
print('It is recommended to reduce the scope gain.')