From af2f20a45bbd184053911d4bea3144004108096a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iwo=20Strzebo=C5=84ski?= Date: Fri, 28 Aug 2026 21:18:46 +0000 Subject: [PATCH] =?UTF-8?q?Add=20BambuGenericPrinter::ensure=5Ftrusted()?= =?UTF-8?q?=20=E2=80=94=20smooth=20auto-connect,=20SSH-style?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The manual fetch_bambu_cert workflow works but needs two commands and a printers.toml edit. ensure_trusted() collapses that into one call, using the same trust model SSH uses for host keys: - a pin already on disk (certs/pinned/.pem) is used directly, no new trust decision is made on every run - no pin yet + bundled CA verifies fine (current-gen printers): nothing to do - no pin yet + bundled CA fails (P1P, etc.): auto-pins via trust-on-first-connect, same as fetch_bambu_cert, but automatic - an *explicit* pin (you set ca_cert_path yourself) never gets silently auto-pinned over — a failure there is a real error examples/auto_connect_bambu.rs demonstrates the one-command flow. Verified all three states against local test servers, not just compiled: first run with no pin auto-pins and connects; second run against the same server is silent (no re-TOFU message) and just verifies against the pin; third run after swapping the server's certificate correctly FAILS instead of silently re-pinning — confirms the security property survives the smoother UX. --- .gitignore | 1 + Cargo.toml | 4 +++ examples/auto_connect_bambu.rs | 34 +++++++++++++++++++++++ src/printer/bambu.rs | 51 ++++++++++++++++++++++++++++++++++ 4 files changed, 90 insertions(+) create mode 100644 examples/auto_connect_bambu.rs diff --git a/.gitignore b/.gitignore index 800ca46..08bd6ac 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ Cargo.lock .env *.log printers.toml +/certs/pinned/ diff --git a/Cargo.toml b/Cargo.toml index 36e8392..2fb7276 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,10 @@ path = "examples/test_bambu_certs.rs" name = "fetch_bambu_cert" path = "examples/fetch_bambu_cert.rs" +[[example]] +name = "auto_connect_bambu" +path = "examples/auto_connect_bambu.rs" + [dependencies] tokio = { version = "1.40", features = ["full"] } tokio-tungstenite = { version = "0.24", features = ["native-tls"] } diff --git a/examples/auto_connect_bambu.rs b/examples/auto_connect_bambu.rs new file mode 100644 index 0000000..4a5c57f --- /dev/null +++ b/examples/auto_connect_bambu.rs @@ -0,0 +1,34 @@ +//! Run with: cargo run --example auto_connect_bambu +//! +//! The "smooth, no manual steps" version of test_bambu_certs + +//! fetch_bambu_cert combined: for each Bambu printer in printers.toml, +//! makes sure it has a working certificate, auto-pinning one via +//! trust-on-first-connect if the bundled CA doesn't verify. Safe to run +//! repeatedly — a printer that's already pinned (certs/pinned/.pem) +//! just gets re-verified against its pin, no network trust decision is +//! made again. + +use continuum_proxy::fleet; +use continuum_proxy::printer::PrinterHandle; + +fn main() -> anyhow::Result<()> { + let mut printers = fleet::load(std::path::Path::new("printers.toml"))?; + let cert_dir = std::path::Path::new("certs/pinned"); + + for printer in &mut printers { + match printer { + PrinterHandle::BambuV1(bambu) => report(&bambu.ensure_trusted(cert_dir)), + PrinterHandle::BambuV2(bambu) => report(&bambu.ensure_trusted(cert_dir)), + _ => {} + } + } + + Ok(()) +} + +fn report(result: &anyhow::Result<()>) { + match result { + Ok(()) => println!("connected"), + Err(err) => println!("failed: {err}"), + } +} diff --git a/src/printer/bambu.rs b/src/printer/bambu.rs index 3d405a8..47f8907 100644 --- a/src/printer/bambu.rs +++ b/src/printer/bambu.rs @@ -163,6 +163,49 @@ impl BambuGenericPrinter { Ok(der_to_pem(&cert.to_der()?)) } + /// Makes sure this printer has a *working* trust configuration, + /// auto-pinning a certificate via trust-on-first-connect if needed — + /// the "smooth, no manual steps" version of the fetch_bambu_cert + /// workflow, built the way SSH handles host keys: + /// + /// - A pinned certificate already on disk (`cert_dir/.pem`) is used + /// directly. No new trust decision gets made on every run — that + /// already happened once, this just re-verifies against it. + /// - No pin yet, and the bundled CA verifies fine (current-generation + /// printers): nothing to do. + /// - No pin yet, and the bundled CA does *not* verify (P1P, etc.): + /// fetches the certificate the printer presents, confirms it + /// produces a working connection, and saves it to `cert_dir/.pem` + /// for every run after this one. This is the one moment a MITM + /// active on your network *right now* could plant a certificate that + /// gets trusted from then on — the same tradeoff SSH accepts on a + /// first connection. + /// - A printer with an *explicit* pin (you set `ca_cert_path` yourself + /// in printers.toml) never auto-pins over it — a verification + /// failure there is a real error, not something to paper over, + /// since that's exactly the signal pinning exists to give you (the + /// cert rotated, or something worse). + pub fn ensure_trusted(&mut self, cert_dir: &Path) -> anyhow::Result<()> { + let pinned_path = cert_dir.join(format!("{}.pem", self.base.id)); + + if matches!(self.tls, BambuTls::BundledCa) && pinned_path.exists() { + self.tls = BambuTls::Custom(std::fs::read(&pinned_path)?); + } + + match self.test_tls_handshake() { + Ok(()) => Ok(()), + Err(err) if matches!(self.tls, BambuTls::BundledCa) => { + eprintln!("{}: bundled CA didn't verify ({err}); auto-pinning on first connect", self.base.name); + let pem = self.fetch_certificate()?; + std::fs::create_dir_all(cert_dir)?; + std::fs::write(&pinned_path, &pem)?; + self.tls = BambuTls::Custom(pem); + self.test_tls_handshake() + } + Err(err) => Err(err), + } + } + /// Shared "connect" logic both V1 and V2 delegate to — the real /// version would open the MQTTS session here. Still a stub, but no /// longer duplicated across two structs. @@ -199,6 +242,10 @@ impl BambuV1Printer { pub fn fetch_certificate(&self) -> anyhow::Result> { self.generic.fetch_certificate() } + + pub fn ensure_trusted(&mut self, cert_dir: &Path) -> anyhow::Result<()> { + self.generic.ensure_trusted(cert_dir) + } } impl GenericPrinter for BambuV1Printer { @@ -234,6 +281,10 @@ impl BambuV2Printer { pub fn fetch_certificate(&self) -> anyhow::Result> { self.generic.fetch_certificate() } + + pub fn ensure_trusted(&mut self, cert_dir: &Path) -> anyhow::Result<()> { + self.generic.ensure_trusted(cert_dir) + } } impl GenericPrinter for BambuV2Printer {