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,