Skip to content

Build1 publisher3 min readPublished

A green test suite that proved nothing: aiortc's mangled cert slot versus libp2p certhash pinning

aiortc 1.5 removed certificates= from RTCConfiguration. The obvious repair, assigning _certificates after construction, is a silent no-op that a loopback echo test cannot see.

The Engineer · Build desk

Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

Photograph accompanying A green test suite that proved nothing: aiortc's mangled cert slot versus libp2p certhash pinning
Photo: hashnode.dev

What happened

  • The account is a dev.to post submitted to DEV's Summer Bug Smash: Smash Stories, powered by Sentry, in which the author describes a py-libp2p certificate-pinning fix whose tests were green while every real connection would have failed.
  • WebRTC-Direct in libp2p connects without a certificate authority: the peer's multiaddr contains a hash of its TLS certificate as /webrtc-direct/certhash/<...>, and when you dial, the DTLS handshake presents a cert, you hash it and check it against the certhash in the address. No CA, no trust store; the address is the pin.
  • For certhash pinning to work, py-libp2p must make aiortc use the libp2p-generated certificate rather than the one aiortc auto-generates; the code did this with config = RTCConfiguration(certificates=[rtc_cert]) passed to RTCPeerConnection.
  • aiortc 1.5 and later dropped certificates= from RTCConfiguration; passing it raises TypeError.
  • Assigning pc._certificates = [rtc_cert] after constructing RTCPeerConnection is a silent no-op, because aiortc never reads _certificates.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

A py-libp2p contributor, writing on dev.to as part of DEV's Summer Bug Smash, describes shipping a certificate-pinning fix that passed its test suite while guaranteeing that every real dial would fail [s1c13]. The interesting part is not the one-line bug. It is that the test suite was checking a property nobody cared about, and the property everyone cared about had no test at all.

The setup: libp2p's WebRTC-Direct transport avoids certificate authorities by putting a hash of the peer's TLS certificate directly into the multiaddr, as /webrtc-direct/certhash/<...>. The dialer completes the DTLS handshake, hashes the presented certificate, and compares it to the address. No CA, no trust store; the address is the pin [1]. For that to hold, py-libp2p has to force aiortc to use the libp2p-generated certificate instead of the one aiortc creates for itself, which the code did by passing RTCConfiguration(certificates=[rtc_cert]) [2].

aiortc 1.5 and later removed certificates= from RTCConfiguration, and passing it now raises TypeError [3]. That failure is loud, which makes it the easy half. The tempting repair is to attach the certificate to the object after construction with pc._certificates = [rtc_cert]. According to the write-up, that assignment is a silent no-op: aiortc never reads _certificates [4]. Inside the class the certificate is stored and read as self.__certificates, and Python's name mangling compiles that to self._RTCPeerConnection__certificates [5]. aiortc reads the mangled slot at createOffer and createAnswer time, when it writes the SDP a=fingerprint line, at aiortc/rtcpeerconnection.py:295 and :1129 [6].

So the multiaddr advertised the libp2p certificate's hash, the DTLS handshake presented aiortc's auto-generated certificate, and every real dial would have failed peer verification with "Remote DTLS fingerprint does not match certhash" [7]. There is no AttributeError, no TypeError, no warning: you invent a junk attribute, and everything downstream keeps working except the security property [9]. Name mangling is doing exactly its job here, which is stopping subclasses from clobbering a base class's private attributes [12].

The loopback echo test, which opens a data channel, sends a payload, and gets it back, passed throughout, because loopback never validates a DTLS fingerprint against a multiaddr [8]. That is the whole lesson. The pinning invariant is only observable where aiortc serialises the fingerprint into SDP, so a test that watches bytes move through a channel cannot reach it, and only a test that inspects the offer or answer can [1].

The correct line is pc._RTCPeerConnection__certificates = [rtc_cert], set after construction and before any SDP operation triggers the handshake; the author says plainly that reaching into private state like this is the sort of coupling that breaks on a library upgrade, and annotated it so the next reader does not revert to pc._certificates [10]. The durable part of the fix is the added trio test, test_cert_pinning_lands_in_sdp_fingerprint, which asserts that the pinned certificate's fingerprint actually appears in the SDP [11].

Worth watching: whether aiortc offers a supported way to supply a certificate now that the constructor argument is gone [3], because until it does, py-libp2p's WebRTC-Direct pinning depends on a mangled private attribute surviving the next release [10]. And in your own repos, the general shape: any invariant enforced by a remote peer, not by your own process, is invisible to loopback tests [8].

Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories