At this point, some of the keys are partially locked out
which makes reacting to an attack slightly less effective
(keys are now locked into a particular process space configuration
which is not available in the loader's default config).
The reaction at this point sums up to seeing if the image is
a developer image, and if so, ensuring that the developer mode
bit is set (which would indicate that the keys have been
erased). I suppose an attacker could get around this by glitching
to some code that sets developer mode without also erasing
the keys. This weakness needs to be explored and hardened against.
this patch uses the previously modified APIs and implements
a multi-pass check on signatures in the case that paranoid
mode is turned on. This may effectively double the boot time but
it also reduces the chance of a successful glitch attack
on signature validation by a multiple of the glitch
success probability.
this also updates other API calls and implements the
XOR-on-target-address code.
Signature checking parameters are now consolidated into
a single "SecurityConfiguration" record. This allows the
sum total of security state transitions to be summarized in
the bao1x-api/src/pubkeys/mod.rs file, easing maintenance
and audit.
Reaction to the signature check is split into a `hardened_erase_policy`
API call which can take the results of the signature check and
react to it if necessary.
This allows the signature check routine to be used for both
audit (where reaction is not necessary) and for live testing
of the code (where reaction is required). It also simplifies
the implementation of double-checking signatures in the case
of paranoid mode.
Another important improvement is that the signature checking
API outputs the target address for the jump, but masked with
an XOR of the function code of the signed block. The purpose
of the XOR mask isn't to obfuscate the value; rather, it's
to prevent there being a single, simple, fixed-constant
jump instruction laying around that a glitcher can use as
a target: instead, a pair of registers must be loaded
with the target and its mask. This is an example of what
the final jump looks like:
```
60003a8e: ffffffff .word 0xffffffff
60003a92: 0440006f j 60003ad6 <bao1x_hal::hardening::die>
60003a96: ffffffff .word 0xffffffff
60003a9a: 167d addi a2,a2,-1
60003a9c: fe71 bnez a2,60003a78 <rust_entry+0x2f74>
60003a9e: a00d j 60003ac0 <rust_entry+0x2fbc>
60003aa0: 0360006f j 60003ad6 <bao1x_hal::hardening::die>
60003aa4: ffffffff .word 0xffffffff
60003aa8: 02e0006f j 60003ad6 <bao1x_hal::hardening::die>
60003aac: ffffffff .word 0xffffffff
60003ab0: 0260006f j 60003ad6 <bao1x_hal::hardening::die>
60003ab4: ffffffff .word 0xffffffff
60003ab8: 01e0006f j 60003ad6 <bao1x_hal::hardening::die>
60003abc: ffffffff .word 0xffffffff
60003ac0: 0149c533 xor a0,s3,s4
60003ac4: 600805b7 lui a1,0x60080
60003ac8: d0058593 addi a1,a1,-768 # 6007fd00 <_etext+0x6e9dc>
60003acc: 82aa mv t0,a0
60003ace: 852e mv a0,a1
60003ad0: 4581 li a1,0
60003ad2: 8282 jr t0
```
So here you can see the alternating bollards in the region that
will cause any glitches that don't land exactly right to
either halt the machine or cause a reaction.
Line 0x6003ac0 is where the xor happens to compute the target
address. Should a glitcher land on that address, `s3` and `s4`
will need to be loaded with the target address and a value when
XOR'd with it that yields a meaningful result. While there is
a non-zero chance that this can be arranged, it's definitely
not as easy as targeting a jump-to-fixed address exit sequence.
The management of different security configurations was getting
a bit out of hand, so all the security configurations used by
the system and their transitions are now consolidated into a single
file, This will make auditing and checking the transitions a lot
easier.
The loader is only lightly hardened at this point. Some minmal
random delay + bollards inserted. The signature check routine
inherits all the hardening of boot0 because it uses the same
code. PLL mode is checked at least once.
more loader hardening will happen in a future PR - at the
moment, we're focused on boot0/boot1 hardening.
boot1 hardening is similar to boot0 hardening - bollard & delay
insertion, checking that we're in PLL mode. Also the first phase
mesh results are checked and second phase is setup for the loader
to finally check.
clocks are still local to the routine - there's a to-do to consolidate
that routine into the API layer later on.
takes 8 samples and makes sure those values aren't repeated in the
next 8 samples.
has a ~1 in 500 million chance of giving a false positive. But
generally should catch: stuck-at fault, and also, a highly trained
oscillator fault.
- add `paranoid` command - when set, puts the chip into
paranoid mode. Not set by default because we need to check that
the security countermeasures are not too aggressive
- `lockdown` now revokes all developer keys in both main and
redundant array, and sets paranoid mode
- api fixes for sig validate
- audit now prints results of paranoid mode and attack count
- harden the check that the boot0 public keys match those in the
data array
- security mesh setup
- insertion of random delays & bollards
- upgrade API on signature verification to allow for random delays
during the signature check
- paranoid mode entry is implemented here. The main outcome of
paranoid mode is that when activated, the system will automatically
reset anytime a voltage glitch is detected.
- clean up PLL mode setting. For boot0, the mode is fixed & hardened
to 175MHz CPU operation, running off the PLL, under the theory that
running at this speed makes glitch attacks harder as the cycle time
is now 5.71ns, and simply disturbing the XTAL clock doesn't cause
the PLL to straight-up stop.
- some hardening to ensure that we are actually in PLL mode, and
that the switch to PLL mode wasn't glitched over
This allocates some additional one way counters dedicated
to hardening:
- Paranoid mode is duplicated in slots 65 & 67
- An attack counter is in slot 66
- Redundant revocation counters are seated in slot 68
The primary revocation counters are in slot 116. This means
that cutting out the revocation counters would probably require
at least two edits to the memory array - simply cutting the
word addressing line for those columns might be enough to
defeat a counter.
Paranoid mode duplicate is stored somewhat close to the
primary counter, which means it could be subject to a single
line cut to fully defeat the counter, but the space is getting
pretty crowded so I'll leave it like that; I really want to
leave the bottom 64 slots available for application use.
A "flag" key is allocated, plus a dummy flag is committed
to the repo. This is a value that, if retrieved, would
ostensibly bring the retriever glory and bounty, to the
extent that any glory or bounty is to be had (ps: I'm poor
and this project is obscure). The actual flag, of course,
is not in the repo...I hope!
The flag value is different from all other values in that
this is the only shared, common secret across all devices.
All other device secrets are generated on a per-device
basis from the TRNGs. So the flag needs some special treatment
such that it's not overwritten during device keying.
The flag exists because I'm hoping that anyone who manages
to extract it will also provide a helpful explanation of
the procedure to get it out, so that the system could be
patched (if the error is patchable) or rev'd in future
silicon. That being said, a flag retrieval is not necessarily
equal to a full break of security across all devices because
as mentioned above, most device secrets are on a per-device
basis. The flag represents then, the amount of effort necessary
to apply to every device to retrieve the secrets within.
clock setting calls, for legacy reasons, are duplicated in all the
stages of the bootloaders. This is a start toward moving these routines
into a common set of calls that improves maintainability.
These macros are added to the platform-agnostic API crate.
bollard is a macro that can respond to the following argument
formats:
bollard!(): inserts 4 illegal instructions and a jump over them
bollard!(#): inserts # of illegal instructions and a jump over them
bollard!(f, #): inserts # of an interleave of illegal instructions
and a jump to f
the last form is intended to "up the ante" by including a call
to a routine that can shut down the chip and/or wipe secrets
should the defender choose to do that. The other ones just
trampoline into an illegal instruction vector which is a fairly
weak defense because the CPU could once again get glitched out
of that.
HardenedBool is a boolean represented by a high-hamming distance
numeric value so that single-bit flips just lead to an invalid
bool, instead of a change in the meaning of the bool.
- key revocation check is fully duplicated and tested twice
- bollards & random delays added throughout the code. Random
delays require passing in a csprng object from the calling
environment.
- the primary go/no-go decision on whether a signature passes
or not is unfortunately not hardenable because the API is in
a third party crate. However, the regions around that call are
random-delayed and surrounded with bollards
- the state bits that determine whether the system is secure
and/or in developer mode are implemented using HardenedBool
- erase_secrets has some delays added to it as well as bollards.
However, it does remain a weakness that an attacker
could pull RRAM power during the erase cycle by just isolating
the RRAM power supply and putting a fast current limit on it,
thus preventing effective erasure by starving the circuit
of power.
Nothing we can really do about an active attack on the power
supply to prevent erasure, I think. It is unfortunately required
by design to have an isolated power supply for the RRAM to perform
elevated-voltage forming at CP time, and so, it should always
be possible to limit the current going into it thus preventing,
at least somewhat, any programming operations on the array.
One way counters are used ot record some security-critical properties,
such as key revocations and paranoid mode.
Two methods of retrieving the counters so that they are hardened
are provide here. Both of them rely on the fact that any value
other than 0 for the counter means it is set.
`hardened_get()` retrieves the counter value and sums it several
times. It does this *twice*, and returns *both* values as a tuple.
By returning both values as a tuple, it reduces the likelihood
that the compiler optimizes out the redundant read. The receiver
should implement serial checks that operate on the hardened_get()
result (i.e., test one value of the tuple, then test the other
value of the tuple redundantly, instead of OR'ing them together
into a single test)
`hardened_get2()` is for counters that are fully duplicated in
the array. For extremely critical counters, they are stored in
two physically separate elements. This is done to make bypass
of the counter harder than just FIB'ing out a single bit in
the memory array. In the case of `hardened_get2()`, a tuple
is returned, but instead of reading the same value twice, the
tuple corresponds to the summed value over each of the redundant
counters.
This library contains the following primitives:
check_pll() - checks that the PLL is the primary clock source.
This is important because allowing direct clocking of the system
from the external clock pin makes glitching much, much easier.
die() - a routine that implements a reaction to attacks. At the
moment, we just note attacks and then halt the machine. Eventually,
this could be upgraded to be more reactive, e.g. erasing secrets
or bricking the chip.
Csprng - this is a chacha8 cipher seeded with ring oscillator
TRNG data, plus whatever tiny bit of entropy we can scavenge
from uninitialized RAM (to make it so that it's not as trivial
as cutting the RO wire and tying it to 0 to bypass the library).
There's also a tiny health check on the RO before using it, again,
to make it harder than just cutting the RO wire. The sole purpose
of the Csprng is to generate random delays that frustrate glitching
attacks. The delay is currently tuned to a range of 0-255 iterations
of a loop.
paranoid_mode() - turns on fully automatic reactive sensor
countermeasures. Whenever a voltage glitch is detected, the system
will just reboot.
mesh countermeasures - these set up the secure mesh and the
reaction policy to tamper detection on the secure meshes
apply_attack_policy() - an API call that applies the currently
determined countermeasure in case that the detected attacks
counter has gone above a threshold. The reason we don't simply
react right away is that some countermeasures are fairly
sensitive, and *also*, very importantly, the chip *is* packaged
in a CSP and thus especially for dabao users you will expect
to see random trips of the attack sensors due to environmental
noise (photoflashes, nearby lightning strikes, etc.).
this is randomly genverated on boot and inserted into the
environment. it is unique per-PID.
This is useful for seeding psuedorandom number generators,
especially in environments where syscalls are not possible.