Skip to content
academia.sh

Lesson 09 / 16

The TLS Handshake

The handshake's round-trip budget, the round session resumption earns, and the 160 decisions the intermediary loses once the transport envelope closes.

Contents

Chain verification asked for no round trip, but it was not done in a vacuum. Before the certificate reached the client, an exchange had to begin, the server had to present its chain, and the two sides had to arrive at a shared secret. TLS (Transport Layer Security) handshake does these three jobs inside a single setup.

This lesson’s question has two parts. The first is budget: how many round trips setup holds, who pays it, and how repeated payment is prevented. The second is the course’s real subject: once setup completes, the message enters a transport envelope and the intermediary can no longer read what it says. What this does to the decision table counted through the course is measured here.

What the Handshake Carries

The handshake gathers everything that needs agreement into a small number of messages.

full setup
  client -> server : hello, supported suites, client's key share
  server -> client: hello, selected suite, server's key share,
                    certificate chain, finished
  client -> server : finished, then application data

session resumption
  client -> server : hello, previous session's resumption identity
  server -> client: hello, finished
  client -> server : finished, then application data

This listing is not run; it shows what the steps carry. Four jobs sit side by side: choosing the suite, exchanging key shares, presenting the certificate chain, and the two sides confirming setup is done.

The four jobs are not independent: key shares cannot be interpreted before the suite is chosen, and finished cannot be computed before key shares are exchanged. This dependency sets the order of the steps; rounds can be merged but not swapped.

The order is a design decision. The certificate chain arrives with the server’s first response; the client makes no separate request to verify it. The previous lesson’s measurement is the counterpart: chain verification ran with zero round trips, because everything it needed was already in hand. Resumption steps carry no certificate chain — the client has already verified that chain once and stored the result with the session.

Choosing the Suite and Finishing

The first of the four jobs is a negotiation: the client declares the cipher suites it supports, the server picks one. The suite decides, after setup, which symmetric transform and digest function will be used; the division of labor from the first lesson becomes concrete here.

The negotiation has a problem repeated in every lesson of this course: the first two messages travel over a channel not yet protected, so a layer on the path can read the client’s list. The specification’s answer is the finished message at the last step of setup: it is computed over the digest of all handshake messages sent and received up to that point, using the key that comes out of setup. Both sides check the other’s finished; if the handshake listing one side saw differs from the other’s, finished does not hold and setup does not complete.

This is encountered for the first time in the course. Every decision of the intermediary rested on a claim, and nothing verified it. Finished verifies the claim in the negotiation, making it checkable afterward. The difference is who checks it — not the intermediary, the endpoints.

One thing finished does not check is the list’s content: it verifies both sides saw the same list, not that no weak suite is on it. Keeping weak suites on the list is therefore a configuration flaw: the choice was made, the negotiation was verified, and setup still lands on a bad suite. Narrowing: if the list itself is narrowed — only acceptable suites are declared — the set the negotiation can land on also narrows and verification becomes meaningful. This is a symptom, and what is written here is what is wrong.

The Budget Is Paid Per Connection

Setup’s round is written not to the exchange but to the connection: forty exchanges in a single connection pay setup once; in forty separate connections, forty times.

The measurement below models full setup as two round trips and session resumption as one. These are model values chosen within the lesson; they differ across versions of the specification family, and the lesson makes no version claim. What is measured is not the numbers themselves but how they grow with connection count.

EXCHANGES = 40
FULL_ROUND = 2          # full setup budget modeled within the lesson
RESUME_ROUND = 1        # session resumption budget modeled within the lesson

print(f"{'connections':>12s} {'setup without resumption':>25s} {'setup with resumption':>22s} "
      f"{'rounds saved':>13s} {'total rounds':>13s} {'per exchange':>13s}")
