Contents
Welcome to the documentation for PAF, the Physical Attack Framework. PAF is a suite of tools and libraries to learn about physical attacks, such as fault injection and side channels, and hopefully help harden code bases against those threats.
Physical attacks are a specific threat to systems, where systems are considered in their entirety, i.e. as software + hardware implementations. Those attacks are taking advantage of physical parameters.
In the case of side channel attacks, the aim of the attacker is, by observing some measurable physical quantities (e.g. time, power consumption, ...), to guess some details about the execution of program. Those details can then be used either to reduce the complexity of a brute force attack (for password guessing for example), or to directly recover a secret (the secret key used in AES for example)
For fault injection, the attacker will physically affect the correct behavior of the silicon. There are many ways to alter the correct functionality of logic gates, flip-flops or memory cells: power supply glitching, clock glitching, EM pulse, laser beam, ... One has to note that physical access is not necessarily required, RowHammer and ClkScrew being two noticeable examples.
It is also worth considering physical attacks from a different point of view:
- passive attacks, where an attacker monitors a system's physical quantities (time, power consumption, electro-magnetic emissions, ...) to derive information leaked by the system's implementation, without trying to affect the system's behavior in anyway.
- active attacks, where the attacker actively attempts to affect the system behavior in some way. This is for example the case with fault injection, where an attacker, using a voltage or clock glitch for example, will attempt to derail the program from its expected execution. But this is also the case for some timing side channel attacks, like cache attacks, where the attacker will change the state of the CPU caches to guess what the program under attacks does (by measuring differences in execution time).
Physical attacks will not always lead to the direct exploitation ; they are often a necessary step though, a prelude to the real exploit, e.g. firmware extraction with fault injection for offline analysis, or side channel leakages can reduce the complexity of an attack on cryptographic code, making brute force attack possible.
Tarmac traces are detailed traces of a program's execution that are generated by a number of Arm products, like software models of CPUs (e.g. FastModel). A detailed description of the Tarmac traces can be found in the documentation of tarmac-trace-utilities that PAF relies on for analyzing traces.
Arm's Fast Models are accurate, flexible programmer's view models of Arm IP, allowing you to develop software such as drivers, firmware, OS and applications prior to silicon availability. They allow full control over the simulation, including profiling, debug and trace. A significant portion of PAF relies on having a FastModel available, please refer to FasttModel to get yourself one.
This sections intends to introduce a number of concepts used in PAF and be a walk-thru of PAF's capabilities.
PAF's fault injection capabilities relies on using an Arm FastModel driven by
the run-model.py
tool.
For example, let's assume that we have a program program.elf
for which we
want to check the resistance against fault injection.
Fault injection is performed in three steps:
Get a reference trace of a normal execution of
program.elf
by running it in simulation mode with run-model.py, without any fault:$ run-model.py -t program.trace program.elf
Analyze the Tarmac reference trace to produce a fault campaign file: given a Fault model and a place of interest for injection (because one is interested in attacking a specific part of the program, not the complete program), the paf-faulter tool will produce a list of all faults to inject as well as some more ancillary data useful for the fault injection in a so-called Fault campaign file.
$ paf-faulter --instructionskip \ --oracle='@(fault_occurred){success};@(crash_detected){crash};return(main){noeffect}' \ --output=campaign.yml \ --image=program.elf --functions=checkPIN program.trace
Execute again
program.elf
with run-model.py, but this time in fault injection mode. This will run the program as many times as there are faults in the campaign, and will classify the fault effects according to Fault classification.$ run-model.py --driver=FaultInjection -c campaign.yml program.elf 41 faults to inject. 100%|##############################################| 41/41 [00:07<00:00, 5.23 faults/s] 41 faults injected: 11 successful, 0 caught, 28 noeffect, 2 crash and 0 undecided
Faults are fundamentally taking place at the transistor level, which makes fault injection simulation at that level of details not so much tractable in practice. Instead, PAF's fault injection simulation relies on fault models, which are a high level abstraction of faults' effects. For example, for now PAF supports:
- InstructionSkip: this models the effect of faults for which the instruction appears not to be executed.
- RegisterDefinitionCorruption: this models the effect of faults that appears to corrupt the destination operand of an instruction.
- Many more fault models can easily be implemented, e.g. memory corruption, or source operand corruption are on the top of the list
All models are wrong (in some way), because they are abstractions of a more
complex underlying reality, but they remain useful to analyze the behavior of
a piece of code under different scenarios. It's also worth mentioning that
different models can make a program exhibit the same behavior, or said
differently, different fault models can be used to model a similar effect ; for
example, in a sequence of instructions like CMP + BNE
(a comparison flowed
by a conditional branch), the effect of skipping the BNE
can be equally
done with faulting the program status register set by the CMP
instruction.
A fault campaign is a container with all information needed to perform a fault injection campaign: information about a program, the fault model used, and the list of all fault to inject together with the details of how to inject them.
When analyzing the resistance of a program against fault attacks, it's useful to classify the faults according to their effects:
- success: the fault was injected and had an effect on the behavior of the program that can be considered a successful attack.
- noeffect: the fault was injected, but did not have a noticeable impact on the behavior of the program. This might be true, but this could also be because the Oracle was not defined precisely enough.
- crash: faults do mess-up the code in many ways (e.g. accesses to invalid memory, unaligned accesses, ...), which are often capture by exception handlers. Note that classifying a fault effect as a crash does not mean the fault can not be successful ! It only means that the fault effect will depend on how the the exception handlers are setup and will manage the exception. The crash classification should be used when it is not known what will happen exactly, because for example the exception handlers behavior are managed by a different team, and further thinking is needed.
- caught: this classification is useful when a program has protections
against fault injections. These protections, on top of passive measures like
redundancy often come with an active aspect, where the program will change
and adapt its behavior when it becomes suspicious of a fault injection. In
the literature, this is often the
kill_card
function that gets invoked to wipe out all secrets for example. It is useful, when testing the resistance of a program to be able to classify the faults that have been caught by the protection schemes. - notrun: this classification is for faults which have not been injected. It's useful in reports to be able mark them as notrun.
- undecided: faults can alter the control flow of a program, and knowing when to halt the simulation is a hard problem. In some cases, the program can still be in the valid control flow (compared to the reference execution), but locked in an infinite loop, or may be a few more cycles of simulation would have enabled to conclude. This classification usually appears when some sort of timeouts set to the simulation have triggered.
The oracle is in charge of classifying the effect of a fault. A fault classification is attempted at specific events, and involves inspecting the state of a program. As such, this is an event based process, with some first order logical formulae referring to program registers and variables. There is captured in a mini-DSL.
A simplified pseudo-grammar for the Oracle-DSL looks like:
classifier ::= event { classification }
event ::= @ (
function
) | return (function
)classification ::=
success
|noeffect
|crash
|caught
|notrun
|undecided
The triggering event is either a call to or a return from function
. In
the full Oracle-DSL, classification is a first order formula, which is
simplified here to always return the fault classification.
Multiple classifiers can be added to an oracle.
When protecting against side channels, one of the first (not so) obvious step is to harden against timing side channels. A timing side channel exist when depending on some sensitive input (like a secret), the program will have a different behavior. The most obvious difference is execution time, i.e. when program execution differs in time. A desirable goal is thus to ensure the sensitive part of a program executes in constant-time, that's to say independent of the sensitive data values.
In this example, we will see how a non-constant time behavior can be found
with PAF. The simplistic check
program below compare pin digits. For the
sake of the example, it is made non constant time in an explicit way, as the
pin comparison exit early as soon as a difference is found:
$ cat check.c
#include <stdio.h>
#define DIGITS 4
char pin[DIGITS] = "1234";
int main(int argc, char \*argv[]) {
if (argc > 1) {
for (unsigned i = 0; i < DIGITS; i++)
if (argv[1][i] != pin[i])
return 0;
return 1;
}
return 0;
}
The program is then compiled, then simulated with run-model.py with different input PIN values. We have used here two well chosen value for the sake of illustration, but in practice one could be using fuzzing for example to explore a number of other values:
$ arm-none-eabi-gcc -o check.elf -O2 -Wall -mthumb -mcpu=cortex-m3 check.c --specs=rdimon.specs
$ run-model.py -u FVP_MPS2_M3.yml -s -t check1.trace check.elf 1344
$ run-model.py -u FVP_MPS2_M3.yml -s -t check2.trace check.elf 1244
Now that we have a number of execution traces captures with different inputs, these can be compared by paf-constanttime, a utility that will report divergences in Tarmac traces:
$ paf-constanttime --image=check.elf main check1.trace check2.trace
index file check1.trace.index is older than trace file check1.trace; rebuilding it
index file check2.trace.index is older than trace file check2.trace; rebuilding it
Running analysis on trace 'check1.trace'
- Building reference trace from main instance at time : 698 to 715
698 X CMP r0,#1
699 - BLE {pc}+0x1a
700 X LDR r1,[r1,#4] R4(0x1a066)@0x106ffff8
701 X LDR r2,{pc}+0x1e R4(0x1a164)@0x8050
702 X SUBS r3,r1,#1
703 X ADDS r1,#3
704 X LDRB r12,[r3,#1]! R1(0x31)@0x1a066
705 X LDRB r0,[r2],#1 R1(0x31)@0x1a164
706 X CMP r12,r0
707 - BNE {pc}+0xa
708 X CMP r3,r1
709 X BNE {pc}-0xe
710 X LDRB r12,[r3,#1]! R1(0x33)@0x1a067
711 X LDRB r0,[r2],#1 R1(0x32)@0x1a165
712 X CMP r12,r0
713 X BNE {pc}+0xa
714 X MOVS r0,#0
715 X BX lr
Running analysis on trace 'check2.trace'
- Comparing reference to instance at time : 698 to 721
o Time:713 Executed:1 PC:0x8042 ISet:1 Width:16 Instruction:0xd103 BNE {pc}+0xa (reference)
Time:713 Executed:0 PC:0x8042 ISet:1 Width:16 Instruction:0xd103 BNE {pc}+0xa
o Time:714 Executed:1 PC:0x804c ISet:1 Width:16 Instruction:0x2000 MOVS r0,#0 (reference)
Time:714 Executed:1 PC:0x8044 ISet:1 Width:16 Instruction:0x428b CMP r3,r1
In this case, paf-constanttime
has found 2 divergences:
- at time 713, depending on the input value, the instruction at PC: 0x8042 was executed (or not).
- at time 714, thus following the difference in control flow, 2 different instructions are executed.
Another source of side channel leakage are the system's power consumption and its electro-magnetic emissions, because the power consumption (and EM emission) depends on the instruction being executed as well as the data manipulated by this instruction. By recording power trace of the system executing with different data, and analyzing their behavior with statistical analysis tools, he might be able to derive some useful information, if not directly a secret information. Those type of attacks require manipulating a large amount of tabular recorded data, so PAF has not re-created the wheel and reuses a commonly used container for storing those traces: NumPy arrays. Reusing this standard storage has additional benefits:
- NumPy arrays can be used natively in other environments than PAF, e.g. python or Jupiter notebooks,
- NumPy arrays can be exported by power trace acquisition environment, including NewAE <https://www.newae.com/>_ ChipWhisperer environment,
making it a de-facto must-use container.
PAF's side channel analysis tools are however written in C++, so PAF's include
a class, NPArray
to manipulate simple 1D or 2D arrays. More complex data
structures supported by the NumPy format are not supported. As a consequence,
different types of data are stored in different files ; for example the power
acquisition trace intrinsically has floating point values and will be stored as
such, whereas the input values that were used to generate that trace are often
integer values.
PAF makes some assumptions on how data are stored in the numpy files. PAF
expects the row major order to be used. For example, let's assume that you want
to use 100 traces of 20 samples each, and that each trace was using 4 data,
then you should have 100 x 20 numpy array of doubles
(in file say
traces.npy
) and another 100 x 4 numpy array of uint32_t
(in file say
inputs.npy
).
While PAF's power analysis is performed on a power trace in in NumpPy format, there are many ways to collect such traces:
- real power acquisition from a development board, like the chipwhisperers from NewAE for example. These boards are very easy to use, and provide all the required equipment for capturing the board power consumption at a very reasonable price compared to buying some lab equipment (power supplies, scope, ...). They also come with support software that makes them very easy to use. A notable framework that makes use of these to study side channels is lip6dromel <https://gitlab.lip6.fr/heydeman/lip6dromel>.
- power estimation from an RTL simulation. Verilog (and VHDL) simulations can
record the signals states and transitions (known as waveforms) to a file in
vcd
format (Value Change Dump) for example. This can be read and analyzed bywan-power
a tool provided by PAF to compute a power trace. Tracesfst
format are also supported. - power estimation from an ISA simulation. FastModels can record the instruction
trace in the so-called tarmac format, and
paf-power
can read and analyze these to compute a power trace.
PAF relies on tarmac-trace-utilities for all its functionality related to tarmac trace analysis. As such, it will give access to all tools provided by the Tarmac Trace Utilities:
tarmac-browser
: a terminal-based interactive browser for trace files.tarmac-callinfo
: reports on calls to a specific function or address.tarmac-calltree
: displays the full hierarchy of function calls identified in the trace.tarmac-flamegraph
: writes out profiling data derived from the trace file, in a format suitable for use with the 'FlameGraph' tools that can be found at https://github.com/brendangregg/FlameGraph.tarmac-gui-browser
: is a GUI-based interactive browser for trace files.tarmac-profile
: prints out simple profiling data derived from the trace file, showing the amount of time spent in every function.tarmac-vcd
: translates the trace file into Value Change Dump.
For more detailled information on those tools, please refer to their documentation.
run-model.py
is a driver for Arm's FastModel. It uses the FastModel Iris
interface to control the simulation and make it do more than just running some
code. It assumes that a FastModel is installed, and it expects the environment
variable IRIS_HOME
to be set and point to where the Iris python module can
be found.
- The command line syntax looks like:
run-model.py
[ options ] elf_image [ image_args+ ]
run-model.py
drives the Arm's FastModel simulation in different ways
depending on the driver it has been invoked with:
- plain simulation mode: this is the standard operating mode of the FastModel.
This is the
IrisDriver
and is the default driver. - fault injection mode: in this mode,
run-model.py
will run the simulation as many times as there are faults in the user supplied fault campaign file, and at each run inject a fault and try to classify it according to the oracle. - check-point mode: in this mode,
run-model.py
will stop the simulation at some user specified point and perform a number of checks (register content, memory values, ...). It's essentially equivalent to setting a breaking in a debugger and inspecting the program state. - data-override mode: in this mode,
run-model.py
will pause the simulation at a user specified location (typically a function entry), and will override data in memory with user provided data. The simulation will then resume its course. This is useful for checking some hypothesis, or using the same binary, without recompilation for example.
Arm's FastModel are versatile and can represent lots of different systems, with
variant configurations and thus options. run-model.py
can make use of a
so-called user session file which will ease the FastModel run configuration.
A typical session file will look like:
Model: "/opt/FastModels/11.12/FVP_MPS2_Cortex-M3_CC312/models/Linux64_GCC-6.4/FVP_MPS2_Cortex-M3_CC312"
PluginsDir: "/opt/FastModels/11.12/FastModelsPortfolio_11.12/plugins/Linux64_GCC-6.4"
Verbosity:
- {Option: false, Name: "fvp_mps2.telnetterminal0.quiet", Value: 1}
- {Option: false, Name: "fvp_mps2.telnetterminal1.quiet", Value: 1}
- {Option: false, Name: "fvp_mps2.telnetterminal2.quiet", Value: 1}
GUI:
- {Option: false, Name: "fvp_mps2.mps2_visualisation.disable-visualisation", Value: 1}
SemiHosting:
Enable: {Name: "armcortexm3ct.semihosting-enable", Value: 1}
CmdLine: {Name: "armcortexm3ct.semihosting-cmd_line", Value: ""}
The Model
and PluginsDir
fields have to be adapted to your specific
installation of the Arm FastModel. Model
points to where the FastModel
executable has been installed, whereas PluginsDir
points to where plugins,
like the one needed for recording Tarmac traces can be found (e.g
TarmacTrace.so
in a linux installation).
The Verbosity
, GUI
and SemiHosting
dictionaries are used by
run-model.py
to perform the right actions on the model when the verbosity
is increased (-v
), or when the GUI is requested (-gui
), or when
semi-hosting is used (--enable-semihosting
). They contain option polarity,
and the Name
field correspond to a parameter in the Arm FastModel.
run-model.py
positional arguments are:
elf_image
- The ELF image to load.
image_args
- The ELF image arguments.
run-model.py
supports the following optional arguments:
-h
or--help
- Show this help message and exit
-v
or--verbose
- Be more verbose, may be specified multiple times.
-V
or--version
- Print the version number of this tool.
-s
or--enable-semihosting
- Use semihosting for passing arguments and getting the exit value
-g
or--enable-remote-gdb
- Enable the remote debug server. You can then point your debugger to 127.0.0.1:31627 ('gdb-remote 127.0.0.1:31627' in LLDB)
-l SECONDS
or--cpu-limit SECONDS
- Set a time limit on the host cpu to the simulation (default:0).
-t [TRACE]
or--enable-trace [TRACE]
- Trace instructions to file TRACE if provided, elf_image.trace otherwise
-d {IrisDriver,FaultInjection,CheckPoint,DataOverrider}
or--driver {IrisDriver,FaultInjection,CheckPoint,DataOverrider}
- Set the simulation driver to use
-c CampaignFile
or--driver-cfg CampaignFile
- simulation driver configuration to use (a.k.a fault injection campaign)
-f FaultIds
or--fault-ids FaultIds
- A comma separated list of fault Ids or Ids range to run (from the fault injection campaign)
-j NUM
or--jobs NUM
- Number of fault injection jobs to run in parallel (default: 1)
--hard-psr-fault
- With the CorruptRegDef model, fault the full PSR instead of just the CC
--reg-fault-value {reset,one,set}
- With the register fault models, reset the register, set it to 1 or set it to all 1s
--gui
- Enable the fancy gui from the FVP
--override-when-entering FUNC
- override data when entering function FUNC
--override-symbol-with SYMBOL:BYTESTRING[,SYMBOL:BYTESTRING]
- Override SYMBOL with bytes from BYTESTRING
--ignore-return-value
- Ignore the return value from semihosting or from the simulator
--dry-run
- Don't actually run the simulator, just print the command line that would be used to run it
-u SessionCfgFile
or--user-cfg SessionCfgFile
- Defines the model meaningful options for you in your environment
--stat
- Print run statistics on simulation exit
--iris-port PORT
- Set the base iris port number to use (default:7100)
--start-address ADDRESS
- Set the PC at ADDRESS at the start of simulation
--exit-address ADDRESSES
- Stop and exit simulation when PC matches any address in ADDRESSES. ADDRESSES is interpreted as a comma separated list of symbol names or addresses
--data binary
- Data loading and placement
Here are a few example usage of run-model.py
. In the first example, one
simply executes the canonical "Hello World !" on a Cortex-M3, using
semi-hosting:
$ cat Hello.c
#include <stdio.h>
int main(int argc, char *argv[]) {
const char *someone = "World";
if (argc>1)
someone = argv[1];
printf("Hello, %s !", someone);
return 0;
}
$ arm-none-eabi-gcc -o Hello.elf -O2 -Wall -mthumb -mcpu=cortex-m3 Hello.c --specs=rdimon.specs
$ run-model.py -u FVP_MPS2_M3.yml -s Hello.elf
$ cat Hello.elf.stdout
Hello, World !
But as semi-hosting is used, one can also pass parameters to the program.
$ run-model.py -u FVP_MPS2_M3.yml -s Hello.elf Bob
$ cat Hello.elf.stdout
Hello, Bob !
One could also record a Tarmac trace with:
$ run-model.py -u FVP_MPS2_M3.yml -s -t Hello.trace Hello.elf Bob
$ head Hello.trace
0 clk E DebugEvent_HaltingDebugState 00000000
0 clk R cpsr 01000000
0 clk SIGNAL: SIGNAL=poreset STATE=N
0 clk SIGNAL: SIGNAL=poreset STATE=N
0 clk E 000080ac 00000001 CoreEvent_RESET
0 clk R r13_main 464c457c
0 clk R MSP 464c457c
1 clk IT (1) 000080ac 2016 T thread : MOVS r0,#0x16
1 clk R r0 00000016
1 clk R cpsr 01000000
campaign.py
is a utility script to perform a number of actions on campaign
files, from displaying a summary to modifying some fields in an automated way.
- The command line syntax looks like:
campaign.py
[ -h ] [ -v ] [ -V ] [ --offset-fault-time-by OFFSET ] [ --offset-fault-address-by OFFSET ] [ --summary ] [ --dry-run ] CAMPAIGN_FILE [CAMPAIGN_FILE...]
where CAMPAIGN_FILE denotes a campaign file to process.
The available actions to perform on the CAMPAIGN_FILEs are:
--offset-fault-time-by OFFSET
- Offset all fault time by OFFSET
--offset-fault-address-by OFFSET
- Offset all fault addresses by OFFSET
--summary
- Display a summary of the campaign results
campaign.py
supports the following optional arguments:
-h
or--help
- Show this help message and exit
-v
or--verbose
- Be more verbose, may be specified multiple times.
-V
or--version
- Print the version number of this tool.
--dry-run
- Perform the action, but don't save the file and dump it for visual inspection.
As an example, one can get a summary report of a fault injection campaign with:
$ campaign.py --summary verifyPIN-O2.is.yml.results
41 faults: 0 caught, 2 crash, 28 noeffect, 0 notrun, 11 success, 0 undecided
which let us know that 41 faults were injected, that 11 led to a successful attack, that 2 crashed somehow the program and the 28 had no noticeable effect.
Given a fault model (e.g. instruction skip), paf-faulter
will analyze a
reference instruction trace in the Tarmac format and produce a fault injection
campaign file.
- The command line syntax looks like:
paf-faulter
[ options ] TRACEFILE
The following options are recognized:
--image=IMAGEFILE
- Image file name
--only-index
- Generate index and do nothing else
--force-index
- Regenerate index unconditionally
--no-index
- Do not regenerate index
--li
- Assume trace is from a little-endian platform
--bi
- Assume trace is from a big-endian platform
-v
or--verbose
- Make tool more verbose
-q
or--quiet
- Make tool quiet
--show-progress-meter
- Force display of the progress meter
--index=INDEXFILE
- Index file name
--instructionskip
- Select InstructionSkip faultModel
--corruptregdef
- Select CorruptRegDef faultModel
--output=CAMPAIGNFILE
- Campaign file name
--oracle=ORACLESPEC
- Oracle specification
--window-labels=WINDOW,LABEL[,LABEL+]
- A pair of labels that delimit the region where to inject faults.
--labels-pair=START_LABEL,END_LABEL
- A pair of labels that delimit the region where to inject faults.
--flat-functions=FUNCTION[,FUNCTION]+
- A comma separated list of function names where to inject faults into (excluding their call-tree)
--functions=FUNCTION[,FUNCTION]+
- A comma separated list of function names where to inject faults into (including their call-tree)
--exclude-functions=FUNCTION[,FUNCTION]+
- A comma separated list of function names to skip for fault injection
An example usage, extracted from the tests/
directory looks like:
$ run-model.py -u FVP_MPS2_M3.yml -s --ignore-return-value --iris-port 7354 \
-t verifyPIN-O2.elf.trace verifyPIN-O2.elf 1244
$ paf-faulter --instructionskip \
--oracle='@(fault_occurred){success};@(crash_detected){crash};return(main){noeffect}' \
--output=verifyPIN-O2.is.yml \
--image=verifyPIN-O2.elf --functions=verifyPIN@0 verifyPIN-O2.elf.trace
index file verifyPIN-O2.elf.trace.index is older than trace file verifyPIN-O2.elf.trace; rebuilding it
Inject faults into (1) functions: verifyPIN@0
Excluded functions (0): -
Will inject faults on 'verifyPIN@0' : t:2944 l:7112 pc=0x8249 - t:2984 l:7214 pc=0x827b
Injecting faults on range t:2944 l:7112 pc=0x8249 - t:2984 l:7214 pc=0x827b
$ cat verifyPIN-O2.is.yml
Image: "verifyPIN-O2.elf"
ReferenceTrace: "verifyPIN-O2.elf.trace"
MaxTraceTime: 4235
ProgramEntryAddress: 0x815c
ProgramEndAddress: 0x10aca
FaultModel: "InstructionSkip"
FunctionInfo:
- { Name: "verifyPIN@0", StartTime: 2944, EndTime: 2984, StartAddress: 0x8248, ...
Oracle:
- { Pc: 0x8010, Classification: [["success",[]]]}
- { Pc: 0x8280, Classification: [["crash",[]]]}
- { Pc: 0x80de, Classification: [["noeffect",[]]]}
Campaign:
- { Id: 0, Time: 2944, Address: 0x8248, Instruction: 0xb530, Width: 16, ...
- { Id: 1, Time: 2945, Address: 0x824a, Instruction: 0x6815, Width: 16, ...
...
A reference trace for program verifyPIN-O2.elf
invoked with user pin
argument 1244
is first recorded. The paf-faulter
is invoked, with the
instruction skip fault model and will analyze the trace and produce a fault
campaign for the very first execution of function verifyPIN
.
paf-calibration
is a small utility to test if the ADC used for acquiring
the power consumption of a device has correct settings (gain, ...).
- The command line syntax looks like:
paf-calibration
file.npy [ file.npy ]*
paf-calibration
will accumulate statistics over the NPY files provided on
the command line and then report them. It will report if some calibration is
required. At the time of writing, this is hard wired for captures done on a
chipwhisperer board but can easily be improved to support other ADCs..
Example usage:
$ paf-calibration traces.npy
Overall min sample value: -0.255859 (3)
Overall max sample value: 0.220703 (2)
As the expected range of values should be in [-0.5 .. 0.5(, the ADC settings could benefit from a bit of gain to use the full available range.
paf-constanttime
is a utility that compare parts of traces, typically
functions, and look for divergences, in control-flow, in execution or in memory
accesses. In some way, this is a diff
tool, but it takes into account the
Tarmac trace format and the structure of the executed code.
- The command line syntax looks like:
paf-constanttime
[ options ] FUNCTION TRACEFILE...
The following options are recognized:
--ignore-conditional-execution-differences
- Ignore differences in conditional execution
--ignore-memory-access-differences
- Ignore differences in memory accesses
--image=IMAGEFILE
- Image file name
--only-index
- Generate index and do nothing else
--force-index
- Regenerate index unconditionally
--no-index
- Do not regenerate index
--li
- Assume trace is from a little-endian platform
--bi
- Assume trace is from a big-endian platform
-v
or--verbose
- Make tool more verbose
-q
or--quiet
- Make tool quiet
--show-progress-meter
- Force display of the progress meter
As an example usage, if we get back to our walk-thru on timing side channels (see Timing):
$ paf-constanttime --image=check.elf main check1.trace check2.trace
index file check1.trace.index is older than trace file check1.trace; rebuilding it
index file check2.trace.index is older than trace file check2.trace; rebuilding it
Running analysis on trace 'check1.trace'
- Building reference trace from main instance at time : 698 to 715
698 X CMP r0,#1
699 - BLE {pc}+0x1a
700 X LDR r1,[r1,#4] R4(0x1a066)@0x106ffff8
701 X LDR r2,{pc}+0x1e R4(0x1a164)@0x8050
702 X SUBS r3,r1,#1
703 X ADDS r1,#3
704 X LDRB r12,[r3,#1]! R1(0x31)@0x1a066
705 X LDRB r0,[r2],#1 R1(0x31)@0x1a164
706 X CMP r12,r0
707 - BNE {pc}+0xa
708 X CMP r3,r1
709 X BNE {pc}-0xe
710 X LDRB r12,[r3,#1]! R1(0x33)@0x1a067
711 X LDRB r0,[r2],#1 R1(0x32)@0x1a165
712 X CMP r12,r0
713 X BNE {pc}+0xa
714 X MOVS r0,#0
715 X BX lr
Running analysis on trace 'check2.trace'
- Comparing reference to instance at time : 698 to 721
o Time:713 Executed:1 PC:0x8042 ISet:1 Width:16 Instruction:0xd103 BNE {pc}+0xa (reference)
Time:713 Executed:0 PC:0x8042 ISet:1 Width:16 Instruction:0xd103 BNE {pc}+0xa
o Time:714 Executed:1 PC:0x804c ISet:1 Width:16 Instruction:0x2000 MOVS r0,#0 (reference)
Time:714 Executed:1 PC:0x8044 ISet:1 Width:16 Instruction:0x428b CMP r3,r1
the analysis of divergences can omit differences in conditional instruction execution:
$ paf-constanttime --image=check.elf \
--ignore-conditional-execution-differences main check1.trace check2.trace
index file check1.trace.index looks ok; not rebuilding it
index file check2.trace.index looks ok; not rebuilding it
Running analysis on trace 'check1.trace'
- Building reference trace from main instance at time : 698 to 715
698 X CMP r0,#1
699 - BLE {pc}+0x1a
700 X LDR r1,[r1,#4] R4(0x1a066)@0x106ffff8
701 X LDR r2,{pc}+0x1e R4(0x1a164)@0x8050
702 X SUBS r3,r1,#1
703 X ADDS r1,#3
704 X LDRB r12,[r3,#1]! R1(0x31)@0x1a066
705 X LDRB r0,[r2],#1 R1(0x31)@0x1a164
706 X CMP r12,r0
707 - BNE {pc}+0xa
708 X CMP r3,r1
709 X BNE {pc}-0xe
710 X LDRB r12,[r3,#1]! R1(0x33)@0x1a067
711 X LDRB r0,[r2],#1 R1(0x32)@0x1a165
712 X CMP r12,r0
713 X BNE {pc}+0xa
714 X MOVS r0,#0
715 X BX lr
Running analysis on trace 'check2.trace'
- Comparing reference to instance at time : 698 to 721
o Time:714 Executed:1 PC:0x804c ISet:1 Width:16 Instruction:0x2000 MOVS r0,#0 (reference)
Time:714 Executed:1 PC:0x8044 ISet:1 Width:16 Instruction:0x428b CMP r3,r1
paf-power
is a tool create a synthetic power trace for a function from a
set of tarmac traces. It's worth mentioning here that by nature synthetic
traces have no noise, which can confuse the tools to analyze them, so
paf-power
adds a small amount of noise by default (this can optionally be
turned off). paf-power
will record one power trace per function execution
it found in the Tarmac traces.
- The command line syntax looks like:
paf-power
[ options ] TRACEFILE...
The following options are recognized:
--no-noise
- Do not add noise to the power trace
--noise-level=VALUE
- Level of noise to add (default: 1.0)
--uniform-noise
- Use a uniform distribution noise sourceforge
--hamming-weight=FILENAME
- Use the hamming weight power model
--hamming-distance=FILENAME
- Use the hamming distance power model
--timing=TimingFilename
- Emit timing information to TimingFilename
--regbank-trace=FILENAME
- Dump a trace of the register bank content in numpy format to FILENAME
--memory-accesses-trace=FILENAME
- Dump a trace of memory accesses in yaml format to FILENAME
--instruction-trace=FILENAME
- Dump an instruction trace in yaml format to FILENAME
--detailed-output
- Emit more detailed information in the CSV file
--with-pc
- Include the program counter contribution to the power (HW, HD)
--with-opcode (HW, HD)
- Include the instruction encoding contribution to the power
--with-mem-address
- Include the memory accesses address contribution to the power (HW, HD)
--with-mem-data
- Include the memory accesses data contribution to the power (HW, HD)
--with-instruction-inputs
- Include the instructions input operands contribution to the power (HW only)
--with-instruction-outputs
- Include the instructions output operands contribution to the power (HW, HD)
--with-load-to-load-transitions
- Include load to load accesses contribution to the power (HD)
--with-store-to-store-transitions
- Include store to store accesses contribution to the power (HD)
--with-all-memory-accesses-transitions
- Include all consecutive memory accesses contribution to the power (HD)
--with-memory-update-transitions
- Include memory update contribution to the power (HD)
--function=FUNCTION
- Analyze code running within FUNCTION
--via-file=FILE
- Read command line arguments from FILE
--between-functions=FUNCTION_START,FUNCTION_END
- Analyze code between FUNCTION_START return and FUNCTION_END call
--image=IMAGEFILE
- Image file name
--only-index
- Generate index and do nothing else
--force-index
- Regenerate index unconditionally
--no-index
- Do not regenerate index
--li
- Assume trace is from a little-endian platform
--bi
- Assume trace is from a big-endian platform
-v
or--verbose
- Make tool more verbose
-q
or--quiet
- Make tool quiet
--show-progress-meter
- force Display of the progress meter
For example, assume that you want to get a synthetic power trace, using the
Hamming weight model, of the execution of function gadget
in
program.elf
. You would first need to record a number of Tarmac traces using
run-model.py (with varying inputs to program.elf
), and then paf-power
can build compute a synthetic power trace with:
$ paf-power --hamming-weight --image=program.elf --npy -o traces.npy gadget traces/*.trace
index file traces/program.0.trace.index looks ok; not rebuilding it
index file traces/program.1.trace.index looks ok; not rebuilding it
index file traces/program.2.trace.index looks ok; not rebuilding it
...
Running analysis on trace 'traces/program.0.trace'
- Building power trace from gadget instance at time : 594 to 606
Running analysis on trace 'traces/program.1.trace'
- Building power trace from gadget instance at time : 594 to 606
Running analysis on trace 'traces/program.2.trace'
- Building power trace from gadget instance at time : 594 to 606
...
paf-correl
will compute the Pearson correlation coefficient for a trace
file considering some intermediate values.
- The command line syntax looks like:
paf-correl
[ options ] INDEX...
The following options are recognized:
-v
or--verbose
- Increase verbosity level (can be specified multiple times)
-a
or--append
- Append to output_file (instead of overwriting)
-o FILE
or--output=FILE
- Write output to FILE (instead of stdout)
-p
or--python
- Emit results in a format suitable for importing in python
-g
or--gnuplot
- Emit results in gnuplot compatible format
--numpy
- Emit results in num^y format
-f S
or--from=S
- Start computation at sample S (default: 0)
-n N
or--numsamples=N
- Restrict computation to N samples
-d T
or--numtraces=T
- Only process the first T traces
-i INPUTSFILE
or--inputs=INPUTSFILE
- Use INPUTSFILE as input data, in npy format
-t TRACESFILE
or--traces=TRACESFILE
- Use TRACESFILE as traces, in npy format
For example, to compute the Pearson correlation coefficient for the combination
inputs[0] ^ inputs[1]
for a number of traces in file traces.npy
(with
50 samples per trace) that was generated assuming input values in file
inputs.npy
:
$ paf-correl -g -o data.gp -i inputs.npy -t traces.npy 0 1
$ cat data.gp
0 0.00300078
1 -0.00619174
2 0.0100264
...
12 0.00902233
13 -0.312871
14 -0.325867
15 -0.23732
...
46 0.0185808
47 0.00560168
48 0.0162943
49 0.0050634
# max = -0.325867 at index 14
In this case, the correlation peak is found at sample 14, with a value of -0.325867.
paf-ns-t-test
is a utility to compute the non-specific t-test, i.e. it
computes the t-test between 2 groups of traces, without making any hypothesis
on an intermediate value.
- The command line syntax looks like:
paf-ns-t-test
[ options ] TRACES...
The following options are recognized:
-v
or--verbose
- Increase verbosity level (can be specified multiple times)
-a
or--append
- Append to output_file (instead of overwriting)
-o FILE
or--output=FILE
- Write output to FILE (instead of stdout)
-p
or--python
- Emit results in a format suitable for importing in python
-g
or--gnuplot
- Emit results in gnuplot compatible format
--numpy
- Emit results in num^y format
--perfect
- assume perfect inputs (i.e. no noise).
--decimate=PERIOD%OFFSET
- decimate result (default: PERIOD=1, OFFSET=0)
-f S
or--from=S
- Start computation at sample S (default: 0)
-n N
or--numsamples=N
- Restrict computation to N samples
--interleaved
- Assume interleaved traces in a single NPY file
For example, let's assume that we have two groups of traces, recorded in two separate files. The non-specific t-test, starting from sample 80, can be computed with:
$ paf-ns-t-test -g -o data.gp -v -f 80 group0.npy group1.npy
Performing non-specific T-Test on traces : group0.npy group1.npy
Saving output to 'data.gp'
Read 25000 traces (100 samples) from 'group0.npy'
Read 25000 traces (100 samples) from 'group1.npy'
Will process 20 samples per traces, starting at sample 80
$ cat data.gp
0 3.62867
1 4.23146
2 3.96177
3 3.68285
4 3.23287
...
12 -8.14007
13 -622.498
14 -633.387
15 -613.356
16 -529.575
17 -558.535
18 -572.168
19 -560.1
# max = -633.387 at index 14
paf-t-test
is a utility to compute the specific t-test, that is a t-test
with an hypothesis on an intermediate value. The intermediate value is computed
from one (or more) expressions that is (are) provided on the command line.
- The command line syntax looks like:
paf-t-test
[ options ] EXPRESSION...
The following options are recognized:
-v
or--verbose
- Increase verbosity level (can be specified multiple times)
-a
or--append
- Append to output_file (instead of overwriting)
-o FILE
or--output=FILE
- Write output to FILE (instead of stdout)
-p
or--python
- Emit results in a format suitable for importing in python
-g
or--gnuplot
- Emit results in gnuplot compatible format
--numpy
- Emit results in num^y format
--perfect
- assume perfect inputs (i.e. no noise).
--decimate=PERIOD%OFFSET
- decimate result (default: PERIOD=1, OFFSET=0)
-f S
or--from=S
- Start computation at sample S (default: 0)
-n N
or--numsamples=N
- Restrict computation to N samples
-t TRACESFILE
or--traces=TRACESFILE
- Use TRACESFILE as traces, in npy format
-i INPUTSFILE
or--inputs=INPUTSFILE
- Use INPUTSFILE as input data, in npy format
-m MASKSFILE
or--masks=MASKSFILE
- Use MASKSFILE as mask data, in npy format
-k KEYSFILE
or--keys=KEYSFILE
- Use KEYSFILE as key data, in npy format
For example, to get the specific t-test for the intermediate 8-bit value inputs[0]
^ keys[0]
for traces in traces.npy
generated with data in
inputs.npy
and keys.npy
, for the 70 samples starting from sample 80:
$ paf-t-test -g -o data.gp -v -f 80 -n 70 -i inputs.npy -k keys.npy -t traces.npy 'trunc8(xor($in[0],$key[0])'
Reading traces from: 'traces.npy'
Reading inputs from: 'inputs.npy'
hw_max=32
Input classification: HAMMING_WEIGHT
Index: 0 1
Saving output to 'data.gp'
Read 20000 traces (150 samples per trace)
Read 20000 inputs (8 data per trace)
Will process 70 samples per traces, starting at sample 80
$ cat data.gp
0 -1.34559
1 0.534966
2 -0.694472
3 -0.210325
...
30 26.6723
31 26.548
32 24.1231
33 63.1241
34 60.8476
35 57.8299
36 47.5652
37 34.4497
38 30.407
39 28.7012
...
67 -14.8748
68 -13.4678
69 -11.1817
# max = 63.1241 at index 33
EXPRESSIONs are a strongly- and explicitely-typed mini-language supporting:
- Literals, that are expressed in decimal form and postfixed with
_u8
,_u16
,_u32
or_u64
to express a literal value with respectively 8-, 16-, 32- or 64-bit. - Inputs,
$in[idx]
,$key[idx]
and$masks[idx]
wich correspond to theidx
element of a row read respectively fromINPUTSFILE
,KEYSFILE
andMASKSFILE
. - Unary operators:
NOT(...)
(bitwise not),TRUNC8(...)
(truncation to 8-bit),TRUNC16(...)
(truncation to 16-bit),TRUNC32(...)
(truncation to 32-bit),AES_SBOX(...)
(look-up value from the AES SBOX) andAES_ISBOX(...)
(reverse look-up from the AES SBOX). TheTRUNC*
operators effectively convert the type of their inputs to 8-, 16-, 32-bit values. TheAES_*
operators expect and return 8-bit values.NOT
will return a value of the same type as its input. - Binary operators:
AND(..., ...)
(bitwise and),OR(..., ...)
(bitwise or),XOR(..., ...)
(bitwise xor),LSL(..., ...)
(logical shift left),LSR(..., ...)
(logical shift right) andASR(..., ...)
(arithmetic shift right). Both operands of a binary operator must have the same type, and the operator result will have the same type as its inputs.
paf-np-cat
is a utility to concatenate simple 1D or 2D numpy arrays.
- The command line syntax looks like:
paf-np-cat
[ options ] INPUT_NPY_FILES...
where INPUT_NPY_FILES
are all the numpy files to concatenate in the specified order.
The following options are recognized:
-v
or--verbose
- Increase verbosity level (can be specified multiple times)
-r
or--rows
- concatenate INPUT_NPY_FILES along the rows axis
-o FILENAME
or--output=FILENAME
- concatenate
INPUT_NPY_FILES
intoFILENAME
Example usage, to concatenate job0.npy
and job1.npy
into
of result.npy
:
$ paf-np-cat -o result.npy job0.npy job1.npy
paf-np-create
is a utility to create simple 1D or 2D numpy arrays. It's
used mostly for testing, but can be handy at times.
- The command line syntax looks like:
paf-np-create
[ options ] VALUE...
where VALUE
is the values to use when filling the matrix.
The following options are recognized:
-v
or--verbose
- Increase verbosity level (can be specified multiple times)
-r ROWS
or--rows=ROWS
- Number of rows in the matrix
-c COLUMNS
or--columns=COLUMNS
- Number of columns in the matrix
-t ELT_TYPE
or--element-type=ELT_TYPE
- Select matrix element typei, where
ELT_TYPE
is one of numpy element types (e.g.u8
,i16
,f32
, ...) -o FILE
or--output=FILE
- Specify output file name
Example usage, to create a numpy file example.npy
containing a 2 x 4 matrix
of double
elements initialized with: 0.0 .. 7.0:
$ paf-np-create -t f8 -r 2 -c 4 -o example.npy 0.0 1.0 2.0 3.0 4.0 5.0 6.0 7.0
paf-np-utils
is a query utility to display information about a numpy file,
like number of rows or columns, ...
- The command line syntax looks like:
paf-np-utils
[ options ] NPY
The following options are recognized:
-v
or--verbose
- Increase verbosity level (can be specified multiple times)
-r
or--rows
- Print number of rows
-c
or--columns
- Print number of columns (this is the default action)
-t
or--elttype
- Print element type
-p
or--python-content
- Print array content as a python array
-f
or--c-content
- Print array content as a C/C++ array
-i
or--info
- Print NPY file information
-m
or--revision
- Print NPY revision
Example usage, querying the element type in file example.npy
, as created in
the example for paf-np-create
:
$ paf-np-utils -t example.npy
<f8
paf-np-expand
is a utility to expand or trunc a matrix, on the x and/or y axis with modulo.
It can optionnaly add noise to the samples in the matrix.
- The command line syntax looks like:
paf-np-expand
[ options ] NPY
The following options are recognized:
-v
or--verbose
- increase verbosity level (can be specified multiple times)
-o
or--output=FILENAME
- NPY output file name (if not specified, input file will be overwritten)
-c
or--columns=NUM_COLS
- Number of column to expand to. If not set, use all columns from the source NPY.
-r
or--rows=NUM_ROWS
- Number of rows to expand to. If not set, use all rows from the source NPY.
--noise=NOISE_LEVEL
- Add noise to all samples (default: 0.0, i.e. no noise)
--uniform-noise
- Use a uniform distribution noise sourceforge
--normal-noise
- Use a normal distribution noise source
$ paf-np-create -o source.npy -t f8 -r 2 -c 3 1.0 2.0 3.0 4.0 5.0 6.0
$ paf-np-utils -p source.npy
[
[ 1, 2, 3 ],
[ 4, 5, 6 ],
]
$ paf-np-expand -o dest.npy -r 4 -c 2 --noise 0.5 source.npy
$ paf-np-utils -p dest.npy
[
[ 1.42664, 2.21827 ],
[ 4.08553, 5.38625 ],
[ 1.09103, 2.39464 ],
[ 4.29554, 5.0181 ],
]
wan-zap-header
is a very simple utility that will clear the version
and timestamp
fields in fst
files given as argument. This makes it
easier to compare waveforms recorded at different times for example or to
simply compare waveform files hashes.
- The command line syntax looks like:
wan-zap-header
FST [FST]*
wan-zap-header
is used internally by PAF for the sample fst
files
used for unit testing.
Example usage:
$ wan-zap-header Counters.fst
Zapping SimVersion='Icarus Verilog' in 'Counters.fst'
Zapping TimeStamp='Wed Apr 17 17:15:19 2024' in 'Counters.fst'
wan-info
is a utility to display some basic information about a waveform file.
- The command line syntax looks like:
wan-info
[options] FILES+
The following options are recognized:
--hier
- dump hierarchy
For example, to display simulation information found in Counters.vcd (from PAF's unit tests samples):
$ wan-info Counters.vcd
Input file: Counters.vcd
Start time: 0
End time: 110000
Timezero: 0
Timescale: 1 ps
Content:
- 2 modules
- 0 tasks
- 0 functions
- 0 blocks
- 2 alias
- 5 wires
- 3 registers
- 1 ints
And to display information about the module and signal hierarchy in Counters.fst:
$ wan-info --hier Counters.fst
File Counters.fst:
o tbench
- cnt2 [31:0] (wire)
- cnt1 [7:0] (wire)
- clk (register)
- reset (register)
o DUT
- clk (wire)
- reset (wire)
- cnt1 [7:0] (wire)
- cnt [8:0] (register)
- cnt2 [31:0] (integer)
wan-diff
is a utility to report the differences between 2 wavefiles. It
often occurs that 2 simulations diverge at some point, and wan-diff
can
be used to pin-point to the differences, in time (when it first started to
differ) and in name / value (the signals and how they differ). wan-diff
reports the difference is user selectable ways:
- by signal (default)
- by time
wan-diff
also accepts a number of filter options:
- by type to only consider register or wires for example,
- by scope to only consider signals in a specific hierarchical part of the system
wan-diff
can also emit the difference to a waveform file, where
for each signal with a difference, the signal will be shown as-is from
the 2 files with a third synthetic signal, showing the difference
with a xor of the 2 signals.
- The command line syntax looks like:
wan-diff
[options] FILE1 FILE2
The following options are recognized:
--verbose
- verbose output
--output=FILE
- Save diff to FILE, in vcd or fst format according to the file extension used.
--regs
- Diff registers only
--wires
- Diff wires only
--time-view
- Display difference by time, rather than by signal
--signal-summary
- Report a summary list of differing signals
--module-summary
- Report a summary list of modules with differing signals
--scope-filter=FILTER
- Filter scopes matching FILTER
For example, suppose that we have have 2 (differing) simulations of our Counters
:
$ wan-diff Counters.vcd Counters-1.vcd --verbose
Simulation duration: 110000
7 signals to analyze.
tbench.DUT/cnt [8:0] Kind::REGISTER difference
- 30000 000000011 <> 000000001
- 40000 000000100 <> 000000101
$ wan-diff Counters.vcd Counters-1.vcd --time-view --verbose
Simulation duration: 110000
7 signals to analyze.
30000
- 000000011 <> 000000001 Kind::REGISTER tbench.DUT/cnt [8:0]
40000
- 000000100 <> 000000101 Kind::REGISTER tbench.DUT/cnt [8:0]
wan-merge
is a utility to to merge waveform files into a single file,
provided there is no conflict in the module hierarchy and signal names.
This can be useful in cases where the simulator emit files per hierarchy
level in the design.
- The command line syntax looks like:
wan-merge
[options] FILES+
The following options are recognized:
--verbose
- verbose output
--output=OUTPUT_FILE
- Save merged traces in OUTPUT_FILE
wan-power
is a utility to ...
- The command line syntax looks like:
wan-power
[options] F*[,*F][%*CYCLE_INFO*]...
where F is one or more input files in fst or vcd format. It can optionnaly be completed with a CYCLE_INFO file. The CYCLE_INFO file is used to extract specific sections of the waveform, which happens when a simulation repeatedly ran some experiment and one wants to analyze only those experiments and produce one power trace per experiment. This is a text file, with one comma separated couple per line, defining the begin and end times of each experiment.
The following options are recognized:
--verbose
- verbose output
--no-noise
- Don't add noise to the power trace
--regs
- Trace registers only
--wires
- Trace wires only
--hamming-weight=FILENAME
- Use hamming weight model and save result to FILENAME. Depending on the FILENAME's extension, it will be saved in numpy format (.npy) or CSV (.csv). Use '-' to output the CSV file to stdout.
--hamming-distance=FILENAME
- Use hamming distance model and save result to FILENAME. Depending on the FILENAME's extension, it will be saved in numpy format (.npy) or CSV (.csv). Use '-' to output the CSV file to stdout.
--decimate=PERIOD%OFFSET
- decimate output (default: PERIOD=1, OFFSET=0)
--scope-filter=FILTER
- Filter scopes matching FILTER
Code contributions, in the form of comments, bug reports or patches, are most welcomed !
Please use the GitHub issue tracker associated with this repository for feedback.
No coding style is perfect to everyone, and the code style used by PAF
does not claim to be perfect, we just aim to have it consistent, as it helps
working with the code base: developers' eyes are agile enough to quickly adapt
provided the formatting is consistent. But formatting is boring, no developper
should have to worry about it in the 21st century ! We have thus provided a
.clang-format
, which allows to automate the formating consistantly in most
develoment environments. A .clang-tidy
file is also provided in order to
use consistant naming. Please use them ! VSCode provide excellent support
for clang-format
and clang-tidy
, and all the major editors have
now the language-server feature to interact with clangd
for example
to provide code completion and much more (like code formating and naming
convention checks).
PAF's general philosophy is to implement as much as possible in libraries, with the application just being a specific glueing of the components in the libraries. The bulk of PAF is C++ code, but a few parts, most notably run-model.py are written in Python.
The code base organization reflects different domains tackled by PAF:
- fault injection related libraries (in
include/PAF/FI
andlib/FI
) - side channel related libraries (in
include/PAF/SCA
andlib/SCA
) - waveforam analysis related libraries (in
include/PAF/WAN
andlib/WAN
) - common libraries (in
include/PAF
andlib/PAF
)
and it also has mundane parts like :
- unit tests (in
unit-test/
) - end to end tests (in
tests/
) - continuous integration testing (in
.github/workflows/
) - documentation (in
doc/
) - build configuration (in
cmake/
)
The configuration and build system used for PAF is CMake.
Most unit tests are using the GoogleTest framework, but a few parts like those written in Python have their dedicated unit tests. All unit tests have been grouped together, using CMake's CTest.
Unit tests can be run with the check
target. For example, if PAF's codebase
has been configured by cmake
to use the ninja
tool :
$ ninja -C build/ check
ninja: Entering directory `build/'
[==========] Running 253 tests from 60 test suites.
[----------] Global test environment set-up.
[----------] 1 test from AddressingMode
[ RUN ] AddressingMode.base
[ OK ] AddressingMode.base (0 ms)
[----------] 1 test from AddressingMode (0 ms total)
[----------] 1 test from InstrInfo
[ RUN ] InstrInfo.inputRegisters
[ OK ] InstrInfo.inputRegisters (0 ms)
[----------] 1 test from InstrInfo (0 ms total)
[----------] 1 test from ValueType
[ RUN ] ValueType.Base
[ OK ] ValueType.Base (0 ms)
[----------] 1 test from ValueType (0 ms total)
[----------] 1 test from Value
[ RUN ] Value.Base
[ OK ] Value.Base (0 ms)
[----------] 1 test from Value (0 ms total)
[----------] 10 tests from Expr
[ RUN ] Expr.Constants
[ OK ] Expr.Constants (0 ms)
[ RUN ] Expr.UnaryOps
[ OK ] Expr.UnaryOps (0 ms)
[ RUN ] Expr.Truncate
[ OK ] Expr.Truncate (0 ms)
[ RUN ] Expr.BinaryOps
[ OK ] Expr.BinaryOps (0 ms)
[ RUN ] Expr.Inputs
[ OK ] Expr.Inputs (0 ms)
[ RUN ] Expr.NPInputs
[ OK ] Expr.NPInputs (0 ms)
[ RUN ] Expr.parse_empty
[ OK ] Expr.parse_empty (0 ms)
[ RUN ] Expr.parse_literals
[ OK ] Expr.parse_literals (0 ms)
[ RUN ] Expr.parse_operator
[ OK ] Expr.parse_operator (0 ms)
[ RUN ] Expr.parse_variable32
[ OK ] Expr.parse_variable32 (0 ms)
[----------] 10 tests from Expr (0 ms total)
[----------] 2 tests from AES
[ RUN ] AES.SBox
[ OK ] AES.SBox (0 ms)
[ RUN ] AES.ISBox
[ OK ] AES.ISBox (0 ms)
[----------] 2 tests from AES (0 ms total)
[----------] 6 tests from Fault
[ RUN ] Fault.BreakPoint
[ OK ] Fault.BreakPoint (0 ms)
[ RUN ] Fault.FaultModelBase
[ OK ] Fault.FaultModelBase (0 ms)
[ RUN ] Fault.InstructionSkip
[ OK ] Fault.InstructionSkip (0 ms)
[ RUN ] Fault.CorruptRegDef
[ OK ] Fault.CorruptRegDef (0 ms)
[ RUN ] Fault.FunctionInfo
[ OK ] Fault.FunctionInfo (0 ms)
[ RUN ] Fault.Campaign
[ OK ] Fault.Campaign (0 ms)
[----------] 6 tests from Fault (0 ms total)
[----------] 1 test from Interval
[ RUN ] Interval.basic
[ OK ] Interval.basic (0 ms)
[----------] 1 test from Interval (0 ms total)
[----------] 1 test from Intervals
[ RUN ] Intervals.basic
[ OK ] Intervals.basic (0 ms)
[----------] 1 test from Intervals (0 ms total)
[----------] 23 tests from LWParser
[ RUN ] LWParser.construct_default_position
[ OK ] LWParser.construct_default_position (0 ms)
...
[ RUN ] Waveform.visitWiresInSpecificScope
[ OK ] Waveform.visitWiresInSpecificScope (1 ms)
[----------] 19 tests from Waveform (25 ms total)
[----------] 1 test from WaveformF
[ RUN ] WaveformF.toFile
[ OK ] WaveformF.toFile (25 ms)
[----------] 1 test from WaveformF (25 ms total)
[----------] 2 tests from FSTWaveFile
[ RUN ] FSTWaveFile.Read
[ OK ] FSTWaveFile.Read (0 ms)
[ RUN ] FSTWaveFile.getAllChangesTimes
[ OK ] FSTWaveFile.getAllChangesTimes (0 ms)
[----------] 2 tests from FSTWaveFile (1 ms total)
[----------] Global test environment tear-down
[==========] 253 tests from 60 test suites ran. (1809 ms total)
[ PASSED ] 253 tests.
The end-to-end testing tests together ruun-model.py
and paf-faulter
; it
thus requires access to a FastModel. It is also intended for the time being to
be run manually, as the results depend on the cross-compiler used.
PAF's continuous integration relies on GitHub's Actions and workflows to build and run unit testing on a number of platforms.
The documentation is written in the reStructuredText format. It allows easy written, and can be transformated automatically to html and pdf, and is rendered directly by GitHub.
When modifying the documentation, please check that it's still parsed
correctly, by using rst2html5.py
for example:
$ rst2html5.py doc/index.rst build/doc/index.html