Decisions · ADR 0010

The Debug Console is in every build, off until its owner switches it on

There is one firmware. The Debug Console of ADR 0004 is compiled into it, and so are the commands that used to exist only in Debug Builds. It listens only while Settings → Debug Console is on, which is not the default, and it…

There is one firmware. The Debug Console of ADR 0004 is compiled into it, and so are the commands that used to exist only in Debug Builds. It listens only while Settings → Debug Console is on, which is not the default, and it lets in whoever proves they hold the device's own token. The cardputer-adv-debug environment, RORO_DEBUG, the +debug version and the token compiled in from the builder's machine are gone.

ADR 0004 kept the console out of release builds with one argument: a console that runs commands is a remote control, so in a release nothing should listen. That argument was never whole: the Update Service has listened on TCP 3232 in every build since Firmware Updates exist, guarded by a signature. A listener that is off by default and guarded by a secret the device made itself is the same kind of trade, and what the split cost had grown:

  • What was tested wasn't what shipped. Development ran on Debug Builds; releases were a different binary, built to be published and never run by the person who wrote them.
  • Debug Builds couldn't be published, since each carried its builder's token. So the console, rdbg.py, screenshots and crash decoding were for whoever built the firmware, and nobody else.
  • Rules that existed only to protect the console: a Debug Build never installed a release (it would have lost the console), update install … force to do it anyway, "keep a Debug Build in the fallback slot", versions with +debug that had to compare equal to their release.
  • CI built two firmwares on every pull request and every tag.

How it works

  • Off means nothing is there. With the setting off, no socket listens, the console's task doesn't exist and neither does its 4 KB ring: a device that never uses the console pays for it in flash (30 KB more than the release build was) and 88 bytes of static RAM, measured. A missing or invalid stored setting counts as off.
  • The token is the device's. The first time the console is switched on, the device makes one from its hardware random generator: 100 bits, as 20 characters of Crockford's base32 (K7QF-3M2X-9WBD-HT4P-6RNC), shown on the Debug Console page and nowhere else. It can be replaced by one typed by hand, of 16 to 64 characters. It is stored with the other settings and is never printed on a console.
  • The token never crosses the network. The device sends 16 random bytes; the client answers with their HMAC-SHA256 keyed by the token. Someone on the same Wi-Fi who records a login has nothing they can use for the next one.
  • Five wrong answers in a row close the console to everyone for a minute, with a Notification naming the address they came from. A wrong answer still costs a second.
  • DBG in the Status Bar while the console listens, bright while a client is connected.
  • USB serial can set it up: debug on, debug token <value>, debug token new. scripts/flash.sh --debug uses them to give a freshly flashed device the developer's token. Whoever holds the cable can flash anything anyway (ADR 0003); the console itself can only switch itself off.
  • Safe Mode starts the console if it's switched on: the settings are read before Safe Mode is decided.

Consequences

  • "Nothing listens" now rests on one stored setting, not on absent code. That setting is typed, validated and host-tested, and its default is off; a bug that flipped it would have to come from code that already runs on the device.
  • Whoever has the token and the network has the device: the console, its keys, the SD card's files, a restart. Not its firmware: an update still has to be signed (ADR 0003).
  • The stream is still plain text. The token is safe from an eavesdropper; what the console prints, and the files it carries, are not. TLS would cost about 52 KB of the 107 KB there is, next to IRC's own connection: not for a console.
  • An old rdbg.py can't talk to a new firmware, and the reverse: the first line of the protocol changed. Accepted, with one user.
  • A device whose settings are damaged has no console in Safe Mode, only the signed update push. Before, a Debug Build always had it.
  • The commands that crash, damage a download or fill a folder on purpose are in every build. They need the cable or the token, like everything else.
  • Releases already publish their ELF, so rdbg.py crash fetches it when it isn't in .pio/elves/: crash reports can be decoded without having built the firmware.

This page is generated from docs/adr/0010-debug-console-in-every-build.md in the repository. To change it, change that file and run site/tools/gen_dev_docs.py.