Wiki / Hardware

ChipWhisperer

An open-source capture and glitch platform for side-channel and fault-injection work, built around owning the target's clock so power traces line up sample for sample and a glitch can be aimed at a chosen clock cycle instead of a chosen microsecond.

Capture board to target, the four links that matter
Capture board Target HS2 (clock out) TIO4 (trigger in) MEASURE (SMA) GLITCH (SMA) nRST clock in trigger GPIO shunt in VCC/GND core VCC rail reset
The clock line is what makes the rest work: the capture board generates the target's clock and derives its own ADC sampling clock from the same source, so sample N lands at the same point of execution on every run. MEASURE reads the voltage across a shunt in the supply path; GLITCH is the output of an on-board crowbar transistor that shorts that same rail for a fraction of a cycle. Without the clock link you still get traces and glitches, just asynchronous ones, and you pay for it in alignment work.

The ChipWhisperer is NewAE's open-source platform for the two physical attacks that are governed by timing rather than by a software bug: power side-channel capture and fault injection. Hardware, firmware and the chipwhisperer Python package are all published, and the whole thing is driven from Python, usually out of a Jupyter notebook.

What separates it from a bench oscilloscope plus a pulse generator is that it owns the target's clock. The capture board generates the clock the target runs on and derives its own sampling clock from the same source, so a sample index means the same point of execution on every capture, and a glitch can be placed on a numbered clock cycle. That is a different unit of work from "trigger a scope and hope the jitter is small", and it is the reason a power analysis campaign that would need alignment gymnastics on a scope often needs none here.

Two boards, on purpose

The platform is deliberately split into a capture board and a target board.

The capture board holds the ADC front end, the clock generator, the glitch generator, the trigger inputs and the USB link to the host. It never runs the victim code. The target board is the victim: a small microcontroller you compile firmware for and deliberately attack.

  • The CW308 UFO board is a baseboard that takes swappable target modules, so the same capture board attacks an STM32, an XMEGA, a RISC-V part or an FPGA without rewiring.
  • The CW305 is an Artix-7 FPGA target board, used when the victim is a hardware crypto core rather than software.
  • On the one-part ChipWhisperer-Lite, the two halves are on a single PCB separated by a break-away line, so a beginner gets a known-good target for free.

The split is what makes the platform teachable. You develop and debug the attack against a target whose firmware you wrote and whose leakage you can verify, then move the same script to something you do not control. Skipping that step is the single most common reason a first campaign produces nothing and the operator cannot tell whether the fault is in the rig, the model, or the target.

The capture side

MEASURE is a single-ended ADC input that reads the voltage across a shunt resistor placed in the target's supply or ground path. The CW308 has that shunt built in; on your own board you have to add one, or fall back to an EM probe.

The settings that decide whether a capture is usable:

  • scope.clock.clkgen_freq sets the clock handed to the target, and scope.clock.adc_src = "clkgen_x4" locks the ADC to four samples per target clock cycle. Synchronous capture is the whole point; do not disable it because a higher raw sample rate sounds better.
  • scope.gain.db sets the analogue gain. Too low buries the signal in quantisation noise, too high clips it, and a clipped trace correlates against nothing.
  • scope.adc.samples and scope.adc.offset cut the window: how many samples to keep, and how many cycles after the trigger to start keeping them. The buffer is finite. You capture a window around one operation, not a whole boot.
  • scope.trigger.triggers = "tio4" with scope.adc.basic_mode = "rising_edge" is the default IO trigger: the target toggles a pin, the capture starts.

The Pro and Husky add a SAD trigger (sum of absolute differences), which arms on a stored reference snippet of the power trace and fires when the live signal matches it. That is the answer when the target gives you no trigger pin at all, which is the normal case outside a training board.

The glitch side

The same board generates faults, in two forms.

Clock glitching inserts an extra or malformed edge into the clock the capture board is already supplying, so an instruction fetch or a register latch happens at the wrong moment. scope.glitch.output is set to "clock_xor" or "clock_or" for this. It only works on a target that runs from an external clock you supply, which in practice means a training board or an older MCU. A part booting from its internal RC oscillator cannot be clock-glitched at all.

Voltage glitching briefly shorts the target's core supply to ground through a crowbar transistor, collapsing the rail for a fraction of a cycle. The crowbar MOSFETs are on the capture board, low-power and high-power, and you enable them with scope.io.glitch_lp = True and scope.io.glitch_hp = True; scope.glitch.output becomes "glitch_only". This is the technique that still applies to a real device, and it is what the secure boot bypass in the fault-injection course is built on.

Both are placed with the same three numbers, and their units are where nearly every beginner goes wrong:

