Cryptography · Protocol Construction

Channel Binding: Tying an Authenticated Session to Its Transport

How a protocol proves that the credential exchange it just performed happened inside this specific TLS connection, and why an unbound exchange is open to a relay in the middle.

An authentication protocol layered inside a TLS connection usually proves something about the user: that they know a password, that they hold a private key, that they can produce a one-time code. It does not, by itself, prove anything about the connection. Those are different claims, and an attacker who can terminate one TLS connection and open another can satisfy the first claim while violating the second, forwarding the credential exchange across the gap.

The attack has a simple shape. A client connects to the attacker, believing it is the server. The attacker connects to the real server and relays the authentication exchange byte for byte. The client’s password proof is valid, because it was computed against the real server’s challenge; the server accepts it, because the challenge it issued was answered correctly. The attacker now holds two half-open connections and sits in the middle of both. Each side has authenticated the other, and neither has authenticated the channel it is actually using.

Channel binding closes this by making the client’s proof depend on a value that identifies the TLS connection. The attacker’s two connections have different identities, so a proof computed for one does not verify in the other, and the relay fails.

Three binding types and what each pins

RFC 5929 defines two channel binding types for TLS, and they pin different things.

tls-server-end-point is a hash of the server’s certificate. It identifies the server’s credential, not the connection, so it survives across connections to the same server and does not stop a relay that presents the same certificate to both sides. It is useful for binding an application-level authentication to the certificate that was validated, which prevents a mismatch between “the certificate the TLS layer accepted” and “the certificate the application thinks it is talking to”, but it provides no freshness.

tls-unique is the first Finished message of the handshake, or for a resumed session the first Finished of the resumption. It is a value that both endpoints computed, that depends on the full handshake transcript, and that no third party can predict or reproduce for a different connection. Binding to it ties the application exchange to this particular handshake. The name is literal: the value is unique to the connection among connections that completed.

The choice between them is a choice about what the application’s authentication claim is supposed to mean. A password exchange wants the connection, so it needs tls-unique or its successor. A certificate-pinning check wants the credential, so tls-server-end-point suffices and is cheaper to compute because the certificate is already available.

The TLS 1.3 gap and the exporter substitute

tls-unique was defined against the TLS 1.2 handshake, where the first Finished message is sent by the client in full handshakes and by the server in resumptions. TLS 1.3 restructured the handshake: the server’s Finished arrives before the client’s, resumption works differently, and the notion of “the first Finished” no longer maps onto a stable value that both sides can identify at the same point in the protocol. A channel binding defined in terms of it would be ambiguous.

RFC 9266 defines tls-exporter for TLS 1.3, built on the exporter interface. An exporter takes a label and a context and returns keying material derived from the connection’s exporter master secret, which itself is derived from the handshake transcript through the server’s Finished. Both endpoints can compute the same value for the same label and context, no third party can compute it for a connection it is not part of, and the value is fixed once the handshake completes. The derivation and its label discipline are the ones described in domain separation and labelled key derivation.

Two properties of the exporter matter for binding. First, the label is chosen by the application protocol, so a binding value cannot be confused with any other use of the same exporter, and a protocol that asks for 32 bytes under one label obtains a value unrelated to the 32 bytes it would obtain under another. Second, the value is available only after the handshake, which places a constraint on the application protocol: the binding cannot be included in anything sent before the TLS handshake completes, which rules out binding an authentication that runs in the same flight as the TLS handshake itself.

The practical consequence is that a protocol supporting both TLS 1.2 and TLS 1.3 needs two binding types, selected by the negotiated version, and must not silently fall back to no binding when the version it expected is not the one in use. The downgrade question is the next problem.

The downgrade problem

Channel binding is negotiated, and negotiation is attacker-influenced. The standard shape is a flag in the authentication protocol’s initial message: the client advertises which binding types it supports, the server responds with the one it will use, and both include the binding value in their proofs.

An attacker who can modify that exchange can remove the client’s advertisement, so the server believes the client supports no binding and accepts an unbound proof. The client, seeing no binding selected, also proceeds unbound. Both sides are behaving correctly with respect to the messages they received, and the relay succeeds.

The defence is that the client’s capability statement is itself authenticated. SCRAM, in RFC 5802, puts the channel binding flag in the GS2 header of the client’s first message, and the client’s final message carries a c= attribute holding that header concatenated with the channel binding data and base64-encoded. The server recomputes the same value from the header it received and aborts if the two differ. An attacker who rewrites the flag in flight changes what the server received but not what the client committed to, so the comparison fails and the exchange ends. SCRAM’s GS2 header also separates three client positions: n for a client that does not support binding, y for one that supports it but believes the server does not, and p= followed by a binding type name for one that requires it. A server that supports binding must abort on y, because the only way an honest client sends that flag is if it believes the server lacks support, which is what a stripped advertisement would make it believe.

