Initial commit

Initial commit.
This commit is contained in:
kntran1
2026-03-23 14:40:39 -05:00
parent e84b2b4166
commit 4e2a5258a5
872 changed files with 165227 additions and 0 deletions

View File

@@ -0,0 +1,211 @@
# Copyright (c) 2020 Arm Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import argparse
import logging
import struct
import sys
from imgtool.image import (IMAGE_HEADER_SIZE, IMAGE_MAGIC,
TLV_INFO_MAGIC, TLV_PROT_INFO_MAGIC, TLV_VALUES)
from shutil import copyfile
def get_tlv_type_string(tlv_type):
tlvs = {v: f"IMAGE_TLV_{k}" for k, v in TLV_VALUES.items()}
return tlvs.get(tlv_type, "UNKNOWN({:d})".format(tlv_type))
class ImageHeader:
def __init__(self):
self.ih_magic = 0
self.ih_load_addr = 0
self.ih_hdr_size = 0
self.ih_protect_tlv_size = 0
self.ih_img_size = 0
self.ih_flags = 0
self.iv_major = 0
self.iv_minor = 0
self.iv_revision = 0
self.iv_build_num = 0
self._pad1 = 0
@staticmethod
def read_from_binary(in_file):
h = ImageHeader()
(h.ih_magic, h.ih_load_addr, h.ih_hdr_size, h.ih_protect_tlv_size, h.ih_img_size,
h.ih_flags, h.iv_major, h.iv_minor, h.iv_revision, h.iv_build_num, h._pad1
) = struct.unpack('<IIHHIIBBHII', in_file.read(IMAGE_HEADER_SIZE))
return h
def __repr__(self):
return "\n".join([
" ih_magic = 0x{:X}".format(self.ih_magic),
" ih_load_addr = " + str(self.ih_load_addr),
" ih_hdr_size = " + str(self.ih_hdr_size),
" ih_protect_tlv_size = " + str(self.ih_protect_tlv_size),
" ih_img_size = " + str(self.ih_img_size),
" ih_flags = " + str(self.ih_flags),
" iv_major = " + str(self.iv_major),
" iv_minor = " + str(self.iv_minor),
" iv_revision = " + str(self.iv_revision),
" iv_build_num = " + str(self.iv_build_num),
" _pad1 = " + str(self._pad1)])
class ImageTLVInfo:
def __init__(self):
self.format_string = '<HH'
self.it_magic = 0
self.it_tlv_tot = 0
@staticmethod
def read_from_binary(in_file):
i = ImageTLVInfo()
(i.it_magic, i.it_tlv_tot) = struct.unpack('<HH', in_file.read(4))
return i
def __repr__(self):
return "\n".join([
" it_magic = 0x{:X}".format(self.it_magic),
" it_tlv_tot = " + str(self.it_tlv_tot)])
def __len__(self):
return struct.calcsize(self.format_string)
class ImageTLV:
def __init__(self):
self.it_value = 0
self.it_type = 0
self.it_len = 0
@staticmethod
def read_from_binary(in_file):
tlv = ImageTLV()
(tlv.it_type, _, tlv.it_len) = struct.unpack('<BBH', in_file.read(4))
(tlv.it_value) = struct.unpack('<{:d}s'.format(tlv.it_len), in_file.read(tlv.it_len))
return tlv
def __len__(self):
round_to = 1
return int((4 + self.it_len + round_to - 1) // round_to) * round_to
def get_arguments():
parser = argparse.ArgumentParser(description='Corrupt an MCUBoot image')
parser.add_argument("-i", "--in-file", required=True, help='The input image to be corrupted (read only)')
parser.add_argument("-o", "--out-file", required=True, help='the corrupted image')
parser.add_argument('-a', '--image-hash',
default=False,
action="store_true",
required=False,
help='Corrupt the image hash')
parser.add_argument('-s', '--signature',
default=False,
action="store_true",
required=False,
help='Corrupt the signature of the image')
return parser.parse_args()
def damage_tlv(image_offset, tlv_off, tlv, out_file_content):
damage_offset = image_offset + tlv_off + 4
logging.info(" Damaging TLV at offset 0x{:X}...".format(damage_offset))
value = bytearray(tlv.it_value[0])
value[0] = (value[0] + 1) % 256
out_file_content[damage_offset] = value[0]
def is_valid_signature(tlv):
return tlv.it_type == TLV_VALUES['RSA2048'] or tlv.it_type == TLV_VALUES['RSA3072']
def damage_image(args, in_file, out_file_content, image_offset):
in_file.seek(image_offset, 0)
# Find the Image header
image_header = ImageHeader.read_from_binary(in_file)
if image_header.ih_magic != IMAGE_MAGIC:
raise Exception("Invalid magic in image_header: 0x{:X} instead of 0x{:X}".format(image_header.ih_magic, IMAGE_MAGIC))
# Find the TLV header
tlv_info_offset = image_header.ih_hdr_size + image_header.ih_img_size
in_file.seek(image_offset + tlv_info_offset, 0)
tlv_info = ImageTLVInfo.read_from_binary(in_file)
if tlv_info.it_magic == TLV_PROT_INFO_MAGIC:
logging.debug("Protected TLV found at offset 0x{:X}".format(tlv_info_offset))
if image_header.ih_protect_tlv_size != tlv_info.it_tlv_tot:
raise Exception("Invalid prot TLV len ({:d} vs. {:d})".format(image_header.ih_protect_tlv_size, tlv_info.it_tlv_tot))
# seek to unprotected TLV
tlv_info_offset += tlv_info.it_tlv_tot
in_file.seek(image_offset + tlv_info_offset)
tlv_info = ImageTLVInfo.read_from_binary(in_file)
else:
if image_header.ih_protect_tlv_size != 0:
raise Exception("No prot TLV was found.")
logging.debug("Unprotected TLV found at offset 0x{:X}".format(tlv_info_offset))
if tlv_info.it_magic != TLV_INFO_MAGIC:
raise Exception("Invalid magic in tlv info: 0x{:X} instead of 0x{:X}".format(tlv_info.it_magic, TLV_INFO_MAGIC))
tlv_off = tlv_info_offset + len(ImageTLVInfo())
tlv_end = tlv_info_offset + tlv_info.it_tlv_tot
# iterate over the TLV entries
while tlv_off < tlv_end:
in_file.seek(image_offset + tlv_off, 0)
tlv = ImageTLV.read_from_binary(in_file)
logging.debug(" tlv {:24s} len = {:4d}, len = {:4d}".format(get_tlv_type_string(tlv.it_type), tlv.it_len, len(tlv)))
if is_valid_signature(tlv) and args.signature:
damage_tlv(image_offset, tlv_off, tlv, out_file_content)
elif tlv.it_type == TLV_VALUES['SHA256'] and args.image_hash:
damage_tlv(image_offset, tlv_off, tlv, out_file_content)
tlv_off += len(tlv)
def main():
args = get_arguments()
logging.debug("The script was started")
copyfile(args.in_file, args.out_file)
in_file = open(args.in_file, 'rb')
out_file_content = bytearray(in_file.read())
damage_image(args, in_file, out_file_content, 0)
in_file.close()
file_to_damage = open(args.out_file, 'wb')
file_to_damage.write(out_file_content)
file_to_damage.close()
if __name__ == "__main__":
logging.basicConfig(format='%(levelname)5s: %(message)s',
level=logging.DEBUG, stream=sys.stdout)
main()

View File

@@ -0,0 +1,45 @@
# Copyright (c) 2020 Arm Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM ubuntu:jammy
# Get dependencies for retrieving and building TF-M with MCUBoot, and QEMU.
RUN apt-get update && \
DEBIAN_FRONTEND="noninteractive" \
apt-get install -y \
cmake \
curl \
gcc-arm-none-eabi \
gdb-multiarch \
git \
libncurses5 \
python3 \
python3-pip \
qemu-system-arm \
file &&\
apt-get clean all
# Installing python packages
RUN python3 -m pip install \
imgtool>=1.9.0 \
Jinja2>=2.10.3 \
PyYAML \
pyasn1
# Add tfm work directory && get rid of spurious git ownership errors
RUN mkdir -p /root/work/tfm &&\
git config --global --add safe.directory '*'
# run the command
CMD ["bash"]

View File

@@ -0,0 +1,29 @@
#!/bin/sh
# Copyright (c) 2020 Arm Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -e
trap cleanup_exit INT TERM EXIT
cleanup_exit()
{
rm -f *.list *.key
}
export LANG=C
image=mcuboot/fih-test
docker build --pull --tag=$image .

View File

@@ -0,0 +1,68 @@
#!/bin/bash -x
# Copyright (c) 2020-2023 Arm Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -e
source $(dirname "$0")/paths.sh
SKIP_SIZE=$1
BUILD_TYPE=$2
DAMAGE_TYPE=$3
FIH_LEVEL=$4
if test -z "$FIH_LEVEL"; then
# Use the default level
CMAKE_FIH_LEVEL=""
else
CMAKE_FIH_LEVEL="-DMCUBOOT_FIH_PROFILE=\"$FIH_LEVEL\""
fi
# build TF-M with MCUBoot
mkdir -p $TFM_BUILD_PATH $TFM_SPE_BUILD_PATH
cmake -S $TFM_TESTS_PATH/tests_reg/spe \
-B $TFM_SPE_BUILD_PATH \
-DTFM_PLATFORM=arm/mps2/an521 \
-DCONFIG_TFM_SOURCE_PATH=$TFM_PATH \
-DCMAKE_BUILD_TYPE=$BUILD_TYPE \
-DTFM_TOOLCHAIN_FILE=$TFM_PATH/toolchain_GNUARM.cmake \
-DTEST_S=ON \
-DTEST_NS=ON \
-DTFM_PSA_API=ON \
-DMCUBOOT_PATH=$MCUBOOT_PATH \
-DMCUBOOT_LOG_LEVEL=INFO \
$CMAKE_FIH_LEVEL
cmake --build $TFM_SPE_BUILD_PATH -- install
cmake -S $TFM_TESTS_PATH/tests_reg \
-B $TFM_BUILD_PATH \
-DCONFIG_SPE_PATH=$TFM_SPE_BUILD_PATH/api_ns \
-DCMAKE_BUILD_TYPE=$BUILD_TYPE \
-DTFM_TOOLCHAIN_FILE=$TFM_SPE_BUILD_PATH/api_ns/cmake/toolchain_ns_GNUARM.cmake
cmake --build $TFM_BUILD_PATH
cd $TFM_BUILD_PATH
$MCUBOOT_PATH/ci/fih_test_docker/run_fi_test.sh $BOOTLOADER_AXF_PATH $SKIP_SIZE $DAMAGE_TYPE> fih_test_output.yaml
echo ""
echo "test finished with"
echo " - BUILD_TYPE: $BUILD_TYPE"
echo " - FIH_LEVEL: $FIH_LEVEL"
echo " - SKIP_SIZE: $SKIP_SIZE"
echo " - DAMAGE_TYPE: $DAMAGE_TYPE"
python3 $MCUBOOT_PATH/ci/fih_test_docker/generate_test_report.py fih_test_output.yaml
python3 $MCUBOOT_PATH/ci/fih_test_docker/validate_output.py fih_test_output.yaml $SKIP_SIZE $FIH_LEVEL

View File

@@ -0,0 +1,41 @@
#!/bin/bash
# Copyright (c) 2020 Arm Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
OBJDUMP=arm-none-eabi-objdump
GDB=gdb-multiarch
# Check if the ELF file specified is compatible
if test $# -eq 0 || ! file $1 | grep "ELF" | grep "ARM" | grep "32" &>/dev/null; then
echo "Incompatible file: $1" 1>&2
exit 1
fi
# Extract the full path
AXF_PATH=$(realpath $1)
#Dump all objects that have a name containing FIH_LABEL
ADDRESSES=$($OBJDUMP $AXF_PATH -t | grep "FIH_LABEL")
# strip all data except "address, label_name"
ADDRESSES=$(echo "$ADDRESSES" | sed "s/\([[:xdigit:]]*\).*\(FIH_LABEL_FIH_CALL_[a-zA-Z]*\)_.*/0x\1, \2/g")
# Sort by address in ascending order
ADDRESSES=$(echo "$ADDRESSES" | sort)
# In the case that there is a START followed by another START take the first one
ADDRESSES=$(echo "$ADDRESSES" | sed "N;s/\(.*START.*\)\n\(.*START.*\)/\1/;P;D")
# Same for END except take the second one
ADDRESSES=$(echo "$ADDRESSES" | sed "N;s/\(.*END.*\)\n\(.*END.*\)/\2/;P;D")
# Output in CSV format with a label
echo "Address, Type"
echo "$ADDRESSES"

View File

@@ -0,0 +1,204 @@
#!/bin/bash
# Copyright (c) 2020-2022 Arm Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
source $(dirname "$0")/paths.sh
function skip_instruction {
local SKIP_ADDRESS=$1
local SKIP_SIZE=$2
# Parse the ASM instruction from the address using gdb
INSTR=$($GDB $AXF_FILE --batch -ex "disassemble $SKIP_ADDRESS" | grep "^ *$SKIP_ADDRESS" | sed "s/.*:[ \t]*\(.*\)$/\1/g")
# Parse the C line from the address using gdb
LINE=$($GDB $AXF_FILE --batch -ex "info line *$SKIP_ADDRESS" | sed "s/Line \([0-9]*\).*\"\(.*\)\".*/\2:\1/g")
# Sometimes an address is in the middle of a 4 byte instruction. In that case
# don't run the test
if test "$INSTR" == ""; then
return
fi
# Print out the meta-info about the test, in YAML
echo "- skip_test:"
echo " addr: $SKIP_ADDRESS"
echo " asm: \"$INSTR\""
echo " line: \"$LINE\""
echo " skip: $SKIP_SIZE"
# echo -ne "$SKIP_ADDRESS | $INSTR...\t"
cat >commands.gdb <<EOF
target remote localhost: 1234
file $AXF_FILE
b boot_go_for_image_id if image_id == 0
continue
delete breakpoints 1
b *$SKIP_ADDRESS
continue&
eval "shell sleep 0.5"
interrupt
if \$pc == $SKIP_ADDRESS
echo "Stopped at breakpoint"
else
echo "Failed to stop at breakpoint"
end
echo "PC before increase:"
print \$pc
set \$pc += $SKIP_SIZE
echo "PC after increase:"
print \$pc
detach
eval "shell sleep 0.5"
EOF
echo -n '.' 1>&2
# start qemu, dump the serial output to $QEMU_LOG_FILE
QEMU_LOG_FILE=qemu.log
QEMU_PID_FILE=qemu_pid.txt
rm -f $QEMU_PID_FILE $QEMU_LOG_FILE
/usr/bin/qemu-system-arm \
-M mps2-an521 \
-s -S \
-kernel $AXF_FILE \
-device loader,file=$TFM_IMAGE_PATH,addr=0x10080000 \
-chardev file,id=char0,path=$QEMU_LOG_FILE \
-serial chardev:char0 \
-display none \
-pidfile $QEMU_PID_FILE \
-daemonize
# start qemu, skip the instruction, and continue execution
$GDB < ./commands.gdb &>gdb_out.txt
# kill qemu
kill -9 `cat $QEMU_PID_FILE`
# If "Secure image initializing" is seen the TFM booted, which means that a skip
# managed to defeat the signature check. Write out whether the image booted or
# not to the log in YAML
if cat $QEMU_LOG_FILE | grep -i "Starting bootloader" &>/dev/null; then
# bootloader started successfully
if cat gdb_out.txt | grep -i "Stopped at breakpoint" &>/dev/null; then
# The target was stopped at the desired address
if cat $QEMU_LOG_FILE | grep -i "Secure image initializing" &>/dev/null; then
echo " test_exec_ok: True"
echo " skipped: True"
echo " boot: True"
#print the address that was skipped, and some context to the console
echo "" 1>&2
echo "Boot success: address: $SKIP_ADDRESS skipped: $SKIP_SIZE" 1>&2
arm-none-eabi-objdump -d $AXF_FILE --start-address=$SKIP_ADDRESS -S | tail -n +7 | head -n 14 1>&2
echo "" 1>&2
echo "" 1>&2
else
LAST_LINE=`tail -n 1 $QEMU_LOG_FILE | tr -dc '[:print:]'`
echo " test_exec_ok: True"
echo " skipped: True"
echo " boot: False"
echo " last_line: \"$LAST_LINE\" "
fi
else
# The target was not stopped at the desired address.
# The most probable reason is that the instruction for that address is
# on a call path that is not taken in this run (e.g. error handling)
if cat $QEMU_LOG_FILE | grep -i "Secure image initializing" &>/dev/null; then
# The image booted, although it shouldn't happen as the test is to
# be run with a corrupt image.
echo " test_exec_ok: False"
echo " test_exec_fail_reason: \"No instructions were skipped (e.g. branch was not executed), but booted successfully\""
else
# the execution didn't stop at the address (e.g. the instruction
# is on a branch that is not taken)
echo " test_exec_ok: True"
echo " skipped: False"
fi
fi
else
# failed before the first printout
echo " test_exec_ok: True"
echo " skipped: True"
echo " boot: False"
echo " last_line: 'N/A' "
fi
}
# Inform how the script is used
usage() {
echo "$0 <image_dir> <start_addr> [<end_addr>] [(-s | --skip) <skip_len>]"
}
#defaults
SKIP=2
AXF_FILE=${BOOTLOADER_AXF_PATH}
GDB=gdb-multiarch
BOOTLOADER=true
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
-s|--skip)
SKIP="$2"
shift
shift
;;
-h|--help)
usage
exit 0
;;
*)
if test -z "$IMAGE_DIR"; then
IMAGE_DIR=$1
elif test -z "$START"; then
START=$1
elif test -z "$END"; then
END=$1
else
usage
exit 1
fi
shift
;;
esac
done
# Check that image directory, start and end address have been supplied
if test -z "$IMAGE_DIR"; then
usage
exit 2
fi
if test -z "$START"; then
usage
exit 2
fi
if test -z "$END"; then
END=$START
fi
if test -z "$SKIP"; then
SKIP='2'
fi
# Create the start-end address range (step 2)
ADDRS=$(printf '0x%x\n' $(seq "$START" 2 "$END"))
# For each address run the skip_instruction function on it
for ADDR in $ADDRS; do
skip_instruction $ADDR $SKIP
done

