14  Sockets and TLS Records

Introduction

In this chapter, you’ll connect the byte-level work to socket-style reads. The socket is still fake, but it behaves enough like a real one that we can practise send(), recv(), record headers, and loop control without touching the network.

Once we know the host and port we want to connect to, the original proof-of-concept used the socket library: a low-level networking interface. We’re using a fixture instead, but the shape is deliberately similar.

Concepts Covered

  • Socket-shaped objects
  • TLS record headers
  • Unpacking bytes with struct.unpack()
  • Receiving a full payload with a loop
  • Using break or return when the handshake marker is found

Why This Matters

TCP is a byte stream, not a message stream. A single recv() call might give you all the bytes you wanted, or only some of them, so robust tools keep reading until they’ve got the expected length.

Objective

Send the Client Hello to a fixture socket, parse the returned TLS record, and stop when the fake server sends Server Hello Done.

Reading a TLS Record

A TLS record starts with a five-byte header: one byte for content type, two bytes for protocol version, and two bytes for payload length. The fixture socket has a recv() method like a real socket, but it only returns deterministic local bytes.

Using the newly created socket-shaped object, we send the Client Hello and then read the server’s response. The important habit is to read the header first, because the header tells us how much payload to expect next.

from fixtures.local_targets import (
    HeartbleedFixtureSocket,
    TLS_HANDSHAKE_CONTENT_TYPE,
    parse_tls_record_header,
    recvall,
    tls_client_hello,
)

sock = HeartbleedFixtureSocket()
bytes_sent = sock.send(tls_client_hello())

header = sock.recv(5)
content_type, version, length = parse_tls_record_header(header)
payload = recvall(sock, length)

print(f"Sent {bytes_sent} bytes")
print(f"Received type={content_type}, version=0x{version:04x}, length={length}")
print(f"Payload: {payload!r}")
1
Create the local socket-shaped fixture; this doesn’t open a network connection.
2
Send the Client Hello bytes from the previous chapter.
3
Read exactly the TLS record header size before parsing fields.
4
Parse content type, version, and payload length from the header.
5
Keep receiving until the promised payload length has arrived.
Sent 225 bytes
Received type=22, version=0x0302, length=4
Payload: b'\x0e\x00\x00\x00'

The payload starts with 0x0e, which is the old handshake message type for Server Hello Done. In a real TLS session there’d be more detail, but this is enough for the original Heartbleed proof-of-concept shape.

To help with understanding what’s going on, this is a good place to print or log the content type, version, and length. Protocol work becomes much friendlier when you can see the fields your program is parsing.

Waiting for Server Hello Done

The proof-of-concept needs to wait until the handshake reaches a safe marker before it sends the heartbeat request. This loop keeps reading TLS records until it sees a handshake record whose payload starts with 0x0e.

The original code used a while True loop and then broke out once the right record appeared. Here we return True instead, because it makes the notebook example easier to test while keeping the same idea: keep reading until the condition we care about is met.

def wait_for_server_hello_done(sock):
    while True:
        header = sock.recv(5)
        if not header:
            return False

        content_type, _version, length = parse_tls_record_header(header)
        payload = recvall(sock, length)
        if payload is None:
            return False

        if content_type == TLS_HANDSHAKE_CONTENT_TYPE and payload[:1] == b"":
            return True


fixture_socket = HeartbleedFixtureSocket()
fixture_socket.send(tls_client_hello())
print(wait_for_server_hello_done(fixture_socket))
1
Read the next TLS record header from the byte stream.
2
Use _version for a parsed field we don’t need in this function.
3
Read the payload length promised by the header.
4
Return when the record is a handshake record and the first payload byte is Server Hello Done.
True

That loop is doing the important work: read a header, read the number of bytes the header promised, then check whether the record is the one we were waiting for.

break, continue, return, and pass are all flow-control tools. You don’t need all of them here, but this is exactly the kind of loop where choosing the right exit point makes the code easier to follow.

Checkpoint

Describe why recvall(sock, length) is safer than assuming one sock.recv(length) call will always return the full TLS payload.

Use the phrase “byte stream” in your answer. That phrase is doing a lot of useful work.

Extension Activity

Modify the loop on paper so it records how many TLS records it inspected before finding Server Hello Done. Explain where you would initialise and update the counter.

References