The runtime-level security issues that come from the MicroPython
interpreter and its bundled TLS stack, as opposed to the app-layer
protocol weaknesses documented in SECURITY_ANALYSIS.md
(cleartext CBOR/TCP, unsigned replies). These are the issues that could let
someone own the box, not just spoof what it displays.
The image reports MicroPython v1.12-663-g9883d8e81, built 2024-12-11 on
ESP-IDF v3.3.2. Two facts frame everything below:
- It isn't literally 1.12 — it's the 1.12 tag plus 663 commits, a dev snapshot between 1.12 (Dec 2019) and 1.13 (Sep 2020). So the runtime is ~4 years stale at build time.
- Most MicroPython hardening — fuzzing fixes, real
sslcertificate support, WebREPL improvements — landed in 1.13–1.19, i.e. after this build's cutoff. - The runtime is frozen into a Bitcoin-signed image. You cannot patch MicroPython independently of Coinkite. Every issue here is only fixable by a Coinkite OTA.
The firmware freezes in webrepl.py, webrepl_setup.py, and the C module
extmod/modwebrepl.c. WebREPL is a raw Python REPL over WebSocket — if
it can be reached it is arbitrary code execution on your LAN: reading
NVS WiFi creds, the shared BACKEND_AUTH_KEY, everything. Its 1.12-era
model is weak on three counts:
- Cleartext transport — plain
ws://(port 8266), notwss://. The password is exchanged in the clear and is sniffable on the LAN segment. - Password is 4–9 chars, single global secret (
New password (4-9 chars):is in the strings) with no rate-limiting / lockout in this version → practical online brute force. - Latent footgun — even if off by default, it is one config toggle away from being a network-exposed root shell.
No static proof was found that Coinkite auto-starts it — the
webrepl.start()/ "manual override mode" strings are the stock library's own text, not evidence of a wired-up boot path. Honest status: shipped and capable; whether it is live is a per-device config question. Ifws://<device-ip>:8266answers, treat as critical.
A genuine MicroPython 1.12 property, not just an app bug. This version's
ussl has no working cert_reqs=CERT_REQUIRED path on ESP32, and the call
site carries no cert args (confirmed: wrap_socket, server_hostname,
do_handshake present; no cert_reqs / CERT_* qstrs). Every "HTTPS"
the device makes completes against any server certificate:
- Blockstream
blockstream.infocalls (Opendime balance / UTXO lookups) — fully MITM-able by anyone on-path. - The TLS leg of OTA — but OTA is saved by the separate Bitcoin
signature on the
.bin, so integrity survives; only confidentiality is lost there.
The TLS stack dates to early 2020 and carries the known 2.16.x CVE set (Lucky-13-style timing, Bleichenbacher-oracle and side-channel issues, etc.). Cert validation would normally blunt these, but combined with #2 the confidentiality of the Blockstream path is already gone; the mbedTLS age mainly adds DoS/downgrade surface.
1.12 predates most fuzzing-driven memory-safety fixes. The untrusted inputs
that reach C code on this device are the WebSocket framing
(modwebrepl.c — note the size == 1, buf_sz == 1 assertion strings,
exactly the edges fuzzers poke) and HTTP request/header parsing. A
malformed frame realistically yields a reboot/DoS of the display;
memory-corruption-to-RCE is plausible but far harder.
Mitigating nuance: the backend CBOR channel is parsed by a pure-Python
cbormodule, not C. So even though an on-path attacker fully controls those replies, the worst they get through the parser is a Python exception or memory-exhaustion DoS — not a C buffer overflow. This meaningfully lowers the risk of the scariest channel.
Whether Secure Boot / Flash Encryption is enabled is a per-unit eFuse
decision undetermined by the image (see
CUSTOM_FIRMWARE_FEASIBILITY.md; the fact
that UART custom-flashing is feasible implies many units ship with them
off). Where they are off, a physical attacker can dump flash and pull
the WiFi PSK and the shared BACKEND_AUTH_KEY from NVS.
The secp256k1 implementation is sound (libngu), but the entropy that feeds it is not — see finding #6. Don't read "uses a proper crypto library" as "the crypto is safe."
Correcting the framing that a "proper crypto library" implies safe crypto: the implementation (libngu's constant-time secp256k1) is fine, but on a Bitcoin-adjacent device the part that matters is the entropy behind it, and that is the weak link.
- The RNG path bottoms out in the ESP32 hardware RNG (
esp_fill_random,hw_random.c) vialibngu/ngu/random.c. Espressif documents thatesp_random()returns true random numbers only while the RF subsystem (WiFi/BT) is running; called with the radio off — early boot, or before the device associates — it degrades to a weak, largely deterministic source. - In practice the generator was found not to be drawing actual hardware randomness for its entropy, so values that must be unpredictable (secp256k1 signing nonces, any on-device key/seed material) can be biased or guessable rather than random.
- Consequence: a predictable ECDSA nonce lets an observer recover the signing private key from as few as two signatures; weak seed entropy makes any generated key guessable. This is the classic way deployments that use a "proper crypto library" still lose keys — the library is only as strong as the randomness it's handed.
Blast-radius caveats specific to this device (why it's ranked here, not at the top):
- The only secp256k1 signing in normal operation is the backend auth
handshake, which uses the fleet-shared
BACKEND_AUTH_KEYthat is already extractable from firmware — so recovering it via nonce leakage yields nothing an attacker didn't already have. - The device reads Opendime addresses rather than minting spending keys
on-chip (the Opendime seals its own key), so no obvious high-value key is
generated with this RNG. The firmware also carries an Opendime
entropy-load path (
Opendime entropy load starting,OD_ADD_ENTROPY,OD_GET_ENTROPY_COUNT) — an attempt to source entropy externally, which mitigates the ESP32 weakness when an Opendime is present. - If libngu signs with RFC 6979 deterministic nonces (standard in Bitcoin tooling), the signing path needs no RNG at all and is unaffected — worth confirming before assuming signatures leak the key.
Net: the weakness is real and "not a weak spot" was the wrong call, but for this product's current operations the practical exploitability is bounded by the shared-key and read-only-address facts above. It becomes critical for any firmware variant that generates keys or seeds on-device.
- Segregate it. Put the Blockclock on a guest/IoT VLAN. Its LAN-facing
services — the
adminHTTP Digest-auth UI and WebREPL (if live) — are the real exposure. Treat anyone on the same segment as able to reach a possible root shell. - Check for / disable WebREPL and confirm the admin password is set. This is the one issue that turns "annoying display spoof" into "device takeover."
- Don't trust its "HTTPS." No cert validation + old mbedTLS = MITM-able. Fine for ambience, not for anything you act on.
- Physical access = secret extraction on units without flash encryption; rotate WiFi creds if a unit leaves your control.
| Claim | How established |
|---|---|
Build is v1.12-663-g9883d8e81, ESP-IDF v3.3.2 |
Version strings in latest.bin |
| WebREPL is frozen into the image | webrepl.py, webrepl_setup.py, extmod/modwebrepl.c in strings + module table |
| WebREPL password is 4–9 chars, cleartext ws | Stock library strings (New password (4-9 chars):, Sec-WebSocket-Key); standard MicroPython 1.12 behavior |
| WebREPL auto-start on this unit | Not proven — no wired-up boot path found statically; config-dependent |
ussl call site has no cert args → CERT_NONE |
qstr/string search: wrap_socket + server_hostname + do_handshake, no cert_reqs/CERT_* (matches SECURITY_ANALYSIS bytecode read) |
| mbedTLS ≈ 2.16.x | Implied by ESP-IDF 3.3.2 bundle; MBEDTLS_ERR_* strings present |
| Backend CBOR parsed in pure Python | cbor/cbor.py etc. in frozen module table |
| secp256k1 implemented by libngu; entropy path is the ESP32 HW RNG | libngu/ngu/random.c, secp256k1*, esp_fill_random, hw_random.c strings |
ESP32 esp_random is a TRNG only while RF (WiFi/BT) is active |
Documented Espressif behavior |
| Generator observed not using true hardware randomness for entropy | Reverse-engineering finding — not re-derived from static strings in this doc |
| Firmware also has an Opendime entropy-load path (partial mitigation) | Opendime entropy load starting, OD_ADD_ENTROPY, OD_GET_ENTROPY_COUNT strings |
| Secure Boot / Flash Encryption status | Per-unit eFuse, not determinable from image (see CUSTOM_FIRMWARE_FEASIBILITY.md) |