Setting What it means Unit
scope.glitch.ext_offset how many target clock cycles after the trigger to fire clock cycles
scope.glitch.repeat how many consecutive cycles to keep glitching clock cycles
scope.glitch.width / .offset the shape of the pulse inside one cycle, and its phase percentage of one clock period on Lite and Pro

There is no nanosecond and no microsecond anywhere in that table. The WHEN is a cycle count, and the pulse shape is a fraction of a period, so both scale with clkgen_freq: change the target clock and every glitch parameter you found moves with it.

A capture and a glitch, end to end

import chipwhisperer as cw

scope  = cw.scope()                       # autodetects Nano/Lite/Pro/Husky
target = cw.target(scope, cw.targets.SimpleSerial)
scope.default_setup()                     # sane clock, gain, trigger

# --- one power trace -------------------------------------------------
trace = cw.capture_trace(scope, target, bytearray(16), bytearray(16))
print(trace.wave.shape, trace.textout)    # samples, and what the target replied

# --- one voltage glitch ----------------------------------------------
scope.glitch.clk_src    = "clkgen"
scope.glitch.output     = "glitch_only"
scope.glitch.trigger_src = "ext_single"
scope.io.glitch_lp      = True            # arm the on-board crowbar
scope.glitch.ext_offset = 1200            # WHEN, in target clock cycles
scope.glitch.repeat     = 3               # HOW MANY cycles
scope.glitch.width      = 8.2             # pulse shape, percent of a period

scope.io.nrst = "low"; scope.io.nrst = "high_z"   # reset, then let it run
scope.arm()
scope.capture()
print(target.read(timeout=100))           # NORMAL, silence, or something new

scope.default_setup() is doing real work in that snippet: it sets the clock, the ADC source, the gain, the trigger pin and the IO routing to the values every tutorial assumes. Reading what it sets, once, saves a lot of guessing later.

Which model, and what actually differs

Board What it is for
ChipWhisperer-Nano the cheapest teaching board, with its target integrated; coarse glitch timing and a low fixed sample rate
ChipWhisperer-Lite (CW1173) the standard rig: OpenADC capture, clock generation, crowbar glitching, available with an integrated target
ChipWhisperer-Pro (CW1200) a much larger capture buffer, trace streaming, and the SAD trigger
ChipWhisperer-Husky the current flagship: higher sample rate and ADC resolution, finer glitch phase control, SAD trigger

Husky gates its modules: scope.glitch.enabled = True before any glitch setting takes effect. A Lite script copied onto a Husky silently does nothing until that line is added.

Where it stops

  • Synchronous capture needs a clock you can drive. On a target with an internal oscillator, a PLL you cannot reach, or aggressive clock gating, you fall back to asynchronous sampling and inherit every alignment problem a plain oscilloscope has.
  • Clock glitching barely applies outside training boards. Nearly every modern part boots from an internal clock source. Assume voltage or EM for real targets.
  • Voltage glitching needs the core rail. A modern SoC with on-die regulators, several rails and hundreds of decoupling capacitors may not see your crowbar at all. The bench work of finding, exposing and decoupling the right rail is most of the effort, and it is destructive.
  • It is not an oscilloscope. The capture buffer holds a window, the front end is single-ended and gain-limited, and there is no second channel to probe something else with. Keep a real scope on the bench for debugging the rig.
  • It reads nothing on its own. It has no debug transport; it cannot halt a core or dump memory. That is a debug probe job, and the two are used together.
  • A negative result is not immunity. "No glitch found across this sweep, with this rig" is a finding. "Not vulnerable to fault injection" is not something one campaign establishes.

Pitfalls

  • Reading width and offset as time. On Lite and Pro they are a percentage of one clock period, and the delay from the trigger is ext_offset in clock cycles. A course or lab shim that exposes offset_us and width_ns (the Espilon fault-injection lab does) is teaching the concept with friendlier units; the hardware counts cycles, and your notes do not transfer verbatim.
  • Glitch settings with no crowbar enabled. Everything reads back correctly and nothing happens, because scope.io.glitch_lp and glitch_hp are both still False.
  • No clock reaching the target. scope.io.hs2 = "clkgen" is what puts the generated clock on the HS2 pin. Without it, clock glitching does nothing and synchronous capture is not actually synchronous.
  • Not resetting between attempts. A target left crashed by attempt 41 will report a "success" on attempt 42 that has nothing to do with attempt 42. Reset, and verify the target answers normally, before every shot.
  • Sweeping before the trigger is stable. If t=0 moves, ext_offset means a different instruction each run and the sweep is measuring noise.
  • Firmware and Python package out of step. The capture board carries its own firmware and cw.scope() complains when it does not match the installed package. Take the warning seriously and run scope.upgrade_firmware(); a mismatched pair fails in ways that look like target behaviour.
  • Comparing traces captured at different gains or clock frequencies. Both change the trace shape. A campaign has to be captured under one configuration, start to finish.

Further reading