for connections in (40, 8, 4, 1):
    without = connections * FULL_ROUND
    with_resumption = FULL_ROUND + (connections - 1) * RESUME_ROUND
    total = with_resumption + EXCHANGES
    print(f"{connections:12d} {without:25d} {with_resumption:22d} {without - with_resumption:13d} "
          f"{total:13d} {total / EXCHANGES:13.3f}")
 connections  setup without resumption  setup with resumption  rounds saved  total rounds  per exchange
          40                        80                     41            39            81         2.025
           8                        16                      9             7            49         1.225
           4                         8                      5             3            45         1.125
           1                         2                      2             0            42         1.050

In the regime that opens forty separate connections, setup without resumption holds 80 rounds; since the forty exchanges themselves hold 40 rounds, opening a connection is twice as expensive as doing the work. Session resumption gives back 39 rounds on this row, bringing the total to 81 — 2.025 rounds per exchange.

Two observations read the table. First, the round resumption earns depends on the connection count: at one persistent connection the gain is zero, because there is nothing to resume. Second, resumption does not eliminate setup — it only makes it cheaper. Even on the forty-connection row, the per-exchange figure stays above 1.000; the only way to zero is keeping the connection persistent.

Resumption earns something beyond rounds too, tying back to the previous lesson: in a resumed session the certificate chain is not re-presented, so chain verification is not redone either. A decision taken once is not retaken for as long as the session lives. Its cost sits in the same place: information about the chain ages, and the session does not see it.

Once the Envelope Closes

Once setup finishes, the application message enters the envelope. The intermediary sees the message arrive, sees its length, sees which host it is going to — but it cannot see the method, the path, the private marker, or the validator.

The decision table counted through the course is now measured in two regimes. In the open regime the intermediary reads all five fields; in the envelope regime, only the host name. The oracle — the truth known because we produced the scenario — does not change. Forty exchanges and five decisions per exchange is a set of 200 decisions; the smallest difference measurable in this set is 1/200 = 0.0050.

Assumptions of this lesson’s two measurements:

  • GT13 — In the budget table, full setup is modeled within the lesson as two round trips, session resumption as one; both values are chosen model values and make no specification version claim.
  • GT14 — Setup’s round is written to the connection, an exchange’s round to the exchange: forty exchanges hold forty rounds, and in the resumed regime the first connection pays full setup, the rest pay resumption.
  • GT15 — In the decision table the course’s forty exchanges are reproduced from the same seed; five decisions per exchange are counted and the oracle does not change when the regime changes.
  • GT16 — Two regimes are compared: in the open regime the intermediary reads the method, the path, the host name, the private marker, and the validator; in the envelope regime, only the host name. The host name is outside the envelope because it is declared in setup’s first message, before a shared secret exists yet.
  • GT17 — The intermediary sits on the path and looks only at what stays outside the envelope; every decision it cannot get is a round debt — one more round is thrown at the server on that exchange.
  • GT18 — The bytes column is a count of the fields the intermediary can read, not the real message’s length; the measurement is not a network measurement.
SEED = 20260809
SAFE = {"GET", "HEAD"}
IDEMPOTENT = {"GET", "HEAD", "PUT", "DELETE"}
METHODS = ["GET", "GET", "GET", "HEAD", "POST", "PUT", "DELETE"]
PATHS = ["/measurement/north", "/measurement/slope", "/summary", "/record", "/session"]
DECISIONS = ("cacheable", "shareable", "fresh", "repeatable", "routable")
VISIBLE = {"open": ("method", "path", "host", "private_marker", "validator"),
           "envelope": ("host",)}


def generator(seed: int):
    d = seed % 2147483646 + 1

    def r(n: int) -> int:
        nonlocal d
        d = (d * 48271) % 2147483647
        return d % n
    return r


def exchanges(count: int = 40) -> list[dict]:
    r, items = generator(SEED), []
    for i in range(count):
        method, path = METHODS[r(7)], PATHS[r(5)]
        private = path == "/session" or r(5) == 0
        changed = r(3) == 0
        items.append({"no": i + 1, "method": method, "path": path,
                       "private": private, "changed": changed,
                       "private_marker": private and r(4) != 0,
                       "stamp_skipped": changed and r(6) == 0})
    return items


