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.
|
/// the executable.
|
||||||
const BUNDLED_CA: &[u8] = include_bytes!("../../certs/bambu_ca2.pem");
|
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
|
/// 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
|
/// port (8883 - Bambu's LAN mode is TLS-only, there's no unencrypted
|
||||||
/// fallback).
|
/// fallback).
|
||||||
@@ -86,14 +91,41 @@ impl BambuTls {
|
|||||||
// to be off regardless of trust mode: what matters is whether the
|
// to be off regardless of trust mode: what matters is whether the
|
||||||
// certificate chains to a CA we trust, not whether its SAN matches
|
// certificate chains to a CA we trust, not whether its SAN matches
|
||||||
// the specific IP we happened to dial today. Chain verification
|
// the specific IP we happened to dial today. Chain verification
|
||||||
// itself stays fully enforced below for BundledCa/Custom - this
|
// itself stays fully enforced below for BundledCa - this narrows
|
||||||
// narrows *what* gets checked, it doesn't turn checking off.
|
// *what* gets checked, it doesn't turn checking off.
|
||||||
builder.danger_accept_invalid_hostnames(true);
|
builder.danger_accept_invalid_hostnames(true);
|
||||||
|
|
||||||
if matches!(self, BambuTls::Insecure) {
|
match self {
|
||||||
builder.danger_accept_invalid_certs(true);
|
BambuTls::BundledCa => {
|
||||||
} else {
|
// A real CA root - chain validation is exactly the right
|
||||||
builder.add_root_certificate(native_tls::Certificate::from_pem(self.ca_bytes())?);
|
// 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()?)
|
Ok(builder.build()?)
|
||||||
@@ -381,7 +413,28 @@ impl GenericPrinter for BambuPrinter {
|
|||||||
/// `preference`'s `Auto`-tries-v5-then-v3.1.1 fallback) and keeps it -
|
/// `preference`'s `Auto`-tries-v5-then-v3.1.1 fallback) and keeps it -
|
||||||
/// same reasoning as `PrusaSerialPrinter::connect` holding its port
|
/// same reasoning as `PrusaSerialPrinter::connect` holding its port
|
||||||
/// open, just an MQTT session instead of a serial 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> {
|
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(
|
let (session, version) = super::bambu_mqtt::BambuMqttSession::open(
|
||||||
&self.base.host,
|
&self.base.host,
|
||||||
&self.access_code,
|
&self.access_code,
|
||||||
|
|||||||
Reference in New Issue
Block a user