Auto-pin certificates on connect; fix Custom TLS mode for real MQTT

connect() now calls ensure_trusted() before opening the MQTT session, so
a newly-added printer with no ca_cert_path configured just works: bundled
CA if it verifies, trust-on-first-connect auto-pin if it doesn't (P1P,
at least) - the same thing examples/auto_connect_bambu.rs already did by
hand, now automatic on every real connect.

Wiring that in immediately surfaced a real, second instance of a bug the
codebase already knew about: build_connector()'s BambuTls::Custom branch
used add_root_certificate on the pinned certificate - but ensure_trusted
pins a *leaf* cert (CN = the printer's serial number, issued by Bambu's
own CA, not a root), for byte-exact fingerprint comparison, not chain
validation. Handing that leaf to add_root_certificate asks OpenSSL to
treat it as its own trust anchor - exactly the "self-signed certificate
in certificate chain" failure test_tls_handshake's doc comment already
described, just not fixed at this second call site (the one the real
MQTT connection actually uses) until now. native_tls has no per-
certificate verification callback hook (unlike rustls), so Custom now
accepts any certificate at the TLS layer instead, relying on
ensure_trusted having already fingerprint-verified this exact printer
moments before - weaker than continuous pinning on every reconnect, but
not a fresh regression: pinning a leaf as a root here never worked.

Verified live: deleted P1P's existing pin entirely, ran the daemon, and
watched it auto-fetch, pin, and connect on the very first attempt - the
freshly-pinned certificate came back byte-identical to the one that was
there before. Both configured printers (P1P forced v1, H2C auto-detected
v2) now connect successfully from a cold start with zero manual steps.
This commit is contained in:
2026-09-05 21:45:32 +00:00
parent 4add7c1248
commit 835a4ae778
+59 -6
View File
@@ -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,