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.
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).
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.
- AI provider APIs (
crates/ai,tools::ai). Every request carriesAuthorization: Bearer <key>from the device settings; the response is text the device shows as the model's answer. Substitution yields both the key and the contents of the conversation in both directions. - The 0xD account (
tools::account). OAuth 2.0 device flow againstid.0xd.to: a code on the screen, polling/oauth2/token, thenuserinfo. The refresh token lies in the store undersys/account, the access token in RAM,AccountHandle::bearer()hands it out to modules. Whoever answers forid.0xd.togets the refresh token on the very first refresh — that is, the account, not a session. net.fetch(tools::web). HTTP as a machine capability for the agent and neighbouring nodes (esp32.net.fetch). What goes through it is decided by the caller; the service does not distinguish a page from a key.- The connectivity probe and captive portals (
tools::captive). The probeconnectivitycheck.gstatic.com/generate_204goes overhttp://and by its meaning is not authenticated: its task is to find out who is intercepting. But the portal forms the device then fills in carry phone numbers and one‑time codes, and some portals live onhttps://.
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:
- The stock
CertVerifierchecks only the first certificate of the chain against the trust anchor; intermediates are not passed (in the code —TODO: Support intermediates). Public authorities sign leaves with intermediate certificates, so "pin ISRG Root X1" will not work with this verifier: one would have to pin the intermediate, which changes more often — or write our ownTlsVerifierincrates/tlsthat handswebpkithe rest of the chain as intermediates. The second is more correct and does not look large. - The set of signature algorithms in a build of
embedded-tlswithoutallocis only ECDSA P‑256/P‑384 and Ed25519; RSA requires itsallocfeature and thersacrate. Which chainsapi.cursor.comand the AI providers have today cannot be learned from the repository; this has to be taken from the live servers before choosing roots.
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.
VPN Chrome extension: what it protects and what it doesn'tA Chrome VPN extension is a proxy for browser traffic only. What it really protects, where it leaks, and when you need a full VPN app instead.
Turnkey smart homes on our own OS: 0xd-os and 0xd-uiWe build turnkey smart homes on components we designed ourselves. At the core is 0xd-os — one operating system that runs identically on a flagship tablet and on a device with 512 KB of memory.
Why security agents need a sandboxAn agent that can 'just run tools' on a security review is a second attacker — unless every call has a grant. AGI Core isolates tool execution; Forge keeps the skill list in UIDE.