Trust the Wire?

Published 2026-09-19
Trust the Wire?
Series: Whose computer is it?Part 8 of 8

From a series of notes on bringing up our own OS on three boards: an STM32F4VE breadboard with an ESP32 module on stock ESP‑AT, the ESP32 itself, and a Samsung Galaxy Tab S6 tablet (Snapdragon 855). "The Parasite in the Tablet" ended with the conclusion: the radio firmware sees all link‑layer traffic, the WPA keys leave for it (in detail — The Password Is Ours, the Key Is the Modem's), so confidentiality is built above — on TLS. This note is about what we actually have at that level, what we do not, and what it will take to have it.


1. The honest state in one paragraph

On all three boards HTTPS works and encrypts the channel. On none of them is the server certificate verified. The transport is closed to a passive observer and open to anyone who can answer for the name api.cursor.com or id.0xd.to before the real server does — the access point, the ISP, a hotspot and, first of all by proximity, the radio firmware. This is documented in the code with the words "trust the wire" and in the threat model as the single item that turns "the modem sees the traffic" (unavoidable) into "the modem substitutes traffic and tokens" (fixable). Below — why it turned out this way, what exactly flows through these connections, and at what price it is closed.

2. What there is: two TLS stacks on three boards

2.1 Our own stack: the ESP32 and the tablet

crates/tls is a wrapper over embedded-tls (version 0.19, TLS 1.3, one cipher suite Aes128GcmSha256). TlsStream takes any kernel ByteStream — a smoltcp TCP stream, a modem socket — and returns a ByteStream back, so the HTTP client from crates/ai and everything written against the trait goes over HTTPS without changes. The record buffers — 16 KiB + 640 bytes for receive and 4 KiB for transmit — belong to the caller and live exactly one exchange: the web module either allocates them per request or (on boards with a heap of a couple of hundred kilobytes) reserves them once on connect, so as not to search for a 16‑kilobyte block in a fragmented heap in the middle of a request. The handshake state beyond the buffers is about 2.5 KiB on x86‑64; a test in the crate keeps it from growing past 8 KiB.

Randomness for the handshake is the platform's Entropy device. Where there is none, Rng warns in the log and falls back to xorshift seeded with time since boot — "enough to talk to a test server, not enough to keep a secret", as the crate itself says. By the platforms' device tables: the ESP32 has a hardware RNG, the host and Android have /dev/urandom; QEMU, the F4VE and the tablet have entropy: None. For the F4VE this does not matter (see 2.2), for QEMU it is expected. For the tablet, where tls and web are already in the build, it means that today the handshake there is seeded with uptime — a separate item to fix, worth confirming with a live run.

The crypto provider is our own, minimal: randomness from Rng, no verifier, no client certificate. The library's UnsecureProvider was not taken because it drags in an ECDSA signer for client authentication — sec1 + der, about 22 KiB of Xtensa code that none of our servers ask for. This is the only figure about the cost of cryptography the sources give; we will come back to it in § 5.

The verification policy is absent. In embedded-tls the verifier is a provider method that by default answers "not implemented", and our provider does not override it: any server certificate is accepted. The host name does go into SNI — servers behind a CDN do not answer without it — but SNI here is only an address, not a check.

On the diagram it looks like this — a fragment of the series' general map from Whose Computer Is This, with the same node names. What matters is what is not on it: the radio does not see TLS plain text — already encrypted records reach ath10k, and everything the WLAN firmware reads is ciphertext plus metadata (SNI, DNS, the rhythm of requests). It gets plain text only along the dashed lines — where we ourselves do not check who we are talking to, and what we seed the handshake with.

Diagram: Trust the Wire?

The two bold frames are the two places where confidentiality on the tablet is actually decided: the provider without a verifier and the Rng without entropy. Everything else on the diagram is transport.

2.2 Someone else's stack: the F4VE behind a modem

On the F4VE the microcontroller has no cryptography at all. Modem::connect_tls sends the module three commands: AT+CIPSSLCCONF=0, AT+CIPSSLCSNI="host", AT+CIPSTART="SSL","host",443. The handshake is done by mbedTLS inside ESP‑AT; the MCU sees plain bytes over the UART. The first command is an explicit choice "do not authenticate the server": the module has no set of root certificates and accepts whoever answers for the name. The driver ignores the errors of the two configuration commands — without them the firmware simply cannot do SSL, and that will come out on connect. A live run (TASKS, line 23): the handshake with api.cursor.com about 2 s, request and response 491/509 bytes.

Here there are two observers of the plain text, not one: the module itself (its firmware is closed Espressif code, as in § 5.2 of "The Parasite") and the UART wire, physically accessible on the breadboard. By construction the second is not curable, the first — below.

The same diagram for the breadboard — and it is arranged the other way round: here TLS is terminated by the modem, and the HTTP plain text lies not in our memory but on the wire between two chips (notation — as on the general map; the F4VE nodes are its own, there are no tablet nodes here).

Diagram: Trust the Wire?

There is no arrow from the module to the MCU on the diagram, and that is not an omission: ESP‑AT has neither DMA nor shared memory with the STM32 — only bytes over the UART, which the driver parses as text. The module sees and can substitute everything that passes through it, but it has no path into the microcontroller's memory; on the tablet, where the radio is a DMA master in our RAM, both troubles live in one chip.

2.3 The third stack: the host

For completeness: on the host and under Android the same Fetch service is implemented via ureq with rustls and the platform verifier (under Android — the Mozilla root set, because the system one is available only through JNI). One service, verifying the server on one platform and not on three others: the rule "a common policy in one place" is violated here by a fact of life, and that hints at where to put it later (§ 6).

3. What flows through these connections

Fetch is one service for all consumers, and it is convenient to list them, because the list of consumers is the list of what is substitutable.

Separately — what is not there: image updates (sys.update.*) and .oxm modules on the tablet today go over the agent link via USB, not over the network. The threat model honestly calls this protection "until it is connected", not architectural: as soon as an update goes through Fetch without image signing, a MITM becomes code execution on the next boot.

4. Why "no clock" is a problem, not an excuse

A certificate is valid in the window notBefore … notAfter. Checking the chain without knowing the current time either accepts expired certificates (and then the leaked key of last year's certificate works forever) or rejects them all. The embedded-tls verifier, in the absence of a clock, substitutes time 0 and — per the comment in its source — the validity check is guaranteed to fail. So "no clock" means not "the check is slightly weaker" but "the check is impossible with the code that exists".

But we do not quite have "no clock". The kernel keeps a WallClock — the offset between uptime and Unix time — which may be unset. Sources, ranked: SNTP (ESP32 and tablet, on net.up and once a day), the RTC (on the F4VE — on a 32,768 Hz crystal with a CR1220 battery, on the tablet — the PMIC RTC plus an offset in 128 bytes of SDAM), a neighbouring node's time, and time.set from the user. More precisely: there are clocks, but (a) there may be none at the moment of the first request, and (b) their only network source is SNTP without authentication, over the same channel that the same observer controls.

How frightening (b) is should be weighed rather than feared. The clock in certificate verification decides one thing: whether a genuine certificate, signed by a trusted root, has expired. A signature cannot be forged by moving the clock. An attacker controlling SNTP extends the window for a compromised and already expired key — a noticeably smaller hole than "we accept any certificate". The options follow from this.

5. Options and their price

Pin the root and require a clock. One or two DER certificates of the authorities that signed our endpoints; verification of the chain, the name and the validity; without time — refuse. Plus: survives scheduled reissues of server certificates. Minus: the first request after a cold boot without an RTC waits for SNTP; "refuse without a clock" has to be decided explicitly (wait, ask a neighbouring node, take the RTC).

A monotonic time anchor instead of a clock. The device knows that now is not earlier than: the image build date, then the last successfully verified time (SNTP or the Date header from an already verified TLS response), stored in the store and never going backwards. With a lower bound L ≤ now the window is checked half‑honestly: a certificate with notAfter < L has definitely expired — refuse; notBefore > L — unknown whether to accept, but "not yet valid" is rarely an attack. This removes the dependency on SNTP at boot and costs one key in the store; the strictness is lower than that of a real clock, and this must be written into the threat model, not hidden.

Pin the leaf or its key (SPKI pin). No clock needed at all: the hash of the server's public key is compared. Suitable for a server we control (id.0xd.to) and whose key we decide not to change on reissue. Not suitable for api.cursor.com and the provider APIs — their keys change without us, and the device would "brick" silently.

TOFU — trust on first use. The first connection memorises the server's SPKI in the store, subsequent ones require a match. Protects from a MITM after the first time, not from the first. And it depends on the store: on the ESP32 that is a flash partition, on the F4VE — 60 KiB of NOR, and on the tablet the store today lives in RAM (Block = NoBlock), so TOFU there would be reset by every boot.

Which of these first. The device talks to a small fixed set of names: id.0xd.to, the endpoint of the chosen AI provider, the connectivity probe. For such a set a pinned root (or two) is the right first step: it closes MITM for the whole list at once, requires no state on the device and does not break on a scheduled reissue of server certificates. The clock for it — the RTC where there is one, SNTP where there is not, and the monotonic anchor as insurance against "cannot verify because we do not know what year it is". The SPKI pin is a second layer for our own server, on top of the root, not instead of it.

The price, as far as it can be named. The sources give one reference point: the ECDSA signer with sec1 and der — about 22 KiB of Xtensa. A verifier — X.509 parsing (webpki), signature verification over P‑256 (P‑256 arithmetic is already in the image — p256 is used by both TLS and SAE in crates/wpa) and one or two roots at ~1–1.5 KiB DER each — is a quantity of the same order, but there is no measurement, and any number before a build would be a guess. Heap: the chain arrives inside a record that the 16‑kilobyte buffer already holds; the embedded-tls verifier also needs a copy of the leaf (the CERT_SIZE parameter) — units of kilobytes for the duration of the handshake. For the ESP32 with a ~220 KiB heap, a ~137 flat level and peaks of 170–200 this is tight but possible. For the F4VE the question does not arise: TLS there is computed by the module, and the image's flash budget is measured in hundreds of bytes (remote 507–508 KiB against a 496 KiB slot).

Two library details that will have to be taken into account — both from the embedded-tls 0.19 source, and both worth re‑checking against the version in the build:

6. Where it lives

§ 2.3 suggests the answer: the verification policy is in crates/tls, one for all consumers of Fetch, because TlsStream is the only place through which HTTPS leaves the board with our own stack. The provider is already there; the verifier is its method. The platform, as with Entropy, only provides: trust anchors (static DER in the image), the clock (kernel.unix_ns(), which is already an Option) and, for the monotonic anchor, a key in the store. The web module passes this into TlsStream::open the same way it now passes Rng. The host and Android with ureq stay on the platform verifier — there it is more honest than any of ours.

For the F4VE the policy is also one, but executed in the module: ESP‑AT has CIPSSLCCONF modes with server verification by a certificate loaded into the module's partition — this is a statement about Espressif's firmware, not about our code, and it must be checked on the ESP‑AT version on the breadboard before relying on it. The esp-at driver then changes one command and stops swallowing its error.

7. Back to the parasite

What verification changes for the radio firmware, which has the traffic keys and the whole air. Without it, it is a MITM: it answers for id.0xd.to, takes the refresh token, substitutes the model's answer, and with network updates — code. For this it needs neither DMA nor any of the channels that stage‑1 SMMU, the rmtfs shadow and the tqftp scratch lock down. In other words, all the isolation work below loses its meaning at the application level: why break the fence if the door is not locked.

With verification it is an observer of metadata: names (SNI in TLS 1.3 without ECH goes in the open), addresses, the time and volume of requests. That is not little — by the rhythm of requests to an AI API one can see when and how much a person is talking — but it is a different category of threat, and with it the priorities from "The Parasite" converge: the IOMMU closes the pivot into the host, TLS with verification — the content, the shadow instead of the disk — persistence. While CIPSSLCCONF=0 and the provider without a verifier stand, two of three are closed, and the third is the cheapest.


Sources: crates/tls/src/lib.rs (Buffers, Rng, Provider, the test handshake_state_fits_a_board), crates/tls/Cargo.toml; crates/esp-at/src/{modem,lib}.rs (connect_tls, open); crates/tools/src/{web,account,ai,captive}.rs, crates/ai/src/{http,dialect}.rs; crates/hal/src/entropy.rs; platforms/*/src/platform.rs (device tables), platforms/hosted/src/fetch.rs, platforms/stm32f4ve/src/rtc.rs, platforms/sdm855/src/persist.rs; crates/kernel/src/{time,boot}.rs (WallClock, unix_ns); docs/wifi-threat-model.md § 6.2, § 6.4; ARCHITECTURE.md (net, esp-at, web/Fetch, Account, Time); TASKS.md lines 23, 37, 48, 55, 56; the embedded-tls 0.19 source (src/webpki.rs, src/config.rs, Cargo.toml) from the local cargo registry.

Part 8 of 8
  1. 1Whose Computer Is This? Yours, or…
  2. 2A Reset Without Witnesses
  3. 3The Megabyte That Killed the Modem
  4. 4The Parasite in the Tablet
  5. 523 Milliseconds to Lie
  6. 6The Hypervisor Said No
  7. 7The Password Is Ours, the Key Is the Modem's
  8. 8Trust the Wire?

Products in this article

0xd-ossecuritytlsembedded
Try it