Chapter Orientation

In Chapter 82 you pressed one function and the handshake ran by itself; this chapter opens the black box. The TLS 1.3 handshake really has only four core messages: ClientHello, ServerHello, (EncryptedExtensions, Certificate, CertificateVerify,) Finished, plus the client's own Finished — each message's "who sent it, what it carries, what it verifies" is laid out, together with the key-schedule chain running throughout (transcript digest → handshake secret → bidirectional traffic secrets → application secrets — all HKDF-driven, where Chapters 74/77's primitives converge). Then three engineering branches: HelloRetryRequest (the server switches groups), version and suite negotiation (1.2/1.3 dual stack), and KeyUpdate (long-connection rekeying). This chapter's examples use two independent entries — the key-exchange primitives and certificate-message encode/decode — to split the handshake's two core mechanisms into unit-testable pieces — Chapter 77's session skeleton fully unfolded at the protocol layer.

84.1引入

When debugging TLS failures, "the handshake hung" carries zero information. Was it the ClientHello's group unsupported (a HelloRetryRequest loop)? Certificate verification failing after ServerHello? The CertificateVerify signature not holding? Or a Finished digest mismatch (a middlebox altered transcript bytes)? Each failure class has different handling — and someone who doesn't understand the timeline can only restart and hope.

The key to understanding the timeline is one insight: every step of the TLS 1.3 handshake accumulates a transcript — both sides feed every handshake message sent and received into the same running digest; keys derive from the transcript, Finished verifies the transcript, resumption tickets bind the transcript. Change one byte of any message and every later key changes — that is the mechanism of "handshake tamper-proofing" itself, not magic but a hash chain. Read the four messages with this lens and every field answers "why it must be here".

84.2概念

The four core messages' timeline

1ClientHello (cleartext)highest supported version + random + session ID + suite list + group list + signature scheme list + key share (preferred group's public key) + SNI/ALPN/PSK
2ServerHello (cleartext)chosen version + suite + random + echoed session ID + chosen key share — both sides compute the handshake secret here
3[server ciphertext begins] EncryptedExtensionsSNI confirmation, ALPN chosen, group-negotiation extras
4Certificateleaf + intermediate chain (entry-based, each entry optionally with an OCSP extension)
5CertificateVerifysignature by the certificate's private key over "role prefix + transcript digest" — the identity proof itself
6Finishedan HMAC over "the transcript before adding this message" — the tamper-proof proof itself
7[client verifies chain and digest] client Finished (protected by the server write key)after mutual confirmation, switch to application keys

Three reading lines. The plaintext/ciphertext boundary sits after ServerHello — the server's EE/Cert/CV/Finished all undergo encryption; eavesdroppers see certificate lengths but no contents; what each message verifies: EE verifies negotiation consistency, CV verifies identity (Chapters 80/81's verification triggers here), Finished verifies global integrity; key-switch points number two — after ServerHello both sides derive handshake keys (EE onward encrypted), and after the client's Finished the application keys take over.

The key schedule: one HKDF chain

1ECDHEClientHello's and ServerHello's key shares -> shared secret (Chapter 76's ECDH)

Compare with Chapter 77's session example: the same "ECDH → HKDF → AEAD" skeleton, which TLS builds into a multi-stage ladder — every Extract/Expand binds the transcript digest at that moment into the key, so keys lock not just the secret but "everything we've said". Each direction has its own secret (client-write/server-read share one, and vice versa) — one-directional rekeying (KeyUpdate) becomes possible because of this. The client's ephemeral private key is erased immediately after the handshake-key switch succeeds — forward secrecy's landing point.

HelloRetryRequest: the server says "switch groups and come again"

When the client's preferred group (say x25519) is unsupported by the server, the server replies HRR asking to switch groups and resend the ClientHello. XRT's client and server support a single HRR — the resent ClientHello switches to the server's designated group (say P-256) and carries the h2 retry marker. Fully transparent to the application; important for diagnostics: "two ClientHellos" in connection logs is not a reconnect, it's HRR.

Version and suite negotiation

Dual-stack (1.2/1.3) negotiation rules: peer's offered list ∩ local support → highest version; suites then pass a triple filter — peer's offered order + local support + identity-type compatibility (an RSA identity excludes pure-ECDSA suites). The local policy array (Chapter 85) is the input baseline for both steps; the server runs them on every connection. xrtTlsVersionSelect/CipherSelect factor negotiation out into unit-testable pure functions — this chapter's example demonstrates the full flow. TLS 1.2's requirements: EMS (extended master secret) and the RFC 5746 empty initial binding mandatory, renegotiation not offered — the old protocol's known weaknesses closed outright by engineering decision.

KeyUpdate: the long connection's rekeying rhythm

When application traffic reaches a threshold, the record layer rekeys automatically (transparent to the application); you can also trigger xrtTlsClientKeyUpdate(方向) manually. Rules: a KeyUpdate message occupies its own record; update_not_requested rekeys only the read direction; update_requested queues the answer under the old write key first, then switches — no AGAIN interruption ever produces a half-updated state (old keys, sequence numbers, and queues preserved as-is; retry after the output drains). The epoch order of queued data is guaranteed by the wire queue — Chapter 75's "nonce counter" discipline automated at the protocol layer.

The certificate message: the entry model

TLS 1.3's Certificate is organized by entries (each entry = a DER certificate + optional extensions like OCSP status); 1.2 is a bare list — xrtTlsCertificateEncode/Parse/Entries handles both versions uniformly (the first parameter gives the version). Entries are DER views, with parsing delegated to Chapter 79's xrtX509Parse. The client's state machine has strict rules for entry extensions: unrequested per-entry extensions are rejected outright.

84.3示例

First complete program: the metadata-driven bidirectional key-exchange loop

The following program comes from examples/tls/key_exchange/main.c — a standalone unit test of the handshake's core mechanism (key-share generation and derivation):

examples/tls/key_exchange/main.c源码 ↗
#include <xrt.h>

#include <stdio.h>
#include <string.h>



/* 选择当前构建中第一个可用命名组。 */
static const xtlsgroupinfo* exampleGroup(void)
{
	static const uint16 Groups[] = {
		XTLS_GROUP_X25519,
		XTLS_GROUP_SECP256R1,
		XTLS_GROUP_X448,
		XTLS_GROUP_SECP384R1
	};

	for ( size_t i = 0; i < (sizeof(Groups) / sizeof(Groups[0])); i++ ) {
		if ( xrtTlsGroupAvailable(Groups[i]) ) {
			return xrtTlsGroupInfo(Groups[i]);
		}
	}
	return NULL;
}






















 */


/* 演示调用方按元数据提供缓冲,并完成双向共享秘密校验。 */
int main(void)
{
	const xtlsgroupinfo* pInfo = exampleGroup();
	uint8 ClientPrivate[56];
	uint8 ClientPublic[97];
	uint8 ServerPrivate[56];
	uint8 ServerPublic[97];
	uint8 ClientShared[56];
	uint8 ServerShared[56];

	if ( pInfo == NULL ) {
		return 1;
	}
	if ( !xrtTlsKeyShareGenerate(
		pInfo->Group,
		ClientPrivate, sizeof(ClientPrivate),
		ClientPublic, sizeof(ClientPublic)
	) || !xrtTlsKeyShareGenerate(
		pInfo->Group,
		ServerPrivate, sizeof(ServerPrivate),
		ServerPublic, sizeof(ServerPublic)
	) || !xrtTlsKeyShareDerive(
		pInfo->Group,
		(xbytesview) { ClientPrivate, pInfo->PrivateSize },
		(xbytesview) { ServerPublic, pInfo->PublicSize },
		ClientShared, sizeof(ClientShared)
	) || !xrtTlsKeyShareDerive(
		pInfo->Group,
		(xbytesview) { ServerPrivate, pInfo->PrivateSize },
		(xbytesview) { ClientPublic, pInfo->PublicSize },
		ServerShared, sizeof(ServerShared)
	) ) {
		return 1;
	}
	if ( memcmp(ClientShared, ServerShared, pInfo->SharedSize) != 0 ) {
		return 1;
	}
	printf("group=%u private=%u public=%u shared=%u\n",
		(unsigned)pInfo->Group,
		(unsigned)pInfo->PrivateSize,
		(unsigned)pInfo->PublicSize,
		(unsigned)pInfo->SharedSize);
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/tls/key_exchange/main.c -lws2_32 -liphlpapigroup=29 private=32 public=32 shared=32

What just happened. (1) exampleGroup searches in x25519 → P-256 → x448 → P-384 order for a group available in the current build (xrtTlsGroupAvailable reflects trimming and backend capabilities) — group=29 is x25519's TLS group number. (2) xtlsgroupinfo gives the three buffers' exact sizes — private 32/public 32/shared 32 (x25519 all 32; P-384 would be 48/97/48): buffers sized from metadata, no guessed constants, the same code runs any group. (3) After the bidirectional Derive, memcmp equal — ECDH symmetry (Chapter 76) verified under the TLS group wrapper. This is all the mathematics of the key-share extension in ClientHello/ServerHello: Generate twice, exchange public keys, Derive the ECDHE shared secret — the first link of the key-schedule chain.

Second complete program: encoding and entry traversal of the Certificate message

The second program comes from examples/tls/messages/main.c, the full round trip of chain → message body → entry cursor:

examples/tls/messages/main.c源码 ↗
#include <stdio.h>
#include <string.h>

#include <xrt.h>






















 */


/* 编码并遍历一个两项 TLS 1.3 Certificate 正文。 */
int main(void)
{
	static const uint8 Leaf[] = { 0x30, 0x01, 0x01 };
	static const uint8 Issuer[] = { 0x30, 0x01, 0x02 };
	xtlscertificateentry Chain[2];
	uint8 Body[64];
	xtlscertificatemessage Message;
	xtlscertificatecursor Cursor;
	xtlscertificateentry Entry;
	xtlsitemresult Result;
	size_t iBodySize;
	size_t iCount = 0;

	memset(Chain, 0, sizeof(Chain));
	Chain[0].Data = (xbytesview) { Leaf, sizeof(Leaf) };
	Chain[1].Data = (xbytesview) { Issuer, sizeof(Issuer) };
	iBodySize = xrtTlsCertificateSize(
		XTLS_VERSION_13, (xbytesview) { NULL, 0 }, Chain, 2u
	);
	if ( (iBodySize == 0) || !xrtTlsCertificateEncode(
		XTLS_VERSION_13, (xbytesview) { NULL, 0 }, Chain, 2u,
		Body, sizeof(Body)
	) || !xrtTlsCertificateParse(
		XTLS_VERSION_13, (xbytesview) { Body, iBodySize }, &Message
	) || !xrtTlsCertificateEntries(&Message, &Cursor) ) {
		return 1;
	}
	while ( (Result = xrtTlsCertificatesRead(
		&Cursor, &Entry
	)) == XTLS_ITEM_VALUE ) {
		printf("certificate[%zu]: %zu bytes\n", iCount, Entry.Data.Size);
		iCount++;
	}
	return (Result == XTLS_ITEM_DONE) && (iCount == 2u) ? 0 : 1;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/tls/messages/main.c -lws2_32 -liphlpapicertificate[0]: 3 bytescertificate[1]: 3 bytes

What just happened. (1) Two-pass encoding: CertificateSize measures the body first (the empty request_context view means the client direction; the server direction's CV context is non-empty), then CertificateEncode writes — a two-entry chain (leaf + issuer, here 3-byte toy DERs) encoded into TLS 1.3 entry format. (2) CertificateParse produces the message view, Entries builds the cursor, Read walks three-state — the same vocabulary as the DER/PEM cursors (Chapter 78). (3) Each Entry.Data is a DER view — the real parsing goes to Chapter 79; the two layers each mind their own: the TLS layer manages entry structure, the X.509 layer certificate semantics. Proxies, sniffers, and certificate-transparency logs all use this pair of entries to handle wire certificates.

84.4契约

  • Protocol scope: TLS 1.2/1.3 dual stack; 1.2 requires EMS and the empty initial binding, no renegotiation; Ed448 unpublished; P-521/SHA-512 excluded from the 1.3 offer.
  • Transcript unbypassable: all handshake messages enter the digest; key derivation and Finished both bind the transcript; CertificateVerify's signed object is "role prefix + the digest before adding CV".
  • Key-switch atomicity: any step failing enters FAILED, publishing no half-set of keys; the client's ephemeral private key is erased immediately after a successful switch (forward secrecy).
  • HRR: client and server support a single retry; the resent ClientHello switches groups and carries the marker; transparent to the application.
  • Negotiation rules: version takes the intersection's highest; suites pass the offered-order + local-support + identity-compatibility triple filter; the policy array is the baseline (Chapter 85).
  • KeyUpdate: occupies its own record; two request modes; the answer under the old write key; AGAIN has no half-updated state; the queue guarantees epoch order.
  • Certificate entries: 1.3 entry-based (extensions allowed), 1.2 bare list, unified Encode/Parse entries; entry DER views go to the X.509 layer; unrequested entry extensions rejected.
  • Error mapping: failures enter XTLS_STATE_FAILED with structured errors; XTLS_AGAIN is merely "more input/output space needed".

Common Mistakes

Pitfall 1: taking HRR as a failed-connection retry:Pitfall 1: taking HRR as a failed-connection retry

Symptoms: monitoring sees two ClientHellos on one connection and misjudges "the client keeps reconnecting", triggering alert storms; or a hand-written proxy treats HRR as an error and disconnects.

Cause: HelloRetryRequest is a normal in-protocol branch — the server asking to switch groups and resend. 1.3 allows it (XRT supports once); more than once is the anomaly.

错误写法
/* at the capture/log layer: alert on seeing a second ClientHello */
if ( hello_count(client) > 1 ) {
	alert("client stuck in reconnect loop");
}
正确写法
   The session state machine folds it into a normal handshake — log by state, not by packet count */
if ( tls_state(session) == XTLS_STATE_FAILED ) {
	alert(failure_reason(session));
}
Pitfall 2: "helpfully" optimizing the transcript:Pitfall 2: "helpfully" optimizing the transcript

Symptoms: a hand-written middle layer (proxy, logger, accelerator) alters handshake-record boundaries — merging or splitting records — and Finished never verifies again.

Cause: the transcript digest's object is the handshake messages, not records, but message integrity depends on byte-faithful records; any "optimization" of wire bytes (reassembly, re-encoding) changes the transcript and Finished's HMAC mismatches — precisely the tampering the protocol exists to detect.

错误写法
/* a middle layer "tidies" record boundaries before forwarding */
merge_small_records(&Inbound);   /* the transcript changed */
正确写法
/* forward raw bytes transparently; to parse, use the record layer's read-only entries */
/* (Chapter 87's read-only record entries exist exactly for such tools) */
Pitfall 3: stuffing TLS-1.2-only schemes into a 1.3 policy:Pitfall 3: stuffing TLS-1.2-only schemes into a 1.3 policy

Symptoms: xrtTlsPolicyValid rejects the configuration — "the signature list contains a 1.2-only scheme"; or conversely, a pure-1.2 deployment stuffed with 1.3-only suites.

Cause: policy validation keeps every list self-consistent with the enabled versions: every suite corresponds to an enabled version, and a non-empty signature list is usable by at least one enabled version — a 1.2 PKCS#1 scheme mixed into a pure-1.3 policy gets intercepted. This is the policy layer's value: catching configuration errors at startup.

错误写法
static const xtlsversion Versions[] = { XTLS_VERSION_13 };
Policy.Versions = Versions;
Policy.VersionCount = 1;
/* SignatureCount unchanged: still the full default family (including 1.2-only) -> Valid rejects */
正确写法
static const xtlsversion Versions[] = { XTLS_VERSION_13 };
static const xtlssignature Sigs13[] = { /* only 1.3 schemes */ };
Policy.Versions = Versions;
Policy.VersionCount = 1;
Policy.Signatures = Sigs13;
Policy.SignatureCount = /* consistent with the array */;
if ( !xrtTlsPolicyValid(&Policy) ) { /* report the configuration error at startup */ }

Programming Exercises

Basic: run two groups of key exchange

Change exampleGroup's order to prefer P-256 and rerun key_exchange — observe the group number and the three sizes changing (a 97-byte public key). Then comment out every group and verify Available's rejection path. Acceptance: both groups close the loop; the size changes match the xtlsgroupinfo documentation.

Advanced: certificate-message version comparison

Encode the same entry chain with xrtTlsCertificateEncode/Parse under XTLS_VERSION_12 and XTLS_VERSION_13; dump both bodies' hex and compare the structural difference (1.2 has no request_context or entry extensions). Acceptance: both bodies walkable by their version's Parse+Entries; you can point out the fields 1.3 adds.

Challenge: a handshake-timeline observer

With the record-parsing layer (Chapter 87) or a raw socket, capture one complete handshake (a local dial to any site) and annotate per this chapter's timeline: each record's type, the cleartext/ciphertext boundary, the handshake-message type sequence. Acceptance: the annotated sequence corresponds exactly to this chapter's timeline (including a possible single HRR); you can name what you can no longer see after ServerHello (the ciphertext begins).

Summary

TopicQuick reference
Four messagesClientHello / ServerHello / (EE+Cert+CV) / Finished + the client's Finished
Clear/cipher boundaryafter ServerHello all server messages encrypted; eavesdroppers see lengths only
Transcriptthe running digest of all handshake messages; keys and Finished both bind it
Key scheduleECDHE → Extract chain (Early/Handshake/Master) → bidirectional traffic secrets → key/IV
Direction independenceeach direction has its own secret; one-directional rekeying (KeyUpdate) follows
Forward secrecythe client's ephemeral private key erased right after the handshake-key switch
HRRthe server's group-switch request; once; the new ClientHello carries the marker; transparent
Negotiationversion takes the intersection's highest; suites pass offered + local + identity-compatibility
KeyUpdateown record; the answer under the old write key; AGAIN never half-updates; epoch order guaranteed
Certificate entries1.3 entry-based / 1.2 bare list unified entries; entry DER views go to the X.509 layer
1.2 boundaryEMS mandatory, no renegotiation; 1.2-only schemes cannot enter a pure-1.3 policy

📖 API references in this chapter:tls (201 entries)crypto (140 entries)

Click for full signatures, parameter constraints, return values and error notes