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 {