View File

@@ -0,0 +1,47 @@
# Copyright (c) 2020-2023 Arm Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import argparse
from utils import CATEGORIES, parse_yaml_file
def print_results(results):
test_stats, failed_boot_last_lines, exec_fail_reasons = results
print("{:s}: {:d}.".format(CATEGORIES['TOTAL'], test_stats[CATEGORIES['TOTAL']]))
print("{:s} ({:d}):".format(CATEGORIES['SUCCESS'], test_stats[CATEGORIES['SUCCESS']]))
print(" {:s}: ({:d}):".format(CATEGORIES['ADDRES_NOEXEC'], test_stats[CATEGORIES['ADDRES_NOEXEC']]))
test_with_skip = test_stats[CATEGORIES['SUCCESS']] - test_stats[CATEGORIES['ADDRES_NOEXEC']]
print(" {:s}: ({:d}):".format(CATEGORIES['SKIPPED'], test_with_skip))
print(" {:s} ({:d}):".format(CATEGORIES['NO_BOOT'], test_with_skip - test_stats[CATEGORIES['BOOT']]))
for last_line in failed_boot_last_lines.keys():
print(" last line: {:s} ({:d})".format(last_line, failed_boot_last_lines[last_line]))
print(" {:s} ({:d})".format(CATEGORIES['BOOT'], test_stats[CATEGORIES['BOOT']]))
print("{:s} ({:d}):".format(CATEGORIES['FAILED'], test_stats[CATEGORIES['TOTAL']] - test_stats[CATEGORIES['SUCCESS']]))
for reason in exec_fail_reasons.keys():
print(" {:s} ({:d})".format(reason, exec_fail_reasons[reason]))
def main():
parser = argparse.ArgumentParser(description='''Process a FIH test output yaml file, and output a human readable report''')
parser.add_argument('filename', help='yaml file to process')
args = parser.parse_args()
results = parse_yaml_file(args.filename)
print_results(results)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,10 @@
WORK_PATH=/root/work/tfm
MCUBOOT_PATH=$WORK_PATH/mcuboot
TFM_PATH=$WORK_PATH/trusted-firmware-m
TFM_TESTS_PATH=$WORK_PATH/tf-m-tests
TFM_SPE_BUILD_PATH=$TFM_PATH/build_spe
TFM_BUILD_PATH=$TFM_PATH/build
BOOTLOADER_AXF_PATH=$TFM_SPE_BUILD_PATH/bin/bl2.axf
TFM_IMAGE_NAME=tfm_s_ns_signed.bin
TFM_IMAGE_OUTPUT_PATH=$TFM_BUILD_PATH
TFM_IMAGE_PATH=$TFM_IMAGE_OUTPUT_PATH/$TFM_IMAGE_NAME