def oracle(a: dict) -> dict:
    return {"cacheable": a["method"] in SAFE and not a["private"],
            "shareable": not a["private"],
            "fresh": not a["changed"],
            "repeatable": a["method"] in IDEMPOTENT,
            "routable": True}


def message(a: dict, visible: tuple) -> dict:
    full = {"method": a["method"], "path": a["path"], "host": "station.example",
             "private_marker": a["private_marker"],
             "validator": a["changed"] and not a["stamp_skipped"]}
    return {k: v for k, v in full.items() if k in visible}


def intermediary(i: dict) -> dict:
    k = {}
    if "method" in i:
        k["repeatable"] = i["method"] in IDEMPOTENT
    if "private_marker" in i:
        k["shareable"] = not i["private_marker"]
        if "method" in i:
            k["cacheable"] = i["method"] in SAFE and not i["private_marker"]
    if "validator" in i:
        k["fresh"] = not i["validator"]
    if "host" in i:
        k["routable"] = True
    return k


def bytes_seen(i: dict) -> int:
    return sum(len(k) + len(str(v)) + 4 for k, v in i.items())


def measure(name: str) -> tuple:
    correct = wrong = unavailable = extra_round = total_bytes = 0
    for a in exchanges():
        truth, given = oracle(a), intermediary(message(a, VISIBLE[name]))
        missing = False
        for decision in DECISIONS:
            if decision not in given:
                unavailable += 1
                missing = True
            elif given[decision] == truth[decision]:
                correct += 1
            else:
                wrong += 1
        extra_round += 1 if missing else 0
        total_bytes += bytes_seen(message(a, VISIBLE[name]))
    return correct, wrong, unavailable, extra_round, total_bytes


print(f"{'regime':<8s} {'correct':>7s} {'wrong':>7s} {'unavailable':>11s} "
      f"{'extra round':>13s} {'bytes':>6s}")
for name in ("open", "envelope"):
    print(("{:<8s} {:7d} {:7d} {:11d} {:13d} {:6d}").format(name, *measure(name)))

print()
print(f"{'decision':<20s} {'open':>8s} {'envelope':>10s}")
for decision in DECISIONS:
    counts = [sum(decision in intermediary(message(a, VISIBLE[name])) for a in exchanges())
              for name in ("open", "envelope")]
    print(f"{decision:<20s} {counts[0]:8d} {counts[1]:10d}")
regime   correct   wrong unavailable   extra round  bytes
open         195       5           0             0   3847
envelope      40       0         160            40    920

decision                 open   envelope
cacheable                  40          0
shareable                  40          0
fresh                      40          0
repeatable                 40          0
routable                   40         40

Where the Hundred Sixty Decisions Went

In the open regime the intermediary took 200 of 200 decisions and never returned to the server; 195 were correct, 5 wrong. In the envelope regime 160 decisions become unavailable. These decisions did not disappear; because the intermediary could not get them, they moved to the endpoints. The decision-maker was not just the two endpoints before; now it is.

The second table shows exactly where this 160 was gathered from. Four of the five decisions — cacheable, shareable, fresh, repeatable — are taken in all forty of the forty exchanges in the open regime and in none of them in the envelope regime: four decisions times forty exchanges, 160. The one decision still standing is routable, because its only support, the host name, stays outside the envelope. The loss is not spread evenly, it is sharp: hiding one field does not reduce the decision tied to that field, it zeroes it.

The extra-round column shows the price of this: in all forty of the 40 exchanges the intermediary is missing at least one decision, meaning all forty throw one more round at the server. The visibility the intermediary loses turns directly into a round debt.

The bytes column looks reversed at first glance: it drops from 3847 to 920. The message did not shrink; only the fields the intermediary can read shrank. This is the reverse reading of the course’s second claim — there, adding a field did not earn a decision; here, hiding a field earns bytes but costs decisions.

