Voltage glitch DFA on hardware AES - anyone gotten a clean round-9 fault and recovered the key?

I am working on a DFA attack against a hardware AES peripheral (not the software AES that ships in the CW notebooks). I am using a ChipWhisperer Husky to do voltage crowbar glitching (VCC dip via scope.glitch). The goal is a clean round-9-input fault so I can run Piret/Quisquater style DFA equations and recover the key.

After a lot of hardware time I still have zero clean round-9-input hits. I want to ask the community two things:

  1. Has anyone here actually pulled a full AES key from a hardware AES accelerator (not software AES) using voltage fault injection with ChipWhisperer? If yes, is the setup or writeup shareable? I looked around the forum and found the enable_only crowbar tip (thank you to whoever posted that, it helped), but I have not found an end-to-end hardware-AES DFA success story with Husky.
  2. Is there a real reason enable_only mode would just never land inside a single round window on a hardware accelerator, or does that point at something wrong with my arming/trigger setup?

What I am attacking

A Custom board, hardware AES-128 engine (not bit-banged software AES). I could recover aes key with power sca from the board. Trigger is on the accelerator start. I decrypt every returned (possibly faulty) ciphertext round by round with the known lab key to figure out which round the fault actually landed in (EARLIER than round 9, exactly R9_INPUT, or LATE i.e. round 9 already done). This is independent of the ciphertext-diff count, so I am not just guessing from “4 bytes changed.”

What I have tried

Two glitch modes:

  • scope.glitch.output = "enable_only" - full VCC dip for one or more target clock cycles. Only ext_offset (when the dip starts, in target clock cycles from trigger) and repeat (how many cycles the dip lasts) matter here. width/offset do nothing in this mode, per the forum tip I found.
  • scope.glitch.output = "glitch_only" - the normal sub-cycle pulse, controlled by width/offset as well as ext_offset.

Basic per-trial setup looks like this:

scope.glitch.enabled = True
scope.glitch.clk_src = "pll"
scope.glitch.output = output_mode          # "enable_only" or "glitch_only"
scope.glitch.trigger_src = "ext_single"    # one glitch event per scope.arm()

scope.glitch.width = width                 # ignored when output == "enable_only"
scope.glitch.offset = offset               # ignored when output == "enable_only"
scope.glitch.ext_offset = ext_offset       # cycles from trigger to glitch start
scope.glitch.repeat = repeat               # cycles the dip/pulse lasts

Then I fire a trial, read back the ciphertext, and classify it:

def localize(faulty_ct, plaintext, key):
    # decrypt round by round with the known key, compare against
    # the real AES state at each round boundary, and report where
    # the fault first appears: EARLIER, R9_INPUT, or LATE
    ...

def classify_diagonal(ref_ct, faulty_ct):
    # count differing bytes; 4 bytes changed in one AES diagonal
    # pattern is the classic DFA-usable signature
    ...

Results so far (real hardware, not simulated)

I swept ext_offset and repeat in stages, about 35,000 trials total across roughly 5 hours of hardware time over two sessions:

Mode ext_offset repeat width/offset trials Result
enable_only 150-172 8-18 - 10,000 15 hits, all LATE
enable_only 155-172 2-6 - 4,000 0 faults, too weak
enable_only 155-172 20-30 - 4,000 14 hits, all LATE
enable_only 130-150 4-10 - 4,000 12 hits, all LATE
enable_only 170-210 1-7 - 4,000 1 hit, LATE, too weak
enable_only 170-210 8-18 - 4,437 215 hits: 51 EARLIER, 164 LATE
glitch_only 155-172 8-18 w=50, off=0 2,500 zero effect
glitch_only 155-172 8-18 w=200, off=0 2,500 zero effect
glitch_only 155-172 8-18 w=200, off=500 2,000 zero effect

Zero R9_INPUT hits anywhere. Zero clean 4-byte diagonal faults.

What stands out to me:

  • enable_only never once landed EARLIER-only or R9_INPUT across every repeat value from 2 to 30 and every ext_offset from 130 to 210. Weak pulses (repeat under 8) just do nothing. Strong pulses (repeat 20-30) disturb the chip more, but land in the exact same LATE window as the weaker-but-still-working pulses. The one window that gave EARLIER hits (170-210, repeat 8-18) also gave LATE hits in the same sweep, so it is not a clean boundary, it is a mix.
  • glitch_only gave literally zero effect at every width/offset pair I tried, up to the tool’s own max width. That is the part that worries me most, because zero effect could mean “this mode cannot disturb this target” or it could mean “my arming sequence for glitch_only is wrong.” I have not ruled out the second one yet.

My current theory

A full-cycle crowbar dip (enable_only) probably does not inject a single clean fault at a precise instruction boundary. It more likely knocks the whole pipeline off track badly enough that by the time the chip is back to normal, execution is already past round 9, no matter when the dip started. If that is right, then no amount of further ext_offset/repeat sweeping in this mode will ever land on round 9 input specifically, it is a “wrong tool for this fault” problem rather than a search problem.

My actual questions for the forum

  1. Has anyone gotten enable_only (or glitch_only) voltage glitching with Husky to land a clean single-round fault against a hardware AES accelerator, and recovered a key from it? Rough parameters or a link to a writeup would help a lot.
  2. Given “0 effect at every width/offset in glitch_only,” is that a known symptom of a bad arming/trigger sequence, or is it plausible that a hardware AES engine (as opposed to software AES) is just not disturbed by a sub-cycle pulse at all and needs the full enable_only dip to do anything?
  3. If enable_only really is too coarse for round-9-input precision on a hardware accelerator, is EM fault injection or clock glitching the usual next step people reach for, or is there another crowbar trick I am missing before giving up on voltage glitching entirely?

Happy to share my full sweep script and CSV logs if that helps someone reproduce or spot a bug in my setup. Thanks in advance.