diff --git a/src/printer/bambu.rs b/src/printer/bambu.rs index 841f823..2196c2a 100644 --- a/src/printer/bambu.rs +++ b/src/printer/bambu.rs @@ -96,6 +96,12 @@ fn der_to_pem(der: &[u8]) -> Vec { pem.into_bytes() } +/// Re-encodes a PEM certificate to DER, so it can be byte-compared against +/// what a live handshake presents (`Certificate::to_der()`). +fn pem_to_der(pem: &[u8]) -> anyhow::Result> { + Ok(native_tls::Certificate::from_pem(pem)?.to_der()?) +} + /// Fields and behavior every Bambu printer shares, regardless of protocol /// generation — the layer between `GenericPrinter` and the version-specific /// `BambuV1Printer`/`BambuV2Printer` leaves below. Those two *have* one of @@ -134,9 +140,44 @@ impl BambuGenericPrinter { /// A real (blocking, one-shot) TLS handshake to port 8883 — no MQTT /// protocol, just "does the certificate verify" against this printer's /// configured trust mode. + /// + /// `BundledCa` verifies normally (Bambu's shared CA is a proper, + /// well-formed root — OpenSSL's usual chain validation handles it + /// fine). `Custom` does something different: *fingerprint pinning*, + /// not chain validation. A device's own leaf certificate often isn't a + /// well-formed CA (no `CA:TRUE`), and the device may present it + /// alongside a separate self-signed root you never captured — asking + /// OpenSSL's normal path-building to accept an arbitrary leaf as a + /// trust anchor is unreliable for exactly that reason (this is a real + /// bug that shipped and failed against a real P1P: `add_root_certificate` + /// with a captured leaf produced `self-signed certificate in + /// certificate chain` / `unable to get local issuer certificate` on + /// the very next connection). So instead: connect with verification + /// off, then compare the certificate actually presented against the + /// exact bytes pinned, byte for byte. A mismatch is a hard failure — + /// no PKI judgment call, just "is this the same certificate as before." pub fn test_tls_handshake(&self) -> anyhow::Result<()> { let addr = resolve_addr(&self.base.host)?; let stream = TcpStream::connect_timeout(&addr, Duration::from_secs(5))?; + + if let BambuTls::Custom(pinned_pem) = &self.tls { + let tls_stream = BambuTls::Insecure.build_connector()?.connect(&self.base.host, stream)?; + let presented = tls_stream + .peer_certificate()? + .ok_or_else(|| anyhow::anyhow!("{} presented no certificate", self.base.name))? + .to_der()?; + + if presented != pem_to_der(pinned_pem)? { + anyhow::bail!( + "{} presented a certificate different from the one pinned — could be a legitimate \ + certificate rotation, could be something worse; re-run fetch_bambu_cert deliberately \ + if you're sure it's the former", + self.base.name + ); + } + return Ok(()); + } + self.tls.build_connector()?.connect(&self.base.host, stream)?; Ok(()) }