Skip to content

security(rest): put the schema fetch behind the pinned CA, on its own port - #175

Merged
cayossarian merged 1 commit into
mainfrom
fix/schema-fetch-over-pinned-tls
Aug 31, 2026
Merged

security(rest): put the schema fetch behind the pinned CA, on its own port#175
cayossarian merged 1 commit into
mainfrom
fix/schema-fetch-over-pinned-tls

Conversation

@cayossarian

Copy link
Copy Markdown
Member

What

Splits the schema fetch's port from the CA download's, so a consumer holding a pinned CA can put its schema reads on verified HTTPS without pointing the deliberately-plaintext CA download at a TLS port. This is the library half of SpanPanel/span#264, where a pinned integration's schema fetch ran plaintext at every startup and the transport warning said so.

How

  • SpanMqttClient takes panel_https_port; _fetch_schema — the connect-time read and every redispatch refetch — rides it under an ssl_context, while panel_http_port stays with the bridge's CA fetches. Naming the HTTPS port without an anchor raises SpanPanelValidationError, mirroring _build_url's refusal of port 80 with a context from the other direction.
  • create_span_client routes its one port into the slot matching its transport (it was already read as the HTTPS port under a context by every REST call the factory makes), and documents the diagnostic-CA-read corollary.
  • New SpanPanelTLSVerificationError(SpanPanelConnectionError): raised only when ssl.SSLCertVerificationError is in the transport error's cause chain, so ambiguous handshake failures never look terminal, and every existing except SpanPanelConnectionError keeps its meaning. The redispatch retry loop leaves it to raise — it cannot succeed on a later attempt under the same session anchor — and the redispatch wrapper logs it as a verification failure instead of a slow boot.
  • The CA download no longer emits the plaintext-transport warning and no longer spends the once-per-host warning slot: the fetch of the anchor is unverifiable by construction, carries no credential in either direction, and every caller states its own trust posture. The warning as it stood named credentials that call never carries — the exact log line #264 reported. Every other bootstrap call warns exactly as before.

Verification

  • 1213 passed / 7 skipped, ruff and mypy clean, coverage 96%.
  • Every behavioral change was written test-first (tests/test_schema_fetch_transport_split.py, reworked tests/test_plaintext_warning.py), including guards that the bridge keeps the plaintext port when the schema fetch moves, that the retry loop makes exactly one attempt on a verification failure, and that the CA download's exemption does not swallow a later legitimate warning.
  • Reviewed adversarially twice (initial review plus a verification pass over the rework); all findings resolved.

Version 3.4.0. The adapter distributions are untouched (no SchemaAdapter change), so schema-0 1.1.2 / schema-1 1.1.3 remain current. SpanPanel/span consumes this via its 3.4.0 pin; its CI cannot pass until this releases to PyPI.

… port

`panel_http_port` served two transports with opposite security properties:
the bridge's CA download, plaintext by design because it fetches the very
anchor everything else is checked against, and the client's schema fetch,
which should ride the pinned HTTPS transport whenever the caller holds one.
One parameter fed both, so a consumer that pinned a CA could not move its
schema fetch to HTTPS without pointing the CA download at a TLS port it
speaks plaintext to (SpanPanel/span#264).

`SpanMqttClient` takes `panel_https_port`, and both schema fetches — connect
and every redispatch refetch — go HTTPS on it whenever an `ssl_context` is
supplied; naming the port without an anchor is refused rather than left
silently plaintext. `create_span_client` routes its one `port` into the slot
matching its transport, and documents the diagnostic-CA-read corollary.

A verification failure gets a name: `SpanPanelTLSVerificationError`, a
subclass of `SpanPanelConnectionError` so every existing except clause keeps
its meaning, raised only when `ssl.SSLCertVerificationError` is in the cause
chain because ambiguous evidence must not look terminal. The redispatch
retry loop leaves it to raise — it cannot succeed on a later attempt under
the same anchor — and its wrapper logs it as what it is instead of blaming
a slow boot.

The CA download itself no longer emits the plaintext-transport warning and
no longer spends the once-per-host slot: the fetch of the anchor is
unverifiable by construction, carries no credential in either direction,
and every caller states its own trust posture — the warning as it stood
named credentials that call never carries, which is the log line the issue
reported. Every other bootstrap call warns exactly as before.
@cayossarian
cayossarian merged commit ca2c9ec into main Aug 31, 2026
6 checks passed
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