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:
2026-08-28 21:18:46 +00:00
parent 88603b58c3
commit af2f20a45b
4 changed files with 90 additions and 0 deletions
+51
View File
@@ -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/<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
/// 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<Vec<u8>> {
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<Vec<u8>> {
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 {