This is the same pattern as TLS’s own downgrade protection, where the server’s random carries a marker that a client connecting to a higher-version-capable server will detect, and the client’s Finished covers the marker. In both cases the rule is that a party’s stated capabilities are committed to in a value the attacker cannot forge, so a stripped capability becomes a detected inconsistency rather than a silent fallback.

A second downgrade vector is version-based rather than flag-based. A client that supports tls-exporter and tls-unique will use the former on TLS 1.3 and the latter on TLS 1.2. An attacker who forces a TLS 1.2 connection does not defeat the binding, because tls-unique still binds, but it does change which value is bound and may expose a weaker binding if the TLS 1.2 handshake’s Finished is computed under a cipher suite with different properties. The general principle is that the binding type must be selected from the actual negotiated parameters, not from a policy default, and that the selection must be covered by the authentication proof.

What a binding value must not be

Three tempting substitutes fail, and each failure is instructive.

A random nonce exchanged in the application protocol. It is fresh and unpredictable, but it is not tied to the TLS connection: the attacker relays it along with everything else, and the client’s proof over it verifies on the other side. A nonce prevents replay of a previous session, not a relay within the current one.

The TLS session ID or ticket. These are chosen by the server and are not authenticated by the handshake in a way that binds them to the transcript; a server that issues the same ticket to two connections, or an attacker that observes a ticket and reuses it, breaks the identification. Tickets are also reused across resumptions by design, which is exactly the property a binding must not have.

A hash of the certificate. As with tls-server-end-point, this identifies a credential rather than a connection. It stops a relay that presents a different certificate, and does nothing against a relay that presents the real server’s certificate to the client and the client’s expected server to the server.

The property all three lack is that the value must be computable only by the two endpoints of this connection, and must differ for any other connection. tls-unique gets this from the Finished message’s dependence on the transcript; tls-exporter gets it from the exporter master secret’s dependence on the transcript and the derived keys. Both are also unavailable to a passive observer, which matters because a binding value that an observer can compute is a value the observer can relay.

import hmac

def verify_binding(client_proof: bytes, channel_value: bytes,
                   expected_tag: bytes) -> bool:
    """Constant-time comparison of a binding-derived tag.

    channel_value is the tls-exporter output (or the tls-unique Finished);
    it must come from the same connection that carried the proof.
    """
    computed = hmac.new(channel_value, client_proof, "sha256").digest()
    return hmac.compare_digest(computed, expected_tag)

The comparison is constant-time because a byte-by-byte early exit leaks the position of the first differing byte, and a binding check that leaks its progress is a binding check that can be probed. The same reasoning applies to every tag comparison in a protocol, not only this one.

Costs and residual gaps

Channel binding is cheap: one hash of the certificate, or one exporter invocation of a few dozen bytes, plus a comparison. The cost is in the protocol plumbing, not the cryptography. Both endpoints must have access to the TLS layer’s connection state at the moment the application protocol needs it, which is awkward in designs where TLS is terminated by a proxy and the application speaks to the proxy over a separate socket. In that topology the application cannot see the Finished message or the exporter, and the binding must either be passed through as an out-of-band value, with its own authentication problem, or the design must accept that the application’s authentication is not bound to the client’s connection.

The residual gaps are worth naming. Binding authenticates the connection, not the endpoints’ configuration: a server with a valid certificate and a compromised application still passes. Binding does not prevent a relay that terminates TLS at a middlebox and re-originates it, because in that topology the middlebox is an endpoint of both connections, and the binding value it computes is the correct one for the leg the client sees. And binding does not survive a protocol that discards the value before comparing it, which is a code-path problem rather than a design one.

The composition question this raises is general: a channel binding is a way of composing two protocols so that the outer one’s guarantees transfer to the inner one, and the conditions under which such composition is sound are not automatic. They are the subject of composition and why isolation proofs fail. For the specific case of a handshake whose transcript supplies the binding value, the value’s freshness rests on the accumulation described in transcript hashing and handshake binding, and the direction separation that keeps a client’s proof from being replayed as a server’s is the one in key confirmation ordering.

References

  1. RFC 5929: Channel Bindings for TLS — IETF
  2. RFC 9266: Channel Bindings for TLS 1.3 — IETF
  3. RFC 5056: On the Use of Channel Bindings to Secure Channels — IETF
  4. RFC 5802: Salted Challenge Response Authentication Mechanism (SCRAM) — IETF