fix(tls): trust OS root certificates for wss connections - #7029
Open
nahyeongjin1 wants to merge 1 commit into
Open
fix(tls): trust OS root certificates for wss connections#7029nahyeongjin1 wants to merge 1 commit into
nahyeongjin1 wants to merge 1 commit into
Conversation
Every wss client in the workspace builds its rustls trust store from
webpki-roots alone, so it ignores the operating system trust store. On a
machine behind a TLS-inspecting proxy — where the OS store holds the
proxy's CA but the bundled Mozilla roots do not — connecting fails with:
WebSocket connection failed: IO error: invalid peer certificate: UnknownIssuer
Desktop pairing (desktop/src-tauri/src/commands/pairing.rs) surfaces this
straight to the user: the pairing QR code never renders, so the phone
cannot be paired at all.
Enable rustls-tls-native-roots alongside the existing
rustls-tls-webpki-roots rather than replacing it. tokio-tungstenite merges
both sources into one root store and only treats an empty native store as
fatal when webpki roots are absent, so hosts without a system trust store
(minimal containers) keep behaving exactly as they do today.
rustls-native-certs additionally honors SSL_CERT_FILE and SSL_CERT_DIR,
which gives operators a supported way to supply a custom CA.
No call sites change: the remaining crates inherit the dependency through
`workspace = true`. rustls-native-certs was already present in both
lockfiles, so this adds a dependency edge rather than a new package.
Trade-off: this widens the client's trust scope to whatever the host
already trusts — the same posture browsers and curl take on these
machines. That is the intent, and it is why the change is opt-out by
uninstalling the CA rather than opt-in per connection.
Scope: this covers Buzz's own wss paths (tokio-tungstenite 0.29). The mesh
compute path reaches the network through nostr-sdk -> async-wsocket, which
resolves a separate tokio-tungstenite 0.28 and is untouched here.
Fixes block#5197
Fixes block#2940
Signed-off-by: 나형진 <hyeongjin.na@musinsa.com>
Co-authored-by: Claude <noreply@anthropic.com>
🔐 Codex Security Review
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Every wss client in the workspace builds its rustls trust store from
webpki-rootsalone, so it ignores the operating system trust store. On amachine where the OS trusts a CA that the bundled Mozilla root set does not —
the common case behind a TLS-inspecting corporate proxy, or with a self-hosted
relay using a private CA — every
wss://connection fails at the handshake:Desktop pairing surfaces this straight to the user:
start_pairing(
desktop/src-tauri/src/commands/pairing.rs) never gets a socket, so the QRcode never renders and the phone cannot be paired at all.
This enables
rustls-tls-native-rootsalongside the existingrustls-tls-webpki-rootsrather than replacing it:tokio-tungstenitemerges both sources into a single root store and onlytreats an empty native store as fatal when webpki roots are absent
(
src/tls.rs), so hosts without a system trust store — minimal containers —behave exactly as they do today.
rustls-native-certsalso readsSSL_CERT_FILEandSSL_CERT_DIR, which gives operators a supported way to supply a custom CA.Note these replace the platform store rather than extend it — when
SSL_CERT_FILEis set, the native certificates are read from that fileinstead of the platform trust store. The webpki roots stay in the store
either way, so public endpoints keep verifying.
No call sites change. The two manifests that pin the feature set are the only
edits; the remaining crates inherit through
workspace = true.rustls-native-certswas already resolved in both lockfiles, so this adds adependency edge rather than a new package.
Trade-off, stated deliberately: this widens the client's trust scope to
whatever the host already trusts. On a machine with an inspection CA
installed, the app will now accept the intercepted connection — the same
posture browsers and
curlalready take there. That is the intent; theopt-out is removing the CA from the host, not per-connection configuration.
Concretely, what that exposes:
KIND_STREAM_MESSAGE(kind:9) events carryplaintext content, so transport confidentiality is TLS alone. Before this
change an inspecting proxy caused the socket to fail closed and no data
flowed; after it, the proxy terminates TLS and can read channel content. What
it does not expose is the pairing payload — including the raw
nsec— whichis NIP-44 v2 encrypted under an ECDH secret and bound to a human-verified SAS
and transcript hash (
crates/buzz-core/src/pairing/), independent of TLS. Thencryptsec1egress guard on the native WebSocket send loop is likewiseunaffected.
One new input worth naming:
SSL_CERT_FILEandSSL_CERT_DIRnow influenceWebSocket TLS trust, where previously they had no effect on this path. Because
those variables replace the platform store rather than add to it, anything able
to set the process environment can substitute a single CA of its choosing for
the host's native trust — not merely append one. The prerequisite (control of
the process environment) is unchanged, so this is not a new privilege boundary,
but it is a new lever behind it. Most relevant for
buzz-acp, which isconfigured through environment variables.
Scope: this covers Buzz's own wss paths (
tokio-tungstenite0.29) — thedesktop native WebSocket and pairing clients,
buzz-ws-client,buzz-acp,and
buzz-pairing-cli.buzz-relayinherits the dependency but only reachesconnect_asyncfrom#[cfg(test)]code against plaintextws://loopbackservers, so no server-side outbound TLS path changes. The mesh compute path
reaches the network through
nostr-sdk→async-wsocket, which resolves aseparate
tokio-tungstenite0.28 and is untouched here.Related issue
Fixes #5197 — same mechanism, same conclusion:
tokio-tungsteniteis pinned torustls-tls-webpki-roots, andconnect_asyncis called with no customConnector, so there is no way to supply an internal root.Fixes #2940 — an earlier report of the same defect, including the same
asymmetry where WebView HTTP succeeds while the native WebSocket does not.
Not addressed here: #3281 concerns proxy settings on the same call sites, which
this change does not affect.
Testing
Reproduced and verified against a live TLS-inspecting proxy, using the same
crate versions, feature flags, and bare
connect_asynccall shape as therepository.
webpki-rootsonly (before)UnknownIssuer— matches the reported failure verbatimSSL_CERT_FILEpointed at an empty fileSSL_CERT_FILEset to the proxy CAThe third row is the one that matters for review: with an empty native store
the connection does not abort with
no native root CA certificates found, itdegrades to the webpki set, which is exactly today's behaviour.
Beyond the synthetic client, the fix was verified against a real product
workflow on the same machine.
buzz agents draft-createopens a WebSocket todeliver an owner-review draft, and on this network it fails outright:
Same host, same network, same command, same minute — the binary is the only
variable. A user-facing feature that was broken here works with this change.
Repository gates run locally, all passing:
cargo fmt --all -- --checkcargo check --workspace --all-targetscargo clippy --workspace --all-targets -- -D warningsjust desktop-tauri-checkjust desktop-tauri-testjust test-unit(all 9 lanes)The JS and Flutter lanes of
just ciwere not run locally — they areunaffected by a Cargo feature flag, and I could not fetch their dependencies
from the network I reproduced this on. Leaving those to CI.
No UI change; the fix restores an existing screen rather than altering it.