The USB wasn't working (which was the planned interface to use), so I suggested using one of the UARTs. The next trick was to find some code for talking to the UART bootloader. I tried stm32ld and jsnyder's stm32loader and they would both recognize the chip but then bailed when trying to unprotect.
I poked around a bit and came across this variant from the Espruino project. That version works on my STM32F4DISCOVERY board and it worked on my brothers board.
The UART bootloader wants a raw .bin file without any special headers or anything, so I used:
arm-none-eabi-objcopy -O binary flash.elf flash.bin
to create a suitable binary file (flash.elf came from the MicroPython build).
To initiate the bootloader on the UART you need to make BOOT0 high, and then press and release RESET (keep BOOT0 high while releasing RESET). Then run:
./stm32loader.py -p /dev/ttyUSB0 -evw flash.bin
Looking at the stm32loader.py code, it appears to support using DTR to reset the processor, and RTS to control BOOT0. I haven't tested this out yet, but it looks to be worth checking out.