diff --git a/analysis/tvla.py b/analysis/tvla.py index 228815774..1b20ed9ee 100755 --- a/analysis/tvla.py +++ b/analysis/tvla.py @@ -11,7 +11,6 @@ from pathlib import Path from types import SimpleNamespace -import chipwhisperer as cw import matplotlib.pyplot as plt import numpy as np import typer @@ -21,7 +20,8 @@ ABS_PATH = os.path.dirname(os.path.abspath(__file__)) sys.path.append(ABS_PATH + '/..') import util.plot as plot # noqa : E402 -from capture.project_library.trace_library import TraceLibrary # noqa : E402 +from capture.project_library.project import ProjectConfig # noqa : E402 +from capture.project_library.project import SCAProject # noqa : E402 from util.leakage_models import compute_leakage_aes # noqa : E402 from util.leakage_models import compute_leakage_general # noqa : E402 from util.leakage_models import find_fixed_key # noqa : E402 @@ -270,14 +270,20 @@ def run_tvla(ctx: typer.Context): # Either don't have previously generated histograms or we need to append previously # generated histograms. # Make sure the project file is compatible with the previously generated histograms. - if OTTraceLib: - project = TraceLibrary(cfg["project_file"], cfg["trace_threshold"], - wave_datatype = np.uint16, - overwrite = False) - proj_waves = project.get_waves() - else: - project = cw.open_project(cfg["project_file"]) - proj_waves = project.waves + project_type = cfg.get("trace_db") + if not project_type: + project_type = "cw" + + project_cfg = ProjectConfig(type = project_type, + path = cfg["project_file"], + wave_dtype = np.uint16, + overwrite = False, + trace_threshold = cfg.get("trace_threshold") + ) + project = SCAProject(project_cfg) + project.open_project() + proj_waves = project.get_waves() + metadata = project.get_metadata() if cfg["input_histogram_file"] is None: num_samples = len(proj_waves[0]) @@ -367,8 +373,8 @@ def run_tvla(ctx: typer.Context): # the operation to free up some memory. if i_step > 0: if not OTTraceLib: - project = cw.open_project(cfg["project_file"]) - proj_waves = project.waves + project.open_project() + proj_waves = project.get_waves() # Converting traces from floating point to integer and creating a dense copy. log.info("Converting Traces") @@ -454,27 +460,24 @@ def run_tvla(ctx: typer.Context): keys = np.empty((num_traces_orig, 16), dtype=np.uint8) if general_test is False: - if OTTraceLib: - keys[:] = project.get_keys_int(trace_start, trace_end + 1) - else: - keys[:] = project.keys[trace_start:trace_end + 1] + keys[:] = project.get_keys()[trace_start:trace_end + 1] else: # Existing KMAC trace sets use a mix of bytes strings and ChipWhisperer byte # arrays. For compatiblity, we need to convert everything to numpy arrays. # Eventually, we can drop this. if i_step == 0: if OTTraceLib: - keys_nparrays = project.keys() + keys_nparrays = project.get_keys() else: # Convert all keys from the project file to numpy # arrays once. keys_nparrays = [] for i in range(num_traces_max): if cfg["mode"] == "sha3": - keys_nparrays.append(np.frombuffer(project.textins[i], + keys_nparrays.append(np.frombuffer(project.project.textins[i], dtype=np.uint8)) else: - keys_nparrays.append(np.frombuffer(project.keys[i], + keys_nparrays.append(np.frombuffer(project.project.keys[i], dtype=np.uint8)) # Select the correct slice of keys for each step. @@ -486,9 +489,9 @@ def run_tvla(ctx: typer.Context): # The plaintexts are only required for non-general AES TVLA. plaintexts = np.empty((num_traces_orig, 16), dtype=np.uint8) if OTTraceLib: - plaintexts = project.get_plaintexts_int(trace_start, trace_end + 1) + plaintexts = project.get_plaintexts(trace_start, trace_end + 1) else: - plaintexts[:] = project.textins[trace_start:trace_end + 1] + plaintexts[:] = project.project.textins[trace_start:trace_end + 1] plaintexts = plaintexts[traces_to_use[trace_start:trace_end + 1]] # We don't need the project file anymore after this point. Close it together with all @@ -699,39 +702,39 @@ def run_tvla(ctx: typer.Context): if not OTTraceLib: # Catch case where certain metadata isn't saved to project file (e.g. older measurement) try: - pll_freq = float(project.config['ChipWhisperer'] + pll_freq = float(metadata['ChipWhisperer'] ['General Settings']['pll_frequency']) / 1e6 textbox = textbox + "PLL:\n" + str(pll_freq) + " MHz\n\n" except KeyError: textbox = textbox try: - pll_freq = float(project.config['ChipWhisperer'] + pll_freq = float(metadata['ChipWhisperer'] ['General Settings']['sample_rate']) / 1e6 textbox = textbox + "ADC:\n" + str(pll_freq) + " MS/s\n\n" except KeyError: textbox = textbox try: - textbox = textbox + "Masks off:\n" + project.config[ + textbox = textbox + "Masks off:\n" + metadata[ 'ChipWhisperer']['General Settings']['masks_off'] + "\n\n" except KeyError: textbox = textbox try: - textbox = textbox + "Samples:\n" + project.config['ChipWhisperer'][ + textbox = textbox + "Samples:\n" + metadata['ChipWhisperer'][ 'General Settings']['num_samples'] + "\n\n" except KeyError: textbox = textbox try: - textbox = textbox + "Offset:\n" + project.config['ChipWhisperer'][ + textbox = textbox + "Offset:\n" + metadata['ChipWhisperer'][ 'General Settings']['offset'] + "\n\n" except KeyError: textbox = textbox try: - textbox = textbox + "Scope gain:\n" + project.config[ + textbox = textbox + "Scope gain:\n" + metadata[ 'ChipWhisperer']['General Settings']['scope_gain'] + "\n\n" except KeyError: textbox = textbox try: - textbox = textbox + "Traces:\n" + project.config['ChipWhisperer'][ + textbox = textbox + "Traces:\n" + metadata['ChipWhisperer'][ 'General Settings']['num_traces'] + "\n\n" except KeyError: textbox = textbox diff --git a/capture/README.md b/capture/README.md new file mode 100644 index 000000000..d7af54dd0 --- /dev/null +++ b/capture/README.md @@ -0,0 +1,44 @@ +# OT-SCA Capture Scripts + +The `caputure_*.py` scripts propvided in this directory allow the user to +capture traces of a cryptographic operation performed by OpenTitan. + +## Quick Usage + +Follow the steps described in the [`getting_started`](../doc/getting_started.md) +documentation to setup the environment, the target, and the scope. + +When using OpenTitan on the CW310 and measuring traces using Husky, AES traces +for a random key tests can be captured with the following command: +```console +mkdir -p projects/ +./capture_aes.py --capture_config config/aes_sca_cw310.yaml --project projects/aes_sca_cw310_random +``` +The traces are then stored in the database file in `projects/`. + +## Capture Config + +The capture configs stored in `configs/` follow the following structure: +- target +- scope type (husky or waverunner) +- capture +- test + +The target entry specifies the target. Currently, only the `cw310` or `cw305` +FGPA boards are supported. The scope entry defines (indirectly) the samling +rate as well as the scope gain and cycle offset. With the capture entry, the +user can select the corresponding scope, configure the trace plot, and select +the trace database format (either chipwhisperer project or OT trace library in +the SQLite format). Test provides different entries for the cipher config. + +## Adding new Cipher Capture Scripts + +To add new cipher capture scripts, please use the `caputure_aes.py` script as +a template. The template follows the following structure: +- Setup the target, scope, and project +- Configure the cipher and establish the communication interface +- Capture traces +- Print traces + +The communication interface (e.g., the individual simpleserial commands) for +each cipher needs to be added to `lib/ot_communication.py`. diff --git a/capture/capture_aes.py b/capture/capture_aes.py index 58349cbac..bdbb50d8e 100755 --- a/capture/capture_aes.py +++ b/capture/capture_aes.py @@ -2,19 +2,23 @@ # Copyright lowRISC contributors. # Licensed under the Apache License, Version 2.0, see LICENSE for details. # SPDX-License-Identifier: Apache-2.0 + import binascii +import logging import random import signal import sys +from dataclasses import dataclass from datetime import datetime from functools import partial from pathlib import Path -import chipwhisperer as cw +import lib.helpers as helpers import numpy as np import yaml from Crypto.Cipher import AES -from project_library.trace_library import Metadata, Trace, TraceLibrary +from lib.ot_communication import OTAES +from project_library.project import ProjectConfig, SCAProject from scopes.scope import Scope, ScopeConfig from tqdm import tqdm @@ -22,54 +26,57 @@ from target.cw_fpga import CWFPGA # noqa: E402 from util import plot # noqa: E402 +logger = logging.getLogger() + -def abort_handler_during_loop(this_project, CWLib, sig, frame): +def abort_handler_during_loop(this_project, sig, frame): # Handler for ctrl-c keyboard interrupts + # FIXME: Has to be modified according to database (i.e. CW project atm) used if this_project is not None: - print("\nHandling keyboard interrupt") - if CWLib: - this_project.close(save=True) - else: - this_project.flush_to_disk() - + logger.info("\nHandling keyboard interrupt") + this_project.close(save=True) sys.exit(0) -if __name__ == '__main__': - now = datetime.now() - cfg_file = sys.argv[1] - # Load configuration from file - with open(cfg_file) as f: - cfg = yaml.load(f, Loader=yaml.FullLoader) - # Determine trace library - CWLib = True - OTTraceLib = False - if cfg['capture'].get('trace_db') == 'ot_trace_library': - CWLib = False - OTTraceLib = True - - if CWLib: - # Create ChipWhisperer project for storage of traces and metadata ------ - project = cw.create_project(cfg["capture"]["project_name"], overwrite=True) - else: - # Create Trace Database------------------------------------------------- - project = TraceLibrary(cfg["capture"]["project_name"], - cfg["capture"]["trace_threshold"], - wave_datatype=np.uint16, - overwrite=True) - - # Init target - cw_target = CWFPGA( - bitstream = cfg["cwfpga"]["fpga_bitstream"], - force_programming = cfg["cwfpga"]["force_program_bitstream"], - firmware = cfg["cwfpga"]["fw_bin"], - pll_frequency = cfg["cwfpga"]["pll_frequency"], - baudrate = cfg["cwfpga"]["baudrate"], - output_len = cfg["cwfpga"]["output_len_bytes"], +@dataclass +class CaptureConfig: + """ Configuration class for the current capture. + """ + capture_mode: str + batch_mode: bool + num_traces: int + num_segments: int + output_len: int + text_fixed: bytearray + key_fixed: bytearray + key_len_bytes: int + text_len_bytes: int + + +def setup(cfg: dict, project: Path): + """Setup target, scope, and project. + + Args: + cfg: The configuration for the current experiment. + project: The path for the project file. + + Returns: + The target, scope, and project. + """ + # Init target. + logger.info(f"Initializing target {cfg['target']['target_type']} ...") + target = CWFPGA( + bitstream = cfg["target"]["fpga_bitstream"], + force_programming = cfg["target"]["force_program_bitstream"], + firmware = cfg["target"]["fw_bin"], + pll_frequency = cfg["target"]["pll_frequency"], + baudrate = cfg["target"]["baudrate"], + output_len = cfg["target"]["output_len_bytes"], ) - # Init scope + # Init scope. scope_type = cfg["capture"]["scope_select"] + logger.info(f"Initializing scope {scope_type} ...") scope_cfg = ScopeConfig( scope_type = scope_type, acqu_channel = cfg[scope_type].get("channel"), @@ -82,190 +89,292 @@ def abort_handler_during_loop(this_project, CWLib, sig, frame): num_segments = cfg[scope_type]["num_segments"], sparsing = cfg[scope_type].get("sparsing"), scope_gain = cfg[scope_type].get("scope_gain"), - pll_frequency = cfg["cwfpga"]["pll_frequency"], + pll_frequency = cfg["target"]["pll_frequency"], ) scope = Scope(scope_cfg) - # Preparation of Key and plaintext generation ------------------------------ - - # Determine which test from configuration - NUM_SEGMENTS = cfg[scope_type]["num_segments"] - TEST_FVSR_KEY_RND_PLAINTEXT = False - TEST_FIXED_KEY_RND_PLAINTEXT = False - if cfg["test"]["which_test"] == "aes_fvsr_key_random_plaintext": - TEST_FVSR_KEY_RND_PLAINTEXT = True - if NUM_SEGMENTS > 1: - print("ERROR: aes_fvsr_key_random_plaintext only supported " - "with num_segments > 1, i.e. batch mode") - elif cfg["test"]["which_test"] == "aes_fixed_key_random_plaintext": - TEST_FIXED_KEY_RND_PLAINTEXT = True - # This test uses the fixed key. It generates random texts through AES - # encryption using a generation key for single mode and uses ciphertexts - # as next text input in batch mode. - - # Load fixed key - key_fixed = bytearray(cfg["test"]["key_fixed"]) - print(f'Using key: {binascii.b2a_hex(bytes(key_fixed))}') - key = key_fixed - cw_target.target.simpleserial_write("k", key) + # Init project. + project_cfg = ProjectConfig(type = cfg["capture"]["trace_db"], + path = project, + wave_dtype = np.uint16, + overwrite = True, + trace_threshold = cfg["capture"].get("trace_threshold") + ) + project = SCAProject(project_cfg) + project.create_project() - # Seed the target's PRNGs for initial key masking, and additionally turn off masking when '0' - cw_target.target.simpleserial_write("l", cfg["test"]["lfsr_seed"].to_bytes(4, "little")) + return target, scope, project - if TEST_FIXED_KEY_RND_PLAINTEXT: - # Cipher to compute expected responses - cipher = AES.new(bytes(key), AES.MODE_ECB) - # Load fixed text as first text - text = bytearray(cfg["test"]["text_fixed"]) +def configure_cipher(cfg, target, capture_cfg) -> OTAES: + """ Configure the AES cipher. + + Establish communication with the AES cipher and configure the seed. - # Prepare generation of new texts/keys by encryption using key_for_generation - key_for_gen = bytearray(cfg["test"]["key_for_gen"]) - cipher_gen = AES.new(bytes(key_for_gen), AES.MODE_ECB) + Args: + cfg: The project config. + target: The OT target. + capture_cfg: The capture config. - if NUM_SEGMENTS > 1: - # Set initial plaintext for batch mode - cw_target.target.simpleserial_write("i", text) + Returns: + The communication interface to the AES cipher. + """ + # Create communication interface to OT AES. + ot_aes = OTAES(target.target) - if TEST_FVSR_KEY_RND_PLAINTEXT: - # Load seed into host-side PRNG (random Python module, Mersenne twister) + # If batch mode, configure PRNGs. + if capture_cfg.batch_mode: + # Seed host's PRNG. random.seed(cfg["test"]["batch_prng_seed"]) - # Load seed into OT (also Mersenne twister) - cw_target.target.simpleserial_write( - "s", - cfg["test"]["batch_prng_seed"].to_bytes(4, "little") - ) - - # First trace uses fixed key - sample_fixed = 1 - # Generate plaintexts and keys for first batch - cw_target.target.simpleserial_write("g", NUM_SEGMENTS.to_bytes(4, "little")) - - # Main loop for measurements with progress bar ----------------------------- - - # Register ctrl-c handler to store traces on abort - signal.signal(signal.SIGINT, partial(abort_handler_during_loop, project, CWLib)) - remaining_num_traces = cfg["capture"]["num_traces"] - with tqdm(total=remaining_num_traces, desc="Capturing", ncols=80, unit=" traces") as pbar: - while remaining_num_traces > 0: - # Note: Capture performance tested Oct. 2023: - # Husy with 1200 samples per trace: 50 it/s - # WaveRunner with 1200 - 50000 samples per trace: ~30 it/s - # +10% by setting 'Performance' to 'Analysis' in 'Utilies->Preferences' in GUI - # WaveRunner batchmode (6k samples, 100 segmets, 1 GHz): ~150 it/s + # Seed the target's PRNGs for initial key masking, and additionally + # turn off masking when '0'. + ot_aes.write_lfsr_seed(cfg["test"]["lfsr_seed"].to_bytes(4, "little")) + ot_aes.write_batch_prng_seed(cfg["test"]["batch_prng_seed"].to_bytes(4, "little")) + + return ot_aes + + +def generate_ref_crypto(sample_fixed, mode, key, key_fixed, plaintext, key_length): + """ Generate cipher material for the encryption. + + This function derives the next key as well as the plaintext for the next + encryption. + + Args: + sample_fixed: Use fixed key or new key. + mode: The mode of the capture. + key: The current key. + key_fixed: The fixed key for FVSR. + plaintext: The current plaintext. + key_length: Th length of the key. + + Returns: + plaintext: The next plaintext. + key: The next key. + ciphertext: The next ciphertext. + sample_fixed: Is the next sample fixed or not? + """ + if mode == "aes_random": + cipher = AES.new(bytes(key), AES.MODE_ECB) + ciphertext = bytearray(cipher.encrypt(bytes(plaintext))) + else: + if sample_fixed: + # Use fixed_key as this key. + key = np.asarray(key_fixed) + else: + # Generate this key from the PRNG. + key = bytearray(key_length) + for i in range(0, key_length): + key[i] = random.randint(0, 255) + # Always generate this plaintext from PRNG (including very first one). + plaintext = bytearray(16) + for i in range(0, 16): + plaintext[i] = random.randint(0, 255) + # Compute ciphertext for this key and plaintext. + cipher = AES.new(bytes(key), AES.MODE_ECB) + ciphertext = bytearray(cipher.encrypt(bytes(plaintext))) + # Determine if next iteration uses fixed_key. + sample_fixed = plaintext[0] & 0x1 + + return plaintext, key, ciphertext, sample_fixed + + +def check_ciphertext(ot_aes, expected_last_ciphertext, ciphertext_len): + """ Compares the received with the generated ciphertext. + + Ciphertext is read from the device and compared against the pre-computed + generated ciphertext. In batch mode, only the last ciphertext is compared. + Asserts on mismatch. + + Args: + ot_aes: The OpenTitan AES communication interface. + expected_last_ciphertext: The pre-computed ciphertext. + ciphertext_len: The length of the ciphertext in bytes. + """ + actual_last_ciphertext = ot_aes.read_ciphertext(ciphertext_len) + assert actual_last_ciphertext == expected_last_ciphertext[0:ciphertext_len], ( + f"Incorrect encryption result!\n" + f"actual: {actual_last_ciphertext}\n" + f"expected: {expected_last_ciphertext}" + ) + + +def capture(scope: Scope, ot_aes: OTAES, capture_cfg: CaptureConfig, project, cwtarget: CWFPGA): + """ Capture power consumption during AES encryption. + + Supports four different capture types: + * aes_random: Fixed key, random plaintext. TODO HOW ptx generated. + * aes_random_batch: Fixed key, random plaintext in batch mode. + * aes_fvsr: Fixed vs. random key. + * aes_fvsr_batch: Fixed vs. random key batch. - # Arm scope -------------------------------------------------------- + Args: + ot_aes: The OpenTitan AES communication interface. + expected_last_ciphertext: The pre-computed ciphertext. + ciphertext_len: The length of the ciphertext in bytes. + """ + # Initial plaintext. + text = bytearray(capture_cfg.text_fixed) + + # Load fixed key. + key_fixed = bytearray(capture_cfg.key_fixed) + key = key_fixed + logger.info(f"Initializing OT AES with key {binascii.b2a_hex(bytes(key))} ...") + ot_aes.write_key(key) + + # Generate plaintexts and keys for first batch. + if capture_cfg.batch_mode and capture_cfg.capture_mode == "aes_fsvr_key": + ot_aes.write_fvsr_batch_generate(capture_cfg.num_segments.to_bytes(4, "little")) + + if capture_cfg.batch_mode and capture_cfg.capture_mode == "aes_random": + ot_aes.write_init_text(text) + + # FVSR setup. + sample_fixed = 1 + + # Register ctrl-c handler to store traces on abort. + signal.signal(signal.SIGINT, partial(abort_handler_during_loop, project)) + + # Main capture with progress bar. + remaining_num_traces = capture_cfg.num_traces + with tqdm(total=remaining_num_traces, desc="Capturing", ncols=80, unit=" traces") as pbar: + while remaining_num_traces > 0: + # Arm the scope. scope.arm() - # Trigger execution(s) --------------------------------------------- - if NUM_SEGMENTS > 1: - # Perform batch encryptions - if TEST_FIXED_KEY_RND_PLAINTEXT: - # Execute and generate next text as ciphertext - cw_target.target.simpleserial_write("a", NUM_SEGMENTS.to_bytes(4, "little")) - if TEST_FVSR_KEY_RND_PLAINTEXT: - # Execute and generate next keys and plaintexts - cw_target.target.simpleserial_write("f", NUM_SEGMENTS.to_bytes(4, "little")) - - else: # single encryption - # Load text and trigger execution - # First iteration uses initial text, new texts are generated below - cw_target.target.simpleserial_write('p', text) - - # Capture trace(s) ------------------------------------------------- - waves = scope.capture_and_transfer_waves() - assert waves.shape[0] == NUM_SEGMENTS - - # Storing traces --------------------------------------------------- - - # Loop to compute keys, texts and ciphertexts for each trace and store them. - for i in range(NUM_SEGMENTS): - - # Compute text, key, ciphertext - if TEST_FIXED_KEY_RND_PLAINTEXT: - ciphertext = bytearray(cipher.encrypt(bytes(text))) - - if TEST_FVSR_KEY_RND_PLAINTEXT: - if sample_fixed: - # Use fixed_key as this key - key = np.asarray(key_fixed) - else: - # Generate this key from PRNG - key = bytearray(cfg["test"]["key_len_bytes"]) - for ii in range(0, cfg["test"]["key_len_bytes"]): - key[ii] = random.randint(0, 255) - # Always generate this plaintext from PRNG (including very first one) - text = bytearray(16) - for ii in range(0, 16): - text[ii] = random.randint(0, 255) - # Compute ciphertext for this key and plaintext - cipher = AES.new(bytes(key), AES.MODE_ECB) - ciphertext = bytearray(cipher.encrypt(bytes(text))) - # Determine if next iteration uses fixed_key - sample_fixed = text[0] & 0x1 - - # Sanity check retrieved data (wave) - assert len(waves[i, :]) >= 1 - # Add trace. - if OTTraceLib: - # OT Trace Library - trace = Trace(wave=waves[i, :].tobytes(), - plaintext=text, - ciphertext=ciphertext, - key=key) - project.write_to_buffer(trace) + # Trigger encryption. + if capture_cfg.batch_mode: + # Batch mode. + if capture_cfg.capture_mode == "aes_random": + # Fixed key, random plaintexts. + ot_aes.encrypt_batch( + capture_cfg.num_segments.to_bytes(4, "little")) else: - # CW Trace - trace = cw.Trace(waves[i, :], text, ciphertext, key) - # Append trace Library trace to database - # TODO Also use uint16 as dtype for 8 bit WaveRunner for - # tvla processing - project.traces.append(trace, dtype=np.uint16) - - if TEST_FIXED_KEY_RND_PLAINTEXT: - # Use ciphertext as next text, first text is the initial one + # Fixed vs random key test. + ot_aes.encrypt_fvsr_key_batch( + capture_cfg.num_segments.to_bytes(4, "little")) + else: + # Non batch mode. + ot_aes.encrypt(text) + + # Capture traces. + waves = scope.capture_and_transfer_waves(cwtarget.target) + assert waves.shape[0] == capture_cfg.num_segments + + # Generate reference crypto material and store trace. + for i in range(capture_cfg.num_segments): + text, key, ciphertext, sample_fixed = generate_ref_crypto( + sample_fixed = sample_fixed, + mode = capture_cfg.capture_mode, + key = key, + key_fixed = key_fixed, + plaintext = text, + key_length = capture_cfg.key_len_bytes + ) + # Sanity check retrieved data (wave). + assert len(waves[i, :]) >= 1 + # Store trace into database. + project.append_trace(wave = waves[i, :], + plaintext = text, + ciphertext = ciphertext, + key = key) + + if capture_cfg.capture_mode == "aes_random": + # Use ciphertext as next text, first text is the initial + # one. text = ciphertext - # Get (last) ciphertext from device and verify --------------------- - if TEST_FIXED_KEY_RND_PLAINTEXT: - compare_len = cw_target.target.output_len - if TEST_FVSR_KEY_RND_PLAINTEXT: + # Compare received ciphertext with generated. + if capture_cfg.capture_mode == "aes_random": + compare_len = capture_cfg.output_len + else: compare_len = 4 - response = cw_target.target.simpleserial_read('r', compare_len, ack=False) - if binascii.b2a_hex(response) != binascii.b2a_hex(ciphertext[0:compare_len]): - raise RuntimeError(f'Bad ciphertext: {response} != {ciphertext}.') - - # Update the loop variable and the progress bar -------------------- - remaining_num_traces -= NUM_SEGMENTS - pbar.update(NUM_SEGMENTS) - - # Create and show test plot ------------------------------------------------ - # Use this plot to check for clipping and adjust gain appropriately - if CWLib: - proj_waves = project.waves - else: - proj_waves = project.get_waves() - - if cfg["capture"]["show_plot"]: - plot.save_plot_to_file(proj_waves, None, cfg["capture"]["plot_traces"], - cfg["capture"]["trace_image_filename"], add_mean_stddev=True) - print(f'Created plot with {cfg["capture"]["plot_traces"]} traces: ' - f'{Path(cfg["capture"]["trace_image_filename"]).resolve()}') - - if CWLib: - project.settingsDict['datetime'] = now - project.settingsDict['cfg'] = cfg - project.settingsDict['sample_rate'] = scope_cfg.num_samples - project.save() - else: - metadata = Metadata(config =cfg_file, - datetime=now, - bitstream_path=cfg["cwfpga"]["fpga_bitstream"], - binary_path=cfg["cwfpga"]["fw_bin"], - offset=scope_cfg.offset_samples, - sample_rate=scope_cfg.num_samples, - scope_gain=scope_cfg.scope_gain - ) - project.write_metadata(metadata) - project.flush_to_disk() + check_ciphertext(ot_aes, ciphertext, compare_len) + + # Update the loop variable and the progress bar. + remaining_num_traces -= capture_cfg.num_segments + pbar.update(capture_cfg.num_segments) + + +def print_plot(project: SCAProject, config: dict) -> None: + """ Print plot of traces. + + Printing the plot helps to adjust the scope gain and check for clipping. + + Args: + project: The project containing the traces. + config: The capture configuration. + """ + if config["capture"]["show_plot"]: + plot.save_plot_to_file(project.get_waves(0, config["capture"]["plot_traces"]), + set_indices = None, + num_traces = config["capture"]["plot_traces"], + outfile = config["capture"]["trace_image_filename"], + add_mean_stddev=True) + print(f'Created plot with {config["capture"]["plot_traces"]} traces: ' + f'{Path(config["capture"]["trace_image_filename"]).resolve()}') + + +def main(argv=None): + # Configure the logger. + logger.setLevel(logging.INFO) + console = logging.StreamHandler() + logger.addHandler(console) + + # Parse the provided arguments. + args = helpers.parse_arguments(argv) + + # Load configuration from file. + with open(args.cfg) as f: + cfg = yaml.load(f, Loader=yaml.FullLoader) + + # Setup the target, scope and project. + target, scope, project = setup(cfg, args.project) + + # Determine the capture mode and configure the current capture. + mode = "aes_fsvr_key" + batch = False + if "aes_random" in cfg["test"]["which_test"]: + mode = "aes_random" + if "batch" in cfg["test"]["which_test"]: + batch = True + capture_cfg = CaptureConfig(capture_mode = mode, + batch_mode = batch, + num_traces = cfg["capture"]["num_traces"], + num_segments = cfg[cfg["capture"]["scope_select"]]["num_segments"], + output_len = cfg["target"]["output_len_bytes"], + text_fixed = cfg["test"]["text_fixed"], + key_fixed = cfg["test"]["key_fixed"], + key_len_bytes = cfg["test"]["key_len_bytes"], + text_len_bytes = cfg["test"]["text_len_bytes"]) + logger.info(f"Setting up capture {capture_cfg.capture_mode} batch={capture_cfg.batch_mode}...") + + # Configure cipher. + ot_aes = configure_cipher(cfg, target, capture_cfg) + + # Capture traces. + capture(scope, ot_aes, capture_cfg, project, target) + + # Print plot. + print_plot(project, cfg) + + # Save metadata. + metadata = {} + metadata["datetime"] = datetime.now().strftime("%m/%d/%Y, %H:%M:%S") + metadata["cfg"] = cfg + metadata["num_samples"] = scope.scope_cfg.num_samples + metadata["offset_samples"] = scope.scope_cfg.offset_samples + metadata["scope_gain"] = scope.scope_cfg.scope_gain + metadata["cfg_file"] = str(args.cfg) + metadata["fpga_bitstream"] = cfg["target"]["fpga_bitstream"] + metadata["fw_bin"] = cfg["target"]["fw_bin"] + metadata["notes"] = "" + project.write_metadata(metadata) + + # Save and close project. + project.save() + + +if __name__ == "__main__": + main() diff --git a/capture/configs/aes_sca_cw310.yaml b/capture/configs/aes_sca_cw310.yaml new file mode 100644 index 000000000..d650bd382 --- /dev/null +++ b/capture/configs/aes_sca_cw310.yaml @@ -0,0 +1,45 @@ +target: + target_type: cw310 + fpga_bitstream: "../objs/lowrisc_systems_chip_earlgrey_cw310_0.1.bit" + force_program_bitstream: False + fw_bin: "../objs/aes_serial_fpga_cw310.bin" + pll_frequency: 100000000 + baudrate: 115200 + output_len_bytes: 16 +husky: + pll_frequency: 100000000 + target_clk_mult: 0.1 + num_segments: 20 + num_cycles: 60 + offset_cycles: -2 + scope_gain: 38 +waverunner: + waverunner_ip: 100.107.71.10 + num_segments: 20 + num_samples: 6000 + sample_offset: 0 +capture: + scope_select: husky + # scope_select: waverunner + num_traces: 1000 + show_plot: True + plot_traces: 100 + trace_image_filename: "projects/simple_capture_aes_sca_sample_traces.html" + trace_db: ot_trace_library + trace_threshold: 10000 + # trace_db: cw +test: + which_test: aes_random_batch + # which_test: aes_random + # which_test: aes_fsvr_key + # which_test: aes_fsvr_key_batch + key_len_bytes: 16 + text_len_bytes: 16 + key_fixed: [0x81, 0x1E, 0x37, 0x31, 0xB0, 0x12, 0x0A, 0x78, 0x42, 0x78, 0x1E, 0x22, 0xB2, 0x5C, 0xDD, 0xF9] + text_fixed: [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] + # seed for PRNG to generate sequence of plaintexts and keys; Python random class on host, Mersenne twister implementation on OT SW + batch_prng_seed: 0 + # 32-bit seed for masking on device. To switch off the masking, use 0 as LFSR seed. + lfsr_seed: 0x00000000 + # lfsr_seed: 0xdeadbeef diff --git a/capture/lib/helpers.py b/capture/lib/helpers.py new file mode 100644 index 000000000..91552f4b5 --- /dev/null +++ b/capture/lib/helpers.py @@ -0,0 +1,64 @@ +# Copyright lowRISC contributors. +# Licensed under the Apache License, Version 2.0, see LICENSE for details. +# SPDX-License-Identifier: Apache-2.0 + +import argparse +from pathlib import Path + + +def ap_check_file_exists(file_path: str) -> Path: + """Verifies that the provided file path is valid + + Args: + file_path: The file path. + + Returns: + The file path. + """ + path = Path(file_path) + if not path.is_file(): + raise argparse.ArgumentTypeError(f"File {path} does not exist") + return path + + +def ap_check_dir_exists(path: str) -> Path: + """Verifies that the provided path is valid + + Args: + path: The path. + + Returns: + The file path. + """ + path = Path(path) + if not path.parent.exists(): + raise argparse.ArgumentTypeError(f"Path {path.parent} does not exist") + return path + + +def parse_arguments(argv): + """ Command line argument parsing. + + Args: + argv: The command line arguments. + + Returns: + The parsed arguments. + """ + parser = argparse.ArgumentParser(description="Parse") + parser.add_argument("-c", + "--capture_config", + dest="cfg", + type=ap_check_file_exists, + required=True, + help="Path of the attack config file") + parser.add_argument("-p", + "--project", + dest="project", + type=ap_check_dir_exists, + required=True, + help="Path of the output project directory") + + args = parser.parse_args(argv) + + return args diff --git a/capture/lib/ot_communication.py b/capture/lib/ot_communication.py new file mode 100644 index 000000000..f1becc27a --- /dev/null +++ b/capture/lib/ot_communication.py @@ -0,0 +1,79 @@ +# Copyright lowRISC contributors. +# Licensed under the Apache License, Version 2.0, see LICENSE for details. +# SPDX-License-Identifier: Apache-2.0 +"""Communication interface for different OpenTitan ciphers. + +Currently, communication with each cipher happens over simpleserial. This file +provides communication interface classes for each of these ciphers. +""" + + +class OTAES: + def __init__(self, target) -> None: + self.target = target + + def write_key(self, key): + """ Write key to AES. + Args: + key: Bytearray containing the key. + """ + self.target.simpleserial_write("k", key) + + def write_lfsr_seed(self, seed): + """ Seed the LFSR. + Args: + seed: The 4-byte seed. + """ + self.target.simpleserial_write("l", seed) + + def write_batch_prng_seed(self, seed): + """ Seed the PRNG. + Args: + seed: The 4-byte seed. + """ + self.target.simpleserial_write("s", seed) + + def write_fvsr_batch_generate(self, num_segments): + """ Generate random plaintexts for FVSR. + Args: + num_segments: Number of encryptions to perform. + """ + self.target.simpleserial_write("g", num_segments) + + def encrypt_batch(self, num_segments): + """ Start encryption for batch. + Args: + num_segments: Number of encryptions to perform. + """ + self.target.simpleserial_write('a', num_segments) + + def encrypt_fvsr_key_batch(self, num_segments): + """ Start encryption for FVSR. + Args: + num_segments: Number of encryptions to perform. + """ + self.target.simpleserial_write('f', num_segments) + + def write_init_text(self, text): + """ Write plaintext to OpenTitan AES. + Args: + text: The plaintext bytearray. + """ + self.target.simpleserial_write('i', text) + + def encrypt(self, text): + """ Write plaintext to OpenTitan AES & start encryption. + Args: + text: The plaintext bytearray. + """ + self.target.simpleserial_write('p', text) + + def read_ciphertext(self, len_bytes): + """ Read ciphertext from OpenTitan AES. + Args: + len_bytes: Number of bytes to read. + + Returns: + The received ciphertext. + """ + return self.target.simpleserial_read('r', len_bytes, ack=False) diff --git a/capture/project_library/trace_library.py b/capture/project_library/ot_trace_library/trace_library.py similarity index 77% rename from capture/project_library/trace_library.py rename to capture/project_library/ot_trace_library/trace_library.py index 02c5df8b0..4c10c89b8 100644 --- a/capture/project_library/trace_library.py +++ b/capture/project_library/ot_trace_library/trace_library.py @@ -1,7 +1,7 @@ # Copyright lowRISC contributors. # Licensed under the Apache License, Version 2.0, see LICENSE for details. # SPDX-License-Identifier: Apache-2.0 -import datetime +import pickle from dataclasses import asdict, dataclass from pathlib import Path from typing import Optional @@ -19,19 +19,7 @@ class Metadata: Stores information about the capture of the traces. """ - config: str - datetime: datetime.datetime - bitstream_path: str - binary_path: str - bitstream: Optional[bytearray] = None - binary: Optional[bytearray] = None - offset: Optional[int] = 0 - sample_rate: Optional[int] = 0 - scope_gain: Optional[float] = 0 - trigger_level: Optional[float] = 0 - time_div: Optional[float] = 0 - ot_sca_commit: Optional[str] = "" - notes: Optional[str] = "" + data: bytearray @dataclass @@ -81,19 +69,7 @@ def __init__(self, db_name, trace_threshold, wave_datatype = np.uint16, self.metadata_table = db.Table( "metadata", self.metadata, - db.Column("config", db.String), - db.Column("datetime", db.DateTime), - db.Column("bitstream_path", db.String), - db.Column("binary_path", db.String), - db.Column("bitstream", db.LargeBinary), - db.Column("binary", db.LargeBinary), - db.Column("offset", db.Integer), - db.Column("sample_rate", db.Integer), - db.Column("scope_gain", db.Float), - db.Column("trigger_level", db.Float), - db.Column("time_div", db.Float), - db.Column("ot_sca_commit", db.String), - db.Column("notes", db.String) + db.Column("data", db.PickleType) ) self.metadata.create_all(self.engine) self.trace_mem = [] @@ -152,25 +128,26 @@ def get_traces(self, start: Optional[int] = None, return [Trace(**trace._mapping) for trace in self.session.execute(query).fetchall()] - def get_waves_bytearray(self): + def get_waves_bytearray(self, start: Optional[int] = None, + end: Optional[int] = None): """ Get all waves from the database. Returns: The bytearray waves from the database. """ - return [trace.wave for trace in self.get_traces()] + return [trace.wave for trace in self.get_traces(start, end)] - def get_waves(self): + def get_waves(self, start: Optional[int] = None, end: Optional[int] = None): """ Get all waves from the database in the trace array format. Returns: The waves from the database in the type wave_datatype. """ return [np.frombuffer(b, self.wave_datatype) - for b in self.get_waves_bytearray()] + for b in self.get_waves_bytearray(start, end)] - def get_plaintexts_int(self, start: Optional[int] = None, - end: Optional[int] = None): + def get_plaintexts(self, start: Optional[int] = None, + end: Optional[int] = None): """ Get all plaintexts between start and end from the database in the int8 array format. @@ -180,8 +157,8 @@ def get_plaintexts_int(self, start: Optional[int] = None, return [np.frombuffer(trace.plaintext, np.uint8) for trace in self.get_traces(start, end)] - def get_keys_int(self, start: Optional[int] = None, - end: Optional[int] = None): + def get_keys(self, start: Optional[int] = None, + end: Optional[int] = None): """ Get all keys between start and end from the database in the int8 array format. @@ -198,7 +175,8 @@ def write_metadata(self, metadata): metadata: The metadata to store. """ query = db.insert(self.metadata_table) - self.session.execute(query, asdict(metadata)) + data = Metadata(str(pickle.dumps(metadata), encoding="latin1")) + self.session.execute(query, asdict(data)) self.session.commit() def get_metadata(self): @@ -208,4 +186,5 @@ def get_metadata(self): The metadata from the database. """ query = db.select(self.metadata_table) - return self.session.execute(query).fetchall() + metadata = Metadata(**self.session.execute(query).fetchall()[0]._mapping) + return pickle.loads(bytes(metadata.data, encoding="latin1")) diff --git a/capture/project_library/project.py b/capture/project_library/project.py new file mode 100644 index 000000000..afe4dbe93 --- /dev/null +++ b/capture/project_library/project.py @@ -0,0 +1,144 @@ +#!/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 sys +from dataclasses import dataclass +from pathlib import Path +from typing import Optional + +import chipwhisperer as cw +import numpy as np + +sys.path.append("../") +from capture.project_library.ot_trace_library.trace_library import ( # noqa: E402 + Trace, TraceLibrary) + + +@dataclass +class ProjectConfig: + """ Project configuration. + Stores information about the project. + """ + type: str + path: Path + wave_dtype: np.dtype + overwrite: bool + trace_threshold: Optional[int] = 1 + + +class SCAProject: + """ Project class. + + Used to manage a SCA project. + """ + def __init__(self, project_cfg: ProjectConfig) -> None: + self.project_cfg = project_cfg + self.project = None + + def create_project(self): + """ Create project. + + Create project using the provided path. + """ + if self.project_cfg.type == "cw": + self.project = cw.create_project(self.project_cfg.path, + overwrite=self.project_cfg.overwrite) + elif self.project_cfg.type == "ot_trace_library": + self.project = TraceLibrary( + str(self.project_cfg.path), + trace_threshold=self.project_cfg.trace_threshold, + wave_datatype=self.project_cfg.wave_dtype, + overwrite=self.project_cfg.overwrite) + else: + raise RuntimeError("Only trace_db='cw' or trace_db='ot_trace_library' supported.") + + def open_project(self) -> None: + """ Open project. + """ + if self.project_cfg.type == "cw": + self.project = cw.open_project(self.project_cfg.path) + elif self.project_cfg.type == "ot_trace_library": + self.project = TraceLibrary( + str(self.project_cfg.path), + trace_threshold=self.project_cfg.trace_threshold, + wave_datatype=self.project_cfg.wave_dtype, + overwrite=self.project_cfg.overwrite) + + def close(self, save: bool) -> None: + """ Close project. + """ + if self.project_cfg.type == "cw": + self.project.close(save = save) + elif self.project_cfg.type == "ot_trace_library": + self.project.flush_to_disk() + + self.project = None + + def save(self) -> None: + """ Save project. + """ + if self.project_cfg.type == "cw": + self.project.save() + elif self.project_cfg.type == "ot_trace_library": + self.project.flush_to_disk() + + def append_trace(self, wave, plaintext, ciphertext, key) -> None: + """ Append trace to trace storage in project. + """ + if self.project_cfg.type == "cw": + trace = cw.Trace(wave, plaintext, ciphertext, key) + self.project.traces.append(trace, dtype=self.project_cfg.wave_dtype) + elif self.project_cfg.type == "ot_trace_library": + trace = Trace(wave=wave.tobytes(), + plaintext=plaintext, + ciphertext=ciphertext, + key=key) + self.project.write_to_buffer(trace) + + def get_waves(self, start: Optional[int] = None, end: Optional[int] = None): + """ Get waves from project. + """ + if self.project_cfg.type == "cw": + return self.project.waves[start:end] + elif self.project_cfg.type == "ot_trace_library": + return self.project.get_waves(start, end) + + def get_keys(self, start: Optional[int] = None, end: Optional[int] = None): + """ Get keys[start, end] from project. + """ + if self.project_cfg.type == "cw": + if start and end: + return self.project.keys[start:end] + else: + return self.project.keys + elif self.project_cfg.type == "ot_trace_library": + return self.project.get_keys(start, end) + + def get_plaintexts(self, start: Optional[int] = None, end: Optional[int] = None): + """ Get plaintexts[start, end] from project. + """ + if self.project_cfg.type == "cw": + if start and end: + return self.project.textins[start:end] + else: + return self.project.textins + elif self.project_cfg.type == "ot_trace_library": + return self.project.get_plaintexts(start, end) + + def write_metadata(self, metadata: dict) -> None: + """ Write metadata to project. + """ + if self.project_cfg.type == "cw": + self.project.settingsDict.update(metadata) + elif self.project_cfg.type == "ot_trace_library": + self.project.write_metadata(metadata) + + def get_metadata(self, ) -> dict: + """ Get metadata from project. + """ + if self.project_cfg.type == "cw": + return self.project.settingsDict + elif self.project_cfg.type == "ot_trace_library": + return self.project.get_metadata() diff --git a/capture/scopes/chipwhisperer/husky.py b/capture/scopes/chipwhisperer/husky.py index 58cb2934e..38a6785fe 100644 --- a/capture/scopes/chipwhisperer/husky.py +++ b/capture/scopes/chipwhisperer/husky.py @@ -87,11 +87,11 @@ def configure_batch_mode(self): def arm(self): self.scope.arm() - def capture_and_transfer_waves(self): + def capture_and_transfer_waves(self, target=None): if self.num_segments == 1: ret = self.scope.capture(poll_done=False) i = 0 - while not self.opentitan_device.target.is_done(): + while not target.is_done(): i += 1 time.sleep(0.05) if i > 100: diff --git a/capture/scopes/scope.py b/capture/scopes/scope.py index 730f581e8..1d1dbecd9 100644 --- a/capture/scopes/scope.py +++ b/capture/scopes/scope.py @@ -79,7 +79,10 @@ def arm(self) -> None: """ self.scope.arm() - def capture_and_transfer_waves(self): + def capture_and_transfer_waves(self, target=None): """ Wait until capture is finished and return traces. """ - return self.scope.capture_and_transfer_waves() + if self.scope_cfg.scope_type == "husky": + return self.scope.capture_and_transfer_waves(target) + else: + return self.scope.capture_and_transfer_waves() diff --git a/ci/azure-pipelines.yml b/ci/azure-pipelines.yml index 84dfc63c4..219accefd 100644 --- a/ci/azure-pipelines.yml +++ b/ci/azure-pipelines.yml @@ -23,48 +23,41 @@ jobs: git-lfs pull displayName: "Pull LFS binaries" - bash: | + set -e pushd ci - ./ci_capture_aes_fvsr.sh + mkdir -p projects + ../capture/capture_aes.py -c cfg/ci_aes_sca_fvsr_cw310.yaml -p projects/aes_sca_fvsr_cw310 popd - displayName: "Capture traces" - - publish: ./ci/ci_projects/opentitan_simple_aes_data/ - artifact: traces - displayName: "Upload traces" + displayName: "Capture AES FVSR traces" + - publish: ./ci/projects/aes_sca_fvsr_cw310.html + artifact: traces_aes_fvsr + displayName: "Upload AES FVSR traces" - bash: | set -e pushd ci - ./ci_check_aes_traces.sh - ./ci_trace_check/ci_compare_aes_traces.py -f ./ci_projects/opentitan_simple_aes.cwp -g ./ci_trace_check/golden_traces/aes_128_ecb_static_cw310.zip -c 0.8 + ../analysis/tvla.py --cfg-file cfg/ci_tvla_cfg_aes_specific_byte0_rnd0.yaml run-tvla popd - displayName: "Compare captured AES trace against golden trace" + displayName: "Perform TVLA on AES FVSR traces" continueOnError: True - - publish: ./ci/tmp/ - artifact: plot_traces_aes - displayName: "Upload plot of captured AES traces." - - publish: ./ci/ci_projects/opentitan_simple_aes.zip - artifact: project_traces_aes - displayName: "Upload project of captured AES traces." + - publish: ./ci/tmp/figures + artifact: tvla_figure + displayName: "Upload figure of AES TVLA." - bash: | set -e pushd ci mkdir -p projects - ../capture/capture_aes.py cfg/ci_simple_capture_aes_sca.yaml - ../analysis/tvla.py --cfg-file cfg/ci_tvla_cfg_aes_specific_byte0_rnd0.yaml run-tvla + ../capture/capture_aes.py -c cfg/ci_aes_sca_random_cw310.yaml -p projects/aes_sca_random_cw310 popd - displayName: "Check AES TVLA" - continueOnError: True - - publish: ./ci/tmp/figures - artifact: tvla_figure - displayName: "Upload figure of AES TVLA." + displayName: "Capture AES Random Key traces" + - publish: ./ci/projects/aes_sca_random_cw310.html + artifact: traces_aes_random_key + displayName: "Upload AES Random Key traces" - bash: | + set -e pushd ci - ./ci_capture_all.sh + ./ci_trace_check/ci_compare_aes_traces.py -f ./projects/aes_sca_random_cw310 -g ./ci_trace_check/golden_traces/aes_sca_random_golden_cw310 -c 0.8 popd - displayName: "Check all traces" - - publish: ./ci/figures_tmp/ - artifact: cw310_figures - displayName: "Upload figures of all traces" - + displayName: "Compare AES Random Key traces against golden trace" - job: sca_capture_cw305 displayName: "Capture SCA traces (CW305)" timeoutInMinutes: 30 @@ -85,6 +78,11 @@ jobs: displayName: "Pull LFS binaries" - bash: | set -e - cd cw - ./capture.py --cfg-file capture_aes_cw305.yaml capture aes-random-batch --num-traces 1000 --force-program-bitstream - displayName: "Capture traces" + pushd ci + mkdir -p projects + ../capture/capture_aes.py -c cfg/ci_aes_sca_random_cw305.yaml -p projects/aes_sca_random_cw305 + popd + displayName: "Capture AES Random Key traces" + - publish: ./ci/projects/aes_sca_fvsr_cw305.html + artifact: traces_aes_random_cw305 + displayName: "Upload AES Random Key traces" diff --git a/ci/cfg/ci_aes_sca_fvsr_cw310.yaml b/ci/cfg/ci_aes_sca_fvsr_cw310.yaml new file mode 100644 index 000000000..bfea65cfe --- /dev/null +++ b/ci/cfg/ci_aes_sca_fvsr_cw310.yaml @@ -0,0 +1,44 @@ +target: + target_type: cw310 + fpga_bitstream: "../objs/lowrisc_systems_chip_earlgrey_cw310_0.1.bit" + force_program_bitstream: False + fw_bin: "../objs/aes_serial_fpga_cw310.bin" + pll_frequency: 100000000 + baudrate: 115200 + output_len_bytes: 16 +husky: + pll_frequency: 100000000 + target_clk_mult: 0.1 + num_segments: 20 + num_cycles: 60 + offset_cycles: -2 + scope_gain: 38 +waverunner: + waverunner_ip: 100.107.71.10 + num_segments: 20 + num_samples: 6000 + sample_offset: 0 +capture: + scope_select: husky + # scope_select: waverunner + num_traces: 1000 + show_plot: True + plot_traces: 100 + trace_image_filename: "projects/aes_sca_fvsr_cw310.html" + trace_db: ot_trace_library + # trace_db: cw + trace_threshold: 10000 +test: + # which_test: aes_random_batch + # which_test: aes_random + which_test: aes_fsvr_key_batch + key_len_bytes: 16 + text_len_bytes: 16 + key_fixed: [0x81, 0x1E, 0x37, 0x31, 0xB0, 0x12, 0x0A, 0x78, 0x42, 0x78, 0x1E, 0x22, 0xB2, 0x5C, 0xDD, 0xF9] + text_fixed: [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] + # seed for PRNG to generate sequence of plaintexts and keys; Python random class on host, Mersenne twister implementation on OT SW + batch_prng_seed: 0 + # 32-bit seed for masking on device. To switch off the masking, use 0 as LFSR seed. + lfsr_seed: 0x00000000 + # lfsr_seed: 0xdeadbeef diff --git a/ci/cfg/ci_aes_sca_random_cw305.yaml b/ci/cfg/ci_aes_sca_random_cw305.yaml new file mode 100644 index 000000000..eed6b9262 --- /dev/null +++ b/ci/cfg/ci_aes_sca_random_cw305.yaml @@ -0,0 +1,42 @@ +target: + target_type: cw305 + fpga_bitstream: "../objs/lowrisc_systems_chip_englishbreakfast_cw305_0.1.bit" + force_program_bitstream: False + fw_bin: "../objs/aes_serial_fpga_cw305.bin" + pll_frequency: 100000000 + baudrate: 115200 + output_len_bytes: 16 +husky: + pll_frequency: 100000000 + target_clk_mult: 0.1 + num_segments: 1 + num_cycles: 60 + offset_cycles: -2 + scope_gain: 20 +waverunner: + waverunner_ip: 100.107.71.10 + num_segments: 1 + num_samples: 6000 + sample_offset: 0 +capture: + scope_select: husky + # scope_select: waverunner + num_traces: 500 + show_plot: True + plot_traces: 100 + trace_image_filename: "projects/aes_sca_fvsr_cw305.html" + trace_db: ot_trace_library + # trace_db: cw + trace_threshold: 10000 +test: + which_test: aes_random + key_len_bytes: 16 + text_len_bytes: 16 + key_fixed: [0x81, 0x1E, 0x37, 0x31, 0xB0, 0x12, 0x0A, 0x78, 0x42, 0x78, 0x1E, 0x22, 0xB2, 0x5C, 0xDD, 0xF9] + text_fixed: [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] + # seed for PRNG to generate sequence of plaintexts and keys; Python random class on host, Mersenne twister implementation on OT SW + batch_prng_seed: 0 + # 32-bit seed for masking on device. To switch off the masking, use 0 as LFSR seed. + lfsr_seed: 0x00000000 + # lfsr_seed: 0xdeadbeef diff --git a/ci/cfg/ci_aes_sca_random_cw310.yaml b/ci/cfg/ci_aes_sca_random_cw310.yaml new file mode 100644 index 000000000..a14b90b6e --- /dev/null +++ b/ci/cfg/ci_aes_sca_random_cw310.yaml @@ -0,0 +1,44 @@ +target: + target_type: cw310 + fpga_bitstream: "../objs/lowrisc_systems_chip_earlgrey_cw310_0.1.bit" + force_program_bitstream: False + fw_bin: "../objs/aes_serial_fpga_cw310.bin" + pll_frequency: 100000000 + baudrate: 115200 + output_len_bytes: 16 +husky: + pll_frequency: 100000000 + target_clk_mult: 0.1 + num_segments: 20 + num_cycles: 60 + offset_cycles: -2 + scope_gain: 38 +waverunner: + waverunner_ip: 100.107.71.10 + num_segments: 20 + num_samples: 6000 + sample_offset: 0 +capture: + scope_select: husky + # scope_select: waverunner + num_traces: 1000 + show_plot: True + plot_traces: 100 + trace_image_filename: "projects/aes_sca_random_cw310.html" + trace_db: ot_trace_library + # trace_db: cw + trace_threshold: 10000 +test: + which_test: aes_random_batch + # which_test: aes_random + # which_test: aes_fsvr_key_batch + key_len_bytes: 16 + text_len_bytes: 16 + key_fixed: [0x81, 0x1E, 0x37, 0x31, 0xB0, 0x12, 0x0A, 0x78, 0x42, 0x78, 0x1E, 0x22, 0xB2, 0x5C, 0xDD, 0xF9] + text_fixed: [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] + # seed for PRNG to generate sequence of plaintexts and keys; Python random class on host, Mersenne twister implementation on OT SW + batch_prng_seed: 0 + # 32-bit seed for masking on device. To switch off the masking, use 0 as LFSR seed. + lfsr_seed: 0x00000000 + # lfsr_seed: 0xdeadbeef diff --git a/ci/cfg/ci_tvla_cfg_aes_specific_byte0_rnd0.yaml b/ci/cfg/ci_tvla_cfg_aes_specific_byte0_rnd0.yaml index 481f9a25e..879b36239 100644 --- a/ci/cfg/ci_tvla_cfg_aes_specific_byte0_rnd0.yaml +++ b/ci/cfg/ci_tvla_cfg_aes_specific_byte0_rnd0.yaml @@ -1,4 +1,4 @@ -project_file: projects/simple_capture_aes_sca +project_file: projects/aes_sca_fvsr_cw310 trace_file: null trace_start: null trace_end: null diff --git a/ci/ci_trace_check/ci_compare_aes_traces.py b/ci/ci_trace_check/ci_compare_aes_traces.py index 8fd4aa04c..df8ca679c 100755 --- a/ci/ci_trace_check/ci_compare_aes_traces.py +++ b/ci/ci_trace_check/ci_compare_aes_traces.py @@ -5,10 +5,13 @@ import argparse import sys -import chipwhisperer as cw import numpy as np import scipy.stats +sys.path.append("../") +from capture.project_library.project import ProjectConfig # noqa: E402 +from capture.project_library.project import SCAProject # noqa: E402 + def analyze_traces(file_proj, file_gold_proj, corr_coeff) -> bool: """Performs a correlation between golden and new traces. @@ -27,14 +30,30 @@ def analyze_traces(file_proj, file_gold_proj, corr_coeff) -> bool: True if trace comparison succeeds, False otherwise. """ # Open the current project - proj_curr = cw.open_project(file_proj) + project_curr_cfg = ProjectConfig(type = "ot_trace_library", + path = file_proj, + wave_dtype = np.uint16, + overwrite = False, + trace_threshold = 10000 + ) + proj_curr = SCAProject(project_curr_cfg) + proj_curr.open_project() # Calculate mean of new traces - curr_trace = np.mean(proj_curr.waves, axis=0) + curr_waves = proj_curr.get_waves() + curr_trace = np.mean(curr_waves, axis=0) # Import the golden project - proj_gold = cw.import_project(file_gold_proj) + project_gold_cfg = ProjectConfig(type = "ot_trace_library", + path = file_gold_proj, + wave_dtype = np.uint16, + overwrite = False, + trace_threshold = 10000 + ) + proj_gold = SCAProject(project_gold_cfg) + proj_gold.open_project() # Calculate mean of golden traces - gold_trace = np.mean(proj_gold.waves, axis=0) + gold_waves = proj_gold.get_waves() + gold_trace = np.mean(gold_waves, axis=0) # Pearson correlation: golden trace vs. mean of new traces calc_coeff = scipy.stats.pearsonr(gold_trace, curr_trace).correlation diff --git a/ci/ci_trace_check/golden_traces/aes_128_ecb_static_cw310.zip b/ci/ci_trace_check/golden_traces/aes_128_ecb_static_cw310.zip deleted file mode 100644 index a067551f8..000000000 Binary files a/ci/ci_trace_check/golden_traces/aes_128_ecb_static_cw310.zip and /dev/null differ diff --git a/ci/ci_trace_check/golden_traces/aes_sca_random_golden_cw310.db b/ci/ci_trace_check/golden_traces/aes_sca_random_golden_cw310.db new file mode 100644 index 000000000..d37c16f34 Binary files /dev/null and b/ci/ci_trace_check/golden_traces/aes_sca_random_golden_cw310.db differ