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:
@@ -96,6 +96,12 @@ fn der_to_pem(der: &[u8]) -> Vec<u8> {
|
|||||||
pem.into_bytes()
|
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
|
/// Fields and behavior every Bambu printer shares, regardless of protocol
|
||||||
/// generation — the layer between `GenericPrinter` and the version-specific
|
/// generation — the layer between `GenericPrinter` and the version-specific
|
||||||
/// `BambuV1Printer`/`BambuV2Printer` leaves below. Those two *have* one of
|
/// `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
|
/// A real (blocking, one-shot) TLS handshake to port 8883 — no MQTT
|
||||||
/// protocol, just "does the certificate verify" against this printer's
|
/// protocol, just "does the certificate verify" against this printer's
|
||||||
/// configured trust mode.
|
/// 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<()> {
|
pub fn test_tls_handshake(&self) -> anyhow::Result<()> {
|
||||||
let addr = resolve_addr(&self.base.host)?;
|
let addr = resolve_addr(&self.base.host)?;
|
||||||
let stream = TcpStream::connect_timeout(&addr, Duration::from_secs(5))?;
|
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)?;
|
self.tls.build_connector()?.connect(&self.base.host, stream)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user