View File

@@ -0,0 +1,88 @@
#!/bin/bash
# Copyright (c) 2020 Arm Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set -e
# Get the dir this is running in and the dir the script is in.
PWD=$(pwd)
DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )
# PAD is the amount of extra instructions that should be tested on each side of
# the critical region
PAD=6
MCUBOOT_AXF=$1
SKIP_SIZES=$2
DAMAGE_TYPE=$3
source $(dirname "$0")/paths.sh
# Take an image and make it unbootable. This is done by replacing one of the
# strings in the image with a different string. This causes the signature check
# to fail
function damage_image
{
local IMAGE_NAME=${TFM_IMAGE_NAME}
local BACKUP_IMAGE_NAME=${TFM_IMAGE_NAME}.orig
local IMAGE=$TFM_IMAGE_OUTPUT_PATH/$IMAGE_NAME
mv $IMAGE $TFM_IMAGE_OUTPUT_PATH/$BACKUP_IMAGE_NAME
if [ "$DAMAGE_TYPE" = "SIGNATURE" ]; then
DAMAGE_PARAM="--signature"
elif [ "$DAMAGE_TYPE" = "IMAGE_HASH" ]; then
DAMAGE_PARAM="--image-hash"
else
echo "Failed to damage image $IMAGE with param $DAMAGE_TYPE" 1>&2
exit -1
fi
python3 $DIR/damage_image.py -i $TFM_IMAGE_OUTPUT_PATH/$BACKUP_IMAGE_NAME -o $IMAGE $DAMAGE_PARAM 1>&2
}
function run_test
{
local SKIP_SIZE=$1
$DIR/fi_make_manifest.sh $MCUBOOT_AXF > $PWD/fih_manifest.csv
# Load the CSV FI manifest file, and output in START, END lines. Effectively
# join START and END lines together with a comma seperator.
REGIONS=$(sed "N;s/\(0x[[:xdigit:]]*\).*START\n\(0x[[:xdigit:]]*\).*END.*/\1,\2/g;P;D" $PWD/fih_manifest.csv)
# Ignore the first line, which includes the CSV header
REGIONS=$(echo "$REGIONS" | tail -n+2)
for REGION in $REGIONS; do
#Split the START,END pairs into the two variables
START=$(echo $REGION | cut -d"," -f 1)
END=$(echo $REGION | cut -d"," -f 2)
# Apply padding, converting back to hex
START=$(printf "0x%X" $((START - PAD)))
END=$(printf "0x%X" $((END + PAD)))
# Invoke the fi tester script
$DIR/fi_tester_gdb.sh $TFM_IMAGE_OUTPUT_PATH $START $END --skip $SKIP_SIZE
done
}
damage_image $MCUBOOT_AXF
# Run the run_test function with each skip length between min and max in turn.
IFS=', ' read -r -a sizes <<< "$SKIP_SIZES"
for size in "${sizes[@]}"; do
echo "Run tests with skip size $size" 1>&2
run_test $size
done

