Limited items in stock
View Purchasing OptionsProject update 6 of 11
Whew, how time flies. The campaign has already passed 100 backers and it’s amazing to see the graph rising day by day. It might be a little cheesy by now, but I really do want to thank all of you for supporting this journey.
Quick challenge update: four golden swords have already been claimed by some amazing hackers. It’s awesome to see the community jump in so instinctively and I want to thank everyone who has taken the time to solve it. But remember: there are five golden swords. The last one is still out there, waiting for someone to grab it. Maybe that someone is you?
This week I wanted to share a peek into the update mechanism. How can an update that runs straight through your browser be both secure and secretive — and still work with a 10-cent MCU? How can firmware be built, shipped, flashed remotely, and unlocked safely on the device without ever being exposed to anyone but the build process (a.k.a., me) compiling it?
Here’s why I designed the update mechanism the way I did:
So the requirements boiled down to:
Which all translates into: let’s write a secure, encrypted bootloader.
The CH32V003 has 16 KB of flash. I dedicated 4 KB for the bootloader (this part never changes) and the remaining 12 KB for the application. That 4 KB has to hold:
memcpy, strlen, etc.)Yeah… that’s a lot crammed into not a lot.
The reason all of this has to live inside the bootloader is simple. If the bootloader depended on code sitting outside its dedicated 4 KB space, say at some address X, things would quickly fall apart. In one firmware, address X might point to the function the bootloader needs. But in another firmware, that same address could contain something completely different.
Here’s the memory map I ended up with (also in the GitHub repo at src/framework/ch32v003fun.ld):
ENTRY( InterruptVector )
MEMORY
{
…
FLASH_ISRVEC (rx) : ORIGIN = 0x00000000, LENGTH = 192
FLASH_TOP (rx) : ORIGIN = 0x000000c0 LENGTH = 3904 // 4K - 192
FLASH (rx) : ORIGIN = 0x00001000, LENGTH = 12K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 2K
…
}
Flash works in pages. On this chip, the smallest page is 64 bytes. That means every erase and write happens in 64-byte chunks. This pretty much dictates how the whole update flow has to work.
Usually you’d find a nice library or example code to handle flash. Not here. For the CH32V003, there was basically nothing online and the programmer’s manual… let’s just say it’s not perfect: Incomplete, sometimes wrong, occasionally misleading. Ask me how I know…
After a lot of trial and error, I wrote my own framework for erasing, writing, and reading flash. You can find it in src/prot.c on GitHub.
The good news: the chip does have a simple security option to lock the flash from external reads. That instantly solves the dumping problem. I can store keys or other secrets on the flash and as long as they are only decrypted on the device itself, they stay safe. If you’re into this kind of thing, check out Chapter 16 “Flash Memory and User Option Bytes” in the programmer’s manual.
To provide the secrecy element for the firmware, encryption is a must. I went with AES-128-CBC because it’s the most standard (pun intended) and just best practice. One of the nice features of CBC is right there in its name: Cipher-Block-Chaining. It makes it harder to learn much about any single block without the others, and even better, any change ripples forward to the next blocks. That means you can’t just tweak one piece without messing up everything else.
Building the encryption tool is the easy part: take a 128-bit key and the firmware.bin file, and encrypt it. The real challenge is on the device side. The chip only has 2 KB of RAM versus 16 KB of flash. That means I can’t just load the whole firmware, decrypt it, and flash it all at once. And since flash only works in 64-byte pages anyway, I had to design the whole thing around receiving 64-byte chunks, decrypting them, writing them to flash, and then moving on.
CBC adds one more wrinkle: the last block of each decrypted chunk has to be saved as the IV for the next one. So the device decrypts a few blocks at a time, keeps the chain going, and repeats until the entire firmware is in place.
So the update flow is:
Since CBC needs the last block of one chunk to decrypt the next, I save that block as the IV each time and keep going.
After all that, I wanted the interface to be painless. The only requirement is sending 64-byte chunks over UART. And as it turns out, modern browsers actually support serial ports! A little JavaScript later, I had a firmware update page running right on the Sword’s website. Plug in, click, done.
Whew. That was a long post and it only scratches the surface of the design decisions and quirks of this MCU. But I wanted to give you a sense of how the update system works, how it sets the stage for secure boot, and why it adds so much value to the Sword.
One caveat: this whole model leans heavily on the assumption that the CH32V003’s flash lock is actually secure. I haven’t tried to break it with fault injection or other out-of-band attacks (yet!). I’d love to experiment someday or see someone else try to dump the firmware anyway!
Thank you again for supporting the project and for being so engaged. It really makes this fun.
More soon,
Gili
Sword of Secrets is part of PCBWay Assembly Hub