From ce6a4c3806bb9582db9a36c5237b0e635c6995bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iwo=20Strzebo=C5=84ski?= Date: Sat, 5 Sep 2026 21:45:32 +0000 Subject: [PATCH] Auto-pin certificates on connect; fix Custom TLS mode for real MQTT connect() now calls ensure_trusted() before opening the MQTT session, so a newly-added printer with no ca_cert_path configured just works: bundled CA if it verifies, trust-on-first-connect auto-pin if it doesn't (P1P, at least) - the same thing examples/auto_connect_bambu.rs already did by hand, now automatic on every real connect. Wiring that in immediately surfaced a real, second instance of a bug the codebase already knew about: build_connector()'s BambuTls::Custom branch used add_root_certificate on the pinned certificate - but ensure_trusted pins a *leaf* cert (CN = the printer's serial number, issued by Bambu's own CA, not a root), for byte-exact fingerprint comparison, not chain validation. Handing that leaf to add_root_certificate asks OpenSSL to treat it as its own trust anchor - exactly the "self-signed certificate in certificate chain" failure test_tls_handshake's doc comment already described, just not fixed at this second call site (the one the real MQTT connection actually uses) until now. native_tls has no per- certificate verification callback hook (unlike rustls), so Custom now accepts any certificate at the TLS layer instead, relying on ensure_trusted having already fingerprint-verified this exact printer moments before - weaker than continuous pinning on every reconnect, but not a fresh regression: pinning a leaf as a root here never worked. Verified live: deleted P1P's existing pin entirely, ran the daemon, and watched it auto-fetch, pin, and connect on the very first attempt - the freshly-pinned certificate came back byte-identical to the one that was there before. Both configured printers (P1P forced v1, H2C auto-detected v2) now connect successfully from a cold start with zero manual steps. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01MWQVtBGxHYLkVw2kBupd5g --- src/printer/bambu.rs | 65 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/src/printer/bambu.rs b/src/printer/bambu.rs index 9e186cf..950fca7 100644 --- a/src/printer/bambu.rs +++ b/src/printer/bambu.rs @@ -30,6 +30,11 @@ fn get_schema_for_command(version: BambuProtocolVersion, root: String, command: /// the executable. const BUNDLED_CA: &[u8] = include_bytes!("../../certs/bambu_ca2.pem"); +/// Where auto-pinned certificates get saved - same directory +/// `examples/auto_connect_bambu.rs` already used by hand. Relative to +/// wherever the daemon is run from, same convention as `printers.toml`. +const CERT_DIR: &str = "certs/pinned"; + /// Which certificate to trust when connecting to a Bambu printer's MQTTS /// port (8883 - Bambu's LAN mode is TLS-only, there's no unencrypted /// fallback). @@ -86,14 +91,41 @@ impl BambuTls { // to be off regardless of trust mode: what matters is whether the // certificate chains to a CA we trust, not whether its SAN matches // the specific IP we happened to dial today. Chain verification - // itself stays fully enforced below for BundledCa/Custom - this - // narrows *what* gets checked, it doesn't turn checking off. + // itself stays fully enforced below for BundledCa - this narrows + // *what* gets checked, it doesn't turn checking off. builder.danger_accept_invalid_hostnames(true); - if matches!(self, BambuTls::Insecure) { - builder.danger_accept_invalid_certs(true); - } else { - builder.add_root_certificate(native_tls::Certificate::from_pem(self.ca_bytes())?); + match self { + BambuTls::BundledCa => { + // A real CA root - chain validation is exactly the right + // check here. + builder.add_root_certificate(native_tls::Certificate::from_pem(self.ca_bytes())?); + } + BambuTls::Custom(_) => { + // `ensure_trusted` pins a *leaf* certificate (the printer's + // own, CN = its serial number, issued by Bambu's own CA - + // not a root), for byte-exact fingerprint comparison, not + // chain validation. Handing that same leaf to + // `add_root_certificate` here would ask OpenSSL to treat it + // as its own trust anchor - exactly the bug + // `test_tls_handshake`'s doc comment already describes + // ("self-signed certificate in certificate chain"), just + // not fixed at this second call site too until now. + // native_tls has no hook for a custom per-certificate + // verification callback (unlike rustls, which would let us + // do the real fingerprint check here) - so this accepts + // any certificate at the TLS layer instead, relying on + // `ensure_trusted` already having fingerprint-verified this + // exact printer moments before `connect()` opens this + // transport. Weaker than continuous fingerprint pinning on + // every reconnect, but not a fresh regression: pinning a + // leaf as a root here never actually worked in the first + // place. + builder.danger_accept_invalid_certs(true); + } + BambuTls::Insecure => { + builder.danger_accept_invalid_certs(true); + } } Ok(builder.build()?) @@ -381,7 +413,28 @@ impl GenericPrinter for BambuPrinter { /// `preference`'s `Auto`-tries-v5-then-v3.1.1 fallback) and keeps it - /// same reasoning as `PrusaSerialPrinter::connect` holding its port /// open, just an MQTT session instead of a serial port. + /// + /// `ensure_trusted` runs first so a newly-added printer with no + /// `ca_cert_path` configured just works on the first real connect + /// attempt: bundled CA if it verifies, auto-pin via trust-on-first- + /// connect if it doesn't (P1P, at least) - the same thing + /// `examples/auto_connect_bambu.rs` already did by hand, now + /// automatic. A printer with an explicit `ca_cert_path` (already + /// resolved to `Custom` in `fleet.rs`) is untouched by this - see + /// `ensure_trusted`'s own doc comment for why it never auto-pins over + /// an operator's explicit choice. + /// + /// `ensure_trusted` is a blocking call (a raw, synchronous TCP+TLS + /// handshake) run here inside an `async fn` - tolerable for a couple + /// of printers connecting concurrently on tokio's multi-thread + /// runtime, but at real farm scale this should move to + /// `tokio::task::spawn_blocking` so a slow/unreachable printer's + /// trust check can't tie up a worker thread other printers' connects + /// need. async fn connect(&mut self) -> Result<(), PrinterError> { + self.ensure_trusted(Path::new(CERT_DIR)) + .map_err(|err| PrinterError(format!("{} certificate trust failed: {err}", self.base.name)))?; + let (session, version) = super::bambu_mqtt::BambuMqttSession::open( &self.base.host, &self.access_code,