Add BambuGenericPrinter::ensure_trusted() — smooth auto-connect, SSH-style
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/<id>.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.
This commit is contained in:
@@ -7,3 +7,4 @@ Cargo.lock
|
|||||||
.env
|
.env
|
||||||
*.log
|
*.log
|
||||||
printers.toml
|
printers.toml
|
||||||
|
/certs/pinned/
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ path = "examples/test_bambu_certs.rs"
|
|||||||
name = "fetch_bambu_cert"
|
name = "fetch_bambu_cert"
|
||||||
path = "examples/fetch_bambu_cert.rs"
|
path = "examples/fetch_bambu_cert.rs"
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "auto_connect_bambu"
|
||||||
|
path = "examples/auto_connect_bambu.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { version = "1.40", features = ["full"] }
|
tokio = { version = "1.40", features = ["full"] }
|
||||||
tokio-tungstenite = { version = "0.24", features = ["native-tls"] }
|
tokio-tungstenite = { version = "0.24", features = ["native-tls"] }
|
||||||
|
|||||||
@@ -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/<id>.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}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -163,6 +163,49 @@ impl BambuGenericPrinter {
|
|||||||
Ok(der_to_pem(&cert.to_der()?))
|
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/<id>.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/<id>.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
|
/// Shared "connect" logic both V1 and V2 delegate to — the real
|
||||||
/// version would open the MQTTS session here. Still a stub, but no
|
/// version would open the MQTTS session here. Still a stub, but no
|
||||||
/// longer duplicated across two structs.
|
/// longer duplicated across two structs.
|
||||||
@@ -199,6 +242,10 @@ impl BambuV1Printer {
|
|||||||
pub fn fetch_certificate(&self) -> anyhow::Result<Vec<u8>> {
|
pub fn fetch_certificate(&self) -> anyhow::Result<Vec<u8>> {
|
||||||
self.generic.fetch_certificate()
|
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 {
|
impl GenericPrinter for BambuV1Printer {
|
||||||
@@ -234,6 +281,10 @@ impl BambuV2Printer {
|
|||||||
pub fn fetch_certificate(&self) -> anyhow::Result<Vec<u8>> {
|
pub fn fetch_certificate(&self) -> anyhow::Result<Vec<u8>> {
|
||||||
self.generic.fetch_certificate()
|
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 {
|
impl GenericPrinter for BambuV2Printer {
|
||||||
|
|||||||
Reference in New Issue
Block a user