The real line is the wrong column: 0 in the envelope regime. None of the remaining 40 decisions are wrong. The reason is direct: the only decision left is routable, and that decision looks only at the host name; since the host name stays outside the envelope, it is always correct. The 5 wrong decisions in the open regime came from the places where the intermediary believed a claim — a marker the server forgot to set, and a change the validator missed. The intermediary cannot believe a field it cannot read.

The sentence that follows is the course’s third claim, not a description of a flaw: fewer decisions, but none wrong. The envelope regime lets the intermediary take 40 of 200 decisions; in return it drops the intermediary’s error surface to zero. Which is preferable is not a measurement question but a design decision.

The Remaining Surface and Its Narrowings

The envelope is a record format: the application message is split into pieces, each wrapped with its length and carried that way. This draws the boundary of what the intermediary sees: it sees the record boundaries and lengths — because it cannot forward the message without seeing them — but not what is inside the record. The zeroing of the 160 in the decision table is the result of this distinction: what is left in the intermediary’s hand is the message’s container, not its content.

The envelope does not close everything. What the intermediary still sees, and the narrowing for each:

  • Host name. Declared in setup’s first message, before a shared secret exists yet; in the measurement it is routable‘s only support. Narrowing: mechanisms exist where the name is also folded into setup’s protected part; in that case the table’s last row also moves to unavailable and the intermediary loses the routing decision too.
  • Certificate chain. In full setup the chain is presented in the open and carries the subject’s name. Narrowing: in resumed sessions the chain is never presented; as the connection count drops, this surface shrinks too.
  • Message length and timing. The envelope closes the body, it does not close its length or arrival moment. Narrowing: padding and merging push the length into a band.

There is exactly one way for an intermediary to regain the 160 decisions, and it is not a loophole but a placement decision: the intermediary terminates the envelope itself. Then there are two separate setups — one between the client and the intermediary, one between the intermediary and the server — and the intermediary in the middle returns to the open regime, regaining the 200 decisions in the table’s first row. The price is not read from the table but from the definition: the endpoints no longer set up with each other, both now set up with the intermediary, and the chain the client verifies is the intermediary’s chain, not the server’s. Narrowing: such an intermediary’s anchor must be separately placed on the client; until it is, chain verification falls and the decision loss stays where it was.

There is also a common configuration flaw: completing setup and passing through without verifying the certificate chain. The gain is local and was counted in the previous lesson’s measurement — not a single round. What is paid is not knowing whom the bond the envelope protects was made with. What this lesson states is what is wrong; how this is done is not written.

Summary

  • The handshake gathers suite selection, key-share exchange, certificate-chain presentation, and mutual finishing into a single setup; because the chain arrives with the first response, its verification needs no separate round.
  • The suite negotiation happens on a still-unprotected channel; because the finished message stands on the digest of the whole handshake, the negotiation is checked by the endpoints, not the intermediary.
  • Setup’s budget is written to the connection, not the exchange: 80 rounds without resumption at 40 connections, 41 with resumption; 2 rounds at one persistent connection.
  • Session resumption earns 39 rounds in the forty-connection regime and does not repeat chain verification; its gain is zero at one connection.
  • Once the envelope closes, 160 of 200 decisions move from the intermediary to the endpoints, all forty of the forty exchanges incur a round debt, and the bytes the intermediary reads drop from 3847 to 920.
  • None of the remaining 40 decisions are wrong; the 5 wrong decisions in the open regime came from where the intermediary believed a claim, and the intermediary cannot believe a field it cannot read.

Next Step

Chain verification ran locally, the handshake paid its budget per connection, and once the envelope closed 160 decisions moved to the endpoints. One thing alone stayed unmeasured: the chain the client verified was valid at the moment it was verified. A certificate has a start and an end, it can be revoked between them, and revocation cannot be written into the document. The next lesson counts the certificate’s life cycle and what revocation adds to the round budget: the chain verified without asking; revocation cannot be known without asking.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close