Skip to content

Build1 publisher3 min readPublished

A fifteen-line listener reads your destination out of TLS without decrypting a byte

A developer dumped the ClientHello that openssl sends and found the requested hostname in plain ASCII inside the server_name extension. A listener holding no private key read it back off the wire, TLS 1.3 included.

The Engineer · Build desk

Illustration accompanying A fifteen-line listener reads your destination out of TLS without decrypting a byte

What happened

  • A developer dumped the raw ClientHello openssl sends for the server name secret.wikipedia.example and found the hostname sitting in the server_name extension as literal ASCII, before any key exchange.
  • Walking the wrapping gave extension type zero, a 29-byte body, a host_name entry marker and a 24-byte name length, and his parser reported the extension at offset 104 and printed the name back.
  • One IP address now serves hundreds or thousands of sites behind a single TLS terminator, and the terminator has to be told which site is wanted before it can present the right certificate.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • exposure Anyone holding the path gets a complete list of the sites a user asks for, and encrypting the payload does not shorten that list.
  • capability An observer needs no key material and no cooperation from either endpoint, so destination logging is close to free to build for anyone already forwarding or terminating traffic.
  • constraint Moving to a newer TLS version does not help here. Any fix has to let a shared terminator choose a certificate without the client naming the site in readable text first.
  • decision Threat models that lean on HTTPS for destination privacy need a separate line for hostname exposure, because the padlock's guarantee stops at the contents of the connection.

The declared body is 29 bytes, and the name length that follows it is 24 [3]. The string "secret.wikipedia.example" is 24 characters long [1]. Subtract, and five bytes of framing sit between the extension header and the name: two for the list length, one marking the entry as a host_name, two for the length of the name [2].

Offset 104 is where his script found the extension in that one hello [4]. Do not hard-code it. The two first packets he read were 1566 bytes and 1489 bytes [7][8], 77 bytes apart [3], and the position of any extension depends on everything the client put in front of it. His parser walked the length prefixes instead of seeking to a fixed byte [3][4].

The zero in his output is structural, not a shortcut. When the ClientHello goes out the key exchange is still several messages away, so there is nothing to decrypt and no key with which to do it [5]. He tested that with a listener of about fifteen lines that accepts a connection, reads the first packet and pulls out the hostname, never completing the handshake and holding no private key [6]. A client asking it for login.mybank.example.com produced 1566 bytes read, a first byte of 0x16, and the hostname printed in full [7]. It never answers the client at all.

The design is deliberate. One IP address today serves hundreds or thousands of different sites behind a single TLS terminator, and the terminator has to be told which site you want before it can hand back the right certificate and set up encryption for it [10]. You cannot encrypt the request for a site with that site's key before you have asked for the site [11]. The post calls this the protocol working as intended, not a bug [15], and for twenty-odd years the answer has been to send the name unprotected [11].

He expected TLS 1.3 to have closed the gap [14]. TLS 1.3 does encrypt the server's certificate, so a watcher cannot see which cert came back [9]. The same listener, pointed at a 1.3-only client asking for tls13.secret.example, read 1489 bytes and printed the hostname with zero bytes decrypted [8].

"They are not reading your traffic. They are reading the label on the envelope, and you wrote it in block capitals," the post's author wrote of proxies, firewalls and ISPs [12]. He had assumed, without checking, that the padlock meant nobody on the network could tell which site he was visiting [16]. Filtering by domain works exactly this way without anyone breaking the encryption, and the connection contents stay encrypted throughout [13].

The captures are example hostnames on one developer's machine, and the post does not measure what any operator actually logs. It does establish the ordering, and the ordering does not move until a terminator can pick a certificate without the client naming the site in the clear [10][11].

What to watch

  • An openssl hello is what the post captured. A capture from a mainstream browser would show whether offset 104 holds anywhere outside this one hello or only the length-prefix walk transfers.
  • The same listener pointed at a client configured to protect the hostname; the post's captures cover default openssl and one TLS 1.3-only client.
  • Whether operators running a single certificate per endpoint can drop the SNI dependence entirely, since the plaintext name exists to serve certificate selection at shared terminators.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories