Fix real bug: pinning a device leaf cert via add_root_certificate is unreliable

This broke against a real P1P: after auto-pinning, the immediate retry
failed with 'self-signed certificate in certificate chain' / 'unable to
get local issuer certificate' - the exact error OpenSSL gives when a
trust-anchor certificate isn't a well-formed CA, or when a device presents
a chain (leaf + its own separate self-signed root) that doesn't terminate
at whatever got pinned. My earlier test only covered a leaf that happened
to have CA:TRUE (openssl req -x509's default), which masked this.

Fixed by switching Custom(pem) from chain validation to true fingerprint
pinning: connect with verification off, then byte-compare the certificate
actually presented (via to_der()) against the pinned bytes, rather than
asking OpenSSL's PKI path-builder to accept an arbitrary leaf as a root.
BundledCa/Insecure are untouched - this only affects the Custom path.

Reproduced the exact failure locally (a leaf signed by a separate
self-signed root, served as a 2-cert chain - a properly non-CA leaf, not
my earlier accidentally-CA:TRUE test cert) before fixing it, then verified
against that same repro: first-run auto-pin now succeeds, second run is
silent, and - regression check - presenting a genuinely different
certificate after pinning still correctly fails, with a clear message
instead of an opaque OpenSSL error.
This commit is contained in:
2026-08-28 21:33:05 +00:00
parent a77e93940f
commit aa471ff906
+41
View File
@@ -96,6 +96,12 @@ fn der_to_pem(der: &[u8]) -> Vec<u8> {
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<Vec<u8>> {
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(())
}