View File

@@ -0,0 +1,63 @@
# Copyright (c) 2023 Arm Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import collections
import yaml
CATEGORIES = {
'TOTAL': 'Total tests run',
'SUCCESS': 'Tests executed successfully',
'FAILED': 'Tests failed to execute successfully',
# the execution never reached the address
'ADDRES_NOEXEC': 'Address was not executed',
# The address was successfully skipped by the debugger
'SKIPPED': 'Address was skipped',
'NO_BOOT': 'System not booted (desired behaviour)',
'BOOT': 'System booted (undesired behaviour)'
}
def parse_yaml_file(filepath):
with open(filepath) as f:
results = yaml.safe_load(f)
if not results:
raise ValueError("Failed to parse output yaml file.")
test_stats = collections.Counter()
failed_boot_last_lines = collections.Counter()
exec_fail_reasons = collections.Counter()
for test in results:
test = test["skip_test"]
test_stats.update([CATEGORIES['TOTAL']])
if test["test_exec_ok"]:
test_stats.update([CATEGORIES['SUCCESS']])
if "skipped" in test.keys() and not test["skipped"]:
# The debugger didn't stop at this address
test_stats.update([CATEGORIES['ADDRES_NOEXEC']])
continue
if test["boot"]:
test_stats.update([CATEGORIES['BOOT']])
continue
failed_boot_last_lines.update([test["last_line"]])
else:
exec_fail_reasons.update([test["test_exec_fail_reason"]])
return test_stats, failed_boot_last_lines, exec_fail_reasons

View File

@@ -0,0 +1,39 @@
# Copyright (c) 2023 Arm Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import argparse
from utils import CATEGORIES, parse_yaml_file
def validate_output(test_stats, skip_size, fih_level):
if (test_stats[CATEGORIES['BOOT']] > 0
and skip_size == "2,4,6" and fih_level == "MEDIUM"):
raise ValueError("The number of sucessful boots was more than zero")
def main():
parser = argparse.ArgumentParser(description='''Process a FIH test output yaml file,
and validate no sucessfull boots have happened''')
parser.add_argument('filename', help='yaml file to process')
parser.add_argument('skip_size', help='instruction skip size')
parser.add_argument('fih_level', nargs="?",
help='fault injection hardening level')
args = parser.parse_args()
test_stats = parse_yaml_file(args.filename)[0]
validate_output(test_stats, args.skip_size, args.fih_level)
if __name__ == "__main__":
main()