cardputer-signer

deadhand: a dead man’s switch on a Cardputer

Long-running things (an agent working overnight, a render, a trading bot) keep running only while a human says “I am still here”. Every N minutes you press the side button of the M5 Cardputer ADV. The press produces a signed, timestamped pulse. If the host stops seeing valid pulses, it stops the process and writes down why.

The signature matters: the key lives on the device and only a physical button press uses it, so the process being watched cannot extend its own leash.

What the device does

Build and flash

You need PlatformIO.

cd firmware/deadhand
pio run                                              # build only
pio run -t upload --upload-port /dev/cu.usbmodemXXXX # flash

This replaces whatever firmware is on the board (for example signer). Both can be re-flashed any time; each keeps its own key in its own NVS namespace, so flashing one does not delete the other’s key, but they are different keys and trusted separately.

Opening the serial port resets the board, and holding the side button during a reset enters the ROM bootloader (blank screen). Do not hold the button while connecting over USB.

First pairing

The device generates an ed25519 key on first boot. Tell the host to trust it, once:

tools/deadhand/deadhand pair --source usb

It prints the key fingerprint. Compare it with the FP on the device screen (bottom line) and answer y. After this the host accepts pulses only from this key.

Personal settings

Edit src/config.h, rebuild, flash:

Setting Meaning
DH_BRIGHTNESS screen brightness, 0..255
DH_SOUND, DH_VOLUME sound on/off, volume
DH_WARN_AT_S seconds-left list at which it beeps (0 = at expiry)
DH_WARN_COLOR_S when the plate turns orange
DH_GRACE_BEEP_S blip period during grace (0 = off)
DH_BLE_NAME, DH_DEFAULT_ID BLE name; default label inside pulses

The interval and grace are not in the firmware: you set them per run on the host (--interval 40m --grace 5m), and the device signs them into every pulse, so the host and the device always agree.

Pulse format

A pulse is a signcheck v1 message: kind = pulse, choice = alive, id = the run label, and text = ctr=<n> int=<seconds> grace=<seconds>. ctr is a counter stored in the device flash and increased on every press, so an old pulse can never be replayed. The signed iat is host-synchronised device time; a pulse is valid for 10 minutes after it is made, which only matters for very slow links.

Serial / BLE protocol

One JSON object per line.

Before the first hello the device shows NO HOST and a button press does nothing.

Limitations

Prototype. The secret key sits in plain flash (ESP32 NVS), no flash encryption or secure boot. Someone with the device and a USB cable can extract it. The device does not store pulses: a press while nothing is listening is lost.