diff --git a/ci/azure-pipelines.yml b/ci/azure-pipelines.yml index 4ebbcc116..45671be95 100644 --- a/ci/azure-pipelines.yml +++ b/ci/azure-pipelines.yml @@ -28,3 +28,11 @@ jobs: - publish: ./ci/ci_projects/opentitan_simple_aes_data/ artifact: traces displayName: "Upload traces" + - bash: | + pushd ci + ./ci_check_aes_traces.sh + popd + displayName: "Capture & check static AES traces" + - publish: ./ci/tmp/ + artifact: traces_aes + displayName: "Upload traces AES." \ No newline at end of file diff --git a/ci/ci_capture_aes_cw310.yaml b/ci/ci_capture_aes_cw310.yaml index 85532f0c4..229df1140 100644 --- a/ci/ci_capture_aes_cw310.yaml +++ b/ci/ci_capture_aes_cw310.yaml @@ -21,10 +21,12 @@ capture: lfsr_seed: 0xdeadbeef batch_prng_seed: 0 scope_gain: 31.5 - num_traces: 1000 + num_traces: 100 project_name: ci_projects/opentitan_simple_aes waverunner_ip: 192.168.1.228 + project_export: True + project_export_filename: ci_projects/opentitan_simple_aes.zip plot_capture: - show: False + show: True num_traces: 100 - trace_image_filename: null + trace_image_filename: ci_projects/sample_traces_aes.html diff --git a/ci/ci_check_aes_traces.sh b/ci/ci_check_aes_traces.sh new file mode 100755 index 000000000..564e43abd --- /dev/null +++ b/ci/ci_check_aes_traces.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# Copyright lowRISC contributors. +# Licensed under the Apache License, Version 2.0, see LICENSE for details. +# SPDX-License-Identifier: Apache-2.0 + +# Simple script to test AES capture. +set -e + +mkdir -p tmp + +# AES +MODE="aes" +BOARD=cw310 +CORR_THRESHOLD=0.8 +declare -A aes_test_list +aes_test_list["aes-static"]=100 + +# ARGS="--force-program-bitstream" +# for test in ${!aes_test_list[@]}; do +# echo Testing ${test} on CW310 - `date` +# NUM_TRACES=${aes_test_list[${test}]} +# ../cw/capture.py --cfg-file ci_capture_aes_cw310.yaml capture ${test} \ +# --num-traces ${NUM_TRACES} ${ARGS} &>> "tmp/test_capture.log" +# +# mv ./ci_projects/sample_traces_${MODE}.html tmp/${test}_traces.html +# ARGS="" +# done + +./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.zip -c ${CORR_THRESHOLD} +ret=$? +if [ $ret -ne 0 ]; then + echo "Trace capture mismatch." + exit 1 +fi \ No newline at end of file diff --git a/ci/ci_trace_check/ci_compare_aes_traces.py b/ci/ci_trace_check/ci_compare_aes_traces.py new file mode 100755 index 000000000..7d47a6f9d --- /dev/null +++ b/ci/ci_trace_check/ci_compare_aes_traces.py @@ -0,0 +1,91 @@ +#!/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 argparse + +import chipwhisperer as cw +import numpy as np +import scipy.stats + + +def analyze_traces(file_proj, file_gold_proj, corr_coeff) -> bool: + """Performs a correlation between golden and new traces. + + This function: + - Computes the mean of the golden and new traces, + - Computes the pearson coefficient of these means, + - Compares the coefficient with the user provided threshold. + + Args: + file_proj: The new Chipwhisperer project file. + file_gold_proj: The golden Chipwhisperer project file. + corr_coeff: User defined correlation threshold. + + Returns: + True if trace comparison succeeds, False otherwise. + """ + # Open the current project + proj_curr = cw.open_project(file_proj) + # Calculate mean of new traces + print(proj_curr.waves) + curr_trace = np.mean(proj_curr.waves, axis=0) + + # Import the golden project + proj_gold = cw.import_project(file_gold_proj) + # Calculate mean of golden traces + gold_trace = np.mean(proj_gold.waves, axis=0) + + # Pearson correlation: golden trace vs. mean of new traces + calc_coeff = scipy.stats.pearsonr(gold_trace, curr_trace).correlation + print(f'Correlation={round(calc_coeff,3)}') + # Fail / pass + if calc_coeff < corr_coeff: + return False + else: + return True + + +def parse_args(): + """Parses command-line arguments.""" + parser = argparse.ArgumentParser( + description="""Calculate Pearson correlation between golden + traces and captured traces. Failes when correlation + coefficient is below user threshold.""" + ) + parser.add_argument( + "-f", + "--file_proj", + required=True, + help="chipwhisperer project file" + ) + parser.add_argument( + "-g", + "--file_gold_proj", + required=True, + help="chipwhisperergolden project file" + ) + parser.add_argument( + "-c", + "--corr_coeff", + type=float, + required=True, + help="specifies the correlation coefficient threshold" + ) + return parser.parse_args() + + +def main() -> int: + """Parses command-line arguments and TODO""" + args = parse_args() + + if analyze_traces(**vars(args)): + print('Traces OK.') + return 0 + else: + print('Traces correlation below threshold.') + return 1 + + +if __name__ == "__main__": + main() diff --git a/ci/ci_trace_check/golden_traces/aes_128_ecb_static.zip b/ci/ci_trace_check/golden_traces/aes_128_ecb_static.zip new file mode 100644 index 000000000..0cd02dec0 Binary files /dev/null and b/ci/ci_trace_check/golden_traces/aes_128_ecb_static.zip differ diff --git a/cw/capture.py b/cw/capture.py index bce16f114..2313d3aae 100755 --- a/cw/capture.py +++ b/cw/capture.py @@ -208,6 +208,45 @@ def capture_loop(trace_gen, ot, capture_cfg, device_cfg): def capture_end(cfg): if cfg["plot_capture"]["show"]: plot_results(cfg["plot_capture"], cfg["capture"]["project_name"]) + if cfg["capture"]["project_export"]: + cw.export(cfg["capture"]["project_export_filename"]) + + +def capture_aes_static(ot): + """A generator for capturing AES traces for fixed key and test. + + Args: + ot: Initialized OpenTitan target. + """ + key = bytearray([0x81, 0x1E, 0x37, 0x31, 0xB0, 0x12, 0x0A, 0x78, + 0x42, 0x78, 0x1E, 0x22, 0xB2, 0x5C, 0xDD, 0xF9]) + text = bytearray([0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, + 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA]) + + tqdm.write(f'Fixed key: {binascii.b2a_hex(bytes(key))}') + + while True: + cipher = AES.new(bytes(key), AES.MODE_ECB) + ret = cw.capture_trace(ot.scope, ot.target, text, key, ack=False, as_int=True) + if not ret: + raise RuntimeError('Capture failed.') + expected = binascii.b2a_hex(cipher.encrypt(bytes(text))) + got = binascii.b2a_hex(ret.textout) + if got != expected: + raise RuntimeError(f'Bad ciphertext: {got} != {expected}.') + yield ret + + +@app_capture.command() +def aes_static(ctx: typer.Context, + force_program_bitstream: bool = opt_force_program_bitstream, + num_traces: int = opt_num_traces, + plot_traces: int = opt_plot_traces): + """Capture AES traces from a target that runs the `aes_serial` program.""" + capture_init(ctx, force_program_bitstream, num_traces, plot_traces) + capture_loop(capture_aes_static(ctx.obj.ot), ctx.obj.ot, + ctx.obj.cfg["capture"], ctx.obj.cfg["device"]) + capture_end(ctx.obj.cfg) def capture_aes_random(ot, ktp):