Skip to content

zephyr-cp: working TCP sockets and TLS on the Pico W boards - #21

Open
tyeth wants to merge 3 commits into
zephyr-cp-ble-scan-timeoutfrom
zephyr-cp-pico2w-sockets-tls
Open

zephyr-cp: working TCP sockets and TLS on the Pico W boards#21
tyeth wants to merge 3 commits into
zephyr-cp-ble-scan-timeoutfrom
zephyr-cp-pico2w-sockets-tls

Conversation

@tyeth

@tyeth tyeth commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Three commits, each a thing that had never worked on these boards and was found while wiring up the captive portal (#⁠ softAP PR follows):

  1. TCP was off. No conf in the port enabled CONFIG_NET_TCP for the Pico W / Pico 2 W; every SOCK_STREAM socket failed in net_context_get() with EPROTOTYPE, reported by socketpool as "Out of sockets". Enabled, fd table raised to 16, and three socketpool fixes: a failed socket() left an object with num == 0 whose finaliser later called zsock_shutdown(0) on the socket service's eventfd and branched into cdc_acm_1's data (usage fault, halt); errno is now reported; accepted sockets inherit the listener's timeout (they read as non-blocking, so the first ssl recv raised EAGAIN).
  2. ssl server-side contexts demanded a client certificate (MBEDTLS_ERR_SSL_NO_CLIENT_CERTIFICATE) because the default root bundle set VERIFY_REQUIRED. Servers now default to VERIFY_NONE like CPython; load_verify_locations() still enables client verification. This is a shared-module/ssl change and affects every port.
  3. TLS 1.2 ECDHE could never complete: with P-256 served by the p256-m PSA driver and no builtin ECP module, mbedTLS sizes handshake->premaster from a dummy MBEDTLS_ECP_MAX_BITS 1sizeof(premaster) == 1 in the DWARF — and psa_raw_key_agreement() returned PSA_ERROR_BUFFER_TOO_SMALL (-138) which leaked out as OSError 138. Disabling p256-m restores a 32-byte buffer (upstream sizing bug; Kconfig workaround). PEM parsing enabled for load_cert_chain() with fullchain.pem/key.pem.

Verified on hardware: Pico 2 W serving an RSA-2048 PEM cert over its softAP, ESP32-C6 client → HTTP/1.0 200 OK in 2.0 s. 11 TCP sockets open before ENOENT (12 net_contexts, one held by DHCP).

Size, Pico 2 W: TCP + fd table: RAM 240,432 → 250,240 B (+9,808 B), flash 1,158,036 → 1,171,756 B (+13,720 B); PEM +1,868 B flash; builtin ECP instead of p256-m +4,716 B flash. Final image of this series: 1,176,616 B flash (75.20 %), 250,240 B RAM (47.00 %).

🤖 Generated with Claude Code

tyeth and others added 3 commits September 9, 2026 15:27
Nothing in the port, the SoC defaults or these boards' Kconfig turned
CONFIG_NET_TCP on, so every SOCK_STREAM socket failed inside
net_context_get() with EPROTOTYPE -- which socketpool reported as a generic
"Out of sockets" -- and the web workflow's listener never opened either.
Enable TCP on both boards, and raise the fd table to 16: Zephyr sized it from
the subsystems' declared needs (13 here), which the DHCPv4 server and the
socket-service eventfd eat into.

Three socketpool fixes found on the way:

- A socket object is allocated with a finaliser and zeroed before
  zsock_socket() runs. If that failed, num stayed 0 and the finaliser later
  called zsock_shutdown(0). fd 0 belongs to the socket service's eventfd,
  whose shorter vtable has no shutdown slot, and the CPU branched into
  cdc_acm_1's data: "USAGE FAULT / Illegal use of the EPSR", pc 0x200003f8,
  lr z_impl_zsock_shutdown, system halted. Mark the object closed (num = -1)
  before attempting creation.
- Report errno from a failed zsock_socket() (ENOENT: no net_context left,
  EPROTOTYPE: protocol off, ENFILE: fd table) instead of the fixed message.
- Accepted sockets did not inherit the listener's timeout: the Python-facing
  accept path only set num, so a fresh object read as timeout 0 and the ssl
  layer's first recv on an accepted TLS connection raised EAGAIN before the
  handshake could finish. Inherit it, as the other ports do.

Measured on a Pico 2 W with the softAP up: 11 TCP sockets open before ENOENT
(12 net_contexts, one held by the DHCPv4 server).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
wrap_socket(server_side=True) configured mbedTLS with VERIFY_REQUIRED
whenever the context had the default root bundle attached, which every
ssl.SSLContext() does. On a server that means "require a client
certificate", so a plain HTTPS server -- load_cert_chain() and nothing else
-- failed every handshake with MBEDTLS_ERR_SSL_NO_CLIENT_CERTIFICATE
(-0x7480) and the client saw a timeout.

Default server-side contexts to VERIFY_NONE, as CPython's do (CERT_NONE for
servers); a context that loaded its own CA with load_verify_locations() still
verifies clients. Client-side behaviour is unchanged.

Seen on a Pico 2 W serving a captive portal over its softAP; the same code
runs on every port using the shared mbedTLS module.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… boards

Two Kconfig gaps kept an HTTPS server (or client) from ever completing a
handshake on these boards:

- Zephyr's mbedTLS 4 default serves secp256r1 through the p256-m PSA driver
  and leaves the builtin ECP module out. mbedTLS then defines the dummy
  MBEDTLS_ECP_MAX_BITS 1, and ssl.h sizes the TLS 1.2 premaster buffer
  (union mbedtls_ssl_premaster_secret._pms_ecdh[MBEDTLS_ECP_MAX_BYTES]) from
  it: sizeof(handshake->premaster) was 1 in the firmware (DWARF). Every ECDHE
  key agreement then failed -- "psa_raw_key_agreement() returned -138
  (-0x008a)", PSA_ERROR_BUFFER_TOO_SMALL for a 32-byte shared secret -- and
  the raw PSA status leaked through mbedtls_ssl_read() as OSError 138 on the
  first read of any TLS connection. Disabling the p256-m driver brings the
  builtin ECP module back and the buffer is 32 bytes again. This is an
  upstream sizing bug (ssl.h should size that buffer from PSA when the ECP
  module is absent); the Kconfig is the workaround until it is fixed.

- PEM parsing was not compiled in, so load_cert_chain() with the usual
  fullchain.pem / key.pem could only fail; the collector's certstore is PEM.

Verified on a Pico 2 W: an RSA-2048 PEM certificate served from CIRCUITPY
over the softAP; an ESP32-C6 client completed the handshake and received
"HTTP/1.0 200 OK" in 2.0 s. Flash +1,868 B (PEM) and +4,716 B (builtin ECP
instead of p256-m) on the Pico 2 W; no static RAM change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant