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. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MWQVtBGxHYLkVw2kBupd5g
This commit is contained in:
+59
-6
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user