Chapter Orientation
TLS's object stack has two layers: the session layer (xtlssession — a bare protocol machine taking ciphertext blocks in and producing ciphertext blocks out, transport entirely the caller's choice) and the Stream layer (Chapter 87 — hanging the session machine on a TCP stream). This chapter covers the session layer's server side: xtlsserverconfig configures identity and ALPN, xrtTlsServerCreate builds the session, Feed* feeds received ciphertext, xrtTlsServerDrive advances the state machine, and the public Send queue yields outbound flights — what the transport is (memory bridge, TCP, serial port, custom IoT link) the protocol machine cares nothing about. Three server-specific things also live in this chapter: SNI dynamic selection (the Select callback swapping identity and protocol by domain), ticket issuance (xrtTlsServerTicket), and resumption acceptance (the Resume callback, PSK+DHE). This chapter's examples use "memory hauling" as the transport, the server sparring with a real client in-process — also the standard posture for protocol testing.
86.1引入
When do you want the bare protocol machine instead of the Stream layer? Three real scenarios. One: protocol testing — you want to drive client and server simultaneously in memory, checking flights byte by byte, touching no network. Two: custom transport — running TLS over an IoT serial link, Bluetooth BLE, even a message queue; where the transport bytes come from is your choice. Three: extreme control — a gateway forwarding ciphertext under custom scheduling, where the Stream layer's event model doesn't fit.
The session-layer server has one more "waiting" dimension than the client: the client sends its first flight on creation, while the server after creation waits for the ClientHello — which SNI, which ALPN it receives may decide which identity answers (the standard scenario of one server serving several domains). This "order after reading the menu" capability is carried by the Select callback; if no routing is needed, give a static Identity — one of the two must exist, guaranteeing every client (including resumption fallbacks) a clear authentication path.
86.2概念
The three-step session-driving loop
The three-step loop is fully isomorphic between client and server (the client uses xrtTlsClientDrive) — master one side and the other comes free. The four feed forms correspond to four ownerships (borrow/take/reference/buffer chain — Chapter 65's four append kinds replayed here); the Send queue's Size/Front/Consume consumption trio is the same vocabulary as Chapter 65's buffer chain. The TLS 1.3 full-certificate flight generates ServerHello, EncryptedExtensions, Certificate, CertificateVerify, Finished in sequence inside Drive (Chapter 84's timeline), and after verifying the client's Finished it atomically switches to the application epoch and READY.
Server configuration and the first-flight Arena
xtlsserverconfig borrows the context and static identity during creation; on success the session holds references and deep-copies the ALPN list. With RequireProtocol = true, no common ALPN is an explicit failure — protocol ambiguity dies at handshake. The first flight's extension tables, certificate entries, signature input, and temporary encoding streams come from the session's lazy temporary Arena: idle connections and un-handshaked sessions allocate no Arena blocks, and the first flight's completion immediately securely zeroes and releases — no fixed handshake buffer per connection; 1.2/1.3 share the same Arena and cleanup path. This memory shape is a key contract for high-connection servers.
SNI dynamic selection: the Select callback
Select executes after the ClientHello is strictly parsed and SNI/ALPN extracted, but before any server output is generated. The callback receives the requested SNI and full ALPN payload (borrowed only during the callback); the xtlsserverchoice initially holds the static identity, the protocol index pre-computed by server preference, and a zero Cookie; the callback may swap identity or protocol and write a 64-bit host routing identifier XRT never interprets. xrtTlsServerCookie returns that identifier after a successful selection — the transport adapter uses it to associate the handshaked connection back to "the configuration generation at selection time" (say, the tenant-config version consulted then). Constraints: the context is borrowed only until the first flight completes; you must not recursively drive the same session. xrtTlsServerName returns a deep-copied SNI (the view stable to session destruction), and xrtTlsSessionProtocol returns the final ALPN.
Ticket issuance and resumption acceptance
With XRT_FEATURE_TLS_SERVER_RESUME enabled, resumption plugs into both server sides:
- Issuance: after READY,
xrtTlsServerTicket(会话, 不透明ticket, 寿命, &恢复对象)encodes the caller-provided ticket into a NewSessionTicket and sends it, transferring ownership of the corresponding server resumption object to the caller; theTicketNewconvenience entry uses a 32-byte securely random ticket and an 86400-second default lifetime. XRT maintains no process-global ticket cache — choose a Map per tenant/capacity/expiry/persistence (Chapter 18), a sharded cache, or external storage. The hard send cap is pre-checked before randomness and derivation; onXTLS_AGAINthe output object isNULLand the write sequence and resumption state are unchanged — retry as-is after draining. - Acceptance: the
Resumecallback receives the SNI, ALPN, opaque ticket, and obfuscated age, returning a borrowed immutablextlsresume(Chapter 88's object contract); the session adds a reference and validates version/suite/SNI/ALPN/validity/age tolerance. Ticket not found or route mismatch safely falls back to the full handshake; but "ticket metadata matching with a binder error" is an authentication failure — it must be a fatal Alert, never a degraded bypass (an attacker who cannot produce the PSK should be refused). Resumption always keeps ECDHE (PSK+DHE, forward secrecy undegraded); pure PSK and 0-RTT are unsupported.
The TLS 1.2 path and KeyUpdate
The server's 1.2 path requires EMS, performs the ECDHE certificate full handshake, and atomically switches epochs at the ChangeCipherSpec boundary; no resumption, no renegotiation, no KeyUpdate (1.2's historical decisions). 1.3's active/passive KeyUpdate both follow "old epoch fully queued, new epoch committed once" — send backpressure and allocation failure produce no half-updated state.
86.3示例
First complete program: transport-agnostic server sparring
The following program comes from examples/tls/server/main.c — the server and a real client completing handshake and bidirectional data over a memory bridge:
#include "../common.h" /* 范例验证器只接管信任决策,协议签名和 Finished 仍由 XRT 验证。 */ static xtlsverifydecision exampleTlsAccept( const xtlspeer* pPeer, ptr pContext ) { (void)pContext; return ((pPeer != NULL) && (pPeer->Role == XTLS_SERVER) && (pPeer->CertificateCount != 0)) ? XTLS_VERIFY_ACCEPT : XTLS_VERIFY_REJECT; } /* 把一个会话的全部待发密文复制到对端并精确消费。 */ static bool exampleTlsMove(xtlssession* pSource, xtlssession* pTarget) { xnetspan Span; while ( xrtTlsSessionSendSize(pSource) != 0 ) { if ( !xrtTlsSessionSendFront(pSource, &Span) || (Span.Size == 0) || (xrtTlsSessionFeed( pTarget, Span.Data, Span.Size ) != XTLS_OK) || !xrtTlsSessionSendConsume( pSource, Span.Size ) ) { return false; } } return true; } /* 驱动双方并搬运密文,直到会话同时进入 READY。 */ static bool exampleTlsHandshake( xtlssession* pClient, xtlssession* pServer ) { for ( size_t i = 0; i < 4096u; i++ ) { xtlsresult ClientResult = xrtTlsClientDrive(pClient); xtlsresult ServerResult = xrtTlsServerDrive(pServer); if ( ((ClientResult != XTLS_OK) && (ClientResult != XTLS_AGAIN)) || ((ServerResult != XTLS_OK) && (ServerResult != XTLS_AGAIN)) || !exampleTlsMove(pClient, pServer) || !exampleTlsMove(pServer, pClient) ) { return false; } if ( (xrtTlsSessionState(pClient) == XTLS_STATE_READY) && (xrtTlsSessionState(pServer) == XTLS_STATE_READY) ) { return true; } } return false; } /* 发送一段应用数据,并在对端驱动后完整读取。 */ static bool exampleTlsTransfer( xtlssession* pSource, xtlssession* pTarget, bool bTargetServer, cstr sText ) { char Output[64]; size_t iSize = strlen(sText); size_t iWritten = 0; size_t iRead = 0; xtlsresult Result; if ( (iSize > sizeof(Output)) || (xrtTlsSessionWrite( pSource, sText, iSize, &iWritten ) != XTLS_OK) || (iWritten != iSize) || !exampleTlsMove(pSource, pTarget) ) { return false; } Result = bTargetServer ? xrtTlsServerDrive(pTarget) : xrtTlsClientDrive(pTarget); if ( (Result != XTLS_OK) && (Result != XTLS_AGAIN) ) { return false; } return (xrtTlsSessionRead( pTarget, Output, sizeof(Output), &iRead ) == XTLS_OK) && (iRead == iSize) && (memcmp(Output, sText, iSize) == 0); } /* 打印当前线程保存的结构化根错误。 */ static void exampleTlsError(void) { const xerror* pError = xrtGetError(); if ( pError != NULL ) { fprintf( stderr, "%s: %s\n", xrtErrorOperation(pError), xrtErrorMessage(pError) ); } } */ /* 用真实客户端演示传输无关 TLS 1.3 服务端握手与双向数据。 */ int main(int argc, char** argv) { static const xstrview Protocols[] = { XRT_STR_INIT("h2"), XRT_STR_INIT("http/1.1") }; uint8* pCertificateData = NULL; uint8* pPrivateData = NULL; size_t iCertificateSize = 0; size_t iPrivateSize = 0; xbytesview Certificate; xtlsidentity* pIdentity = NULL; xtlsverifierconfig VerifierConfig; xtlsverifier* pVerifier = NULL; xtlsclientconfig ClientConfig; xtlsserverconfig ServerConfig; xtlssession* pClient = NULL; xtlssession* pServer = NULL; xbytesview Protocol; int iResult = 1; if ( argc != 4 ) { printf( "usage: server <rsa|p256|p384|ed25519> " "<certificate.der> <private.der>\n" ); return 0; } if ( !exampleTlsReadFile( argv[2], &pCertificateData, &iCertificateSize ) || !exampleTlsReadFile( argv[3], &pPrivateData, &iPrivateSize ) ) { fprintf(stderr, "failed to read DER identity\n"); goto Cleanup; } Certificate = (xbytesview) { pCertificateData, iCertificateSize }; pIdentity = exampleTlsIdentity( argv[1], &Certificate, 1u, (xbytesview) { pPrivateData, iPrivateSize } ); if ( pIdentity == NULL ) { exampleTlsError(); goto Cleanup; } xrtTlsVerifierConfigInit(&VerifierConfig); VerifierConfig.Verify = exampleTlsAccept; pVerifier = xrtTlsVerifierCreate(&VerifierConfig); if ( pVerifier == NULL ) { exampleTlsError(); goto Cleanup; } xrtTlsClientConfigInit(&ClientConfig); ClientConfig.ServerName = XRT_STR_LITERAL("example.com"); ClientConfig.Protocols = Protocols; ClientConfig.ProtocolCount = sizeof(Protocols) / sizeof(Protocols[0]); ClientConfig.Verifier = pVerifier; xrtTlsServerConfigInit(&ServerConfig); ServerConfig.Identity = pIdentity; ServerConfig.Protocols = Protocols; ServerConfig.ProtocolCount = sizeof(Protocols) / sizeof(Protocols[0]); ServerConfig.RequireProtocol = true; pClient = xrtTlsClientCreate(&ClientConfig, NULL); pServer = xrtTlsServerCreate(&ServerConfig, NULL); if ( (pClient == NULL) || (pServer == NULL) || !exampleTlsHandshake(pClient, pServer) || !exampleTlsTransfer(pClient, pServer, true, "ping") || !exampleTlsTransfer(pServer, pClient, false, "pong") || !xrtTlsSessionProtocol(pServer, &Protocol) ) { exampleTlsError(); goto Cleanup; } printf( "ready, protocol=%.*s, client=ping, server=pong\n", (int)Protocol.Size, (const char*)Protocol.Data ); iResult = 0; Cleanup: xrtTlsSessionDestroy(pServer); xrtTlsSessionDestroy(pClient); xrtTlsVerifierRelease(pVerifier); xrtTlsIdentityRelease(pIdentity); free(pPrivateData); free(pCertificateData); return iResult; }
What just happened. (1) Identity and verifier assembly: after reading DER files, exampleTlsIdentity constructs the identity (Chapter 83's startup-time validation); the verifier uses the minimal Verify callback (the sparring scenario trusting itself). (2) Dual session creation: ClientCreate and ServerCreate each take a configuration — the server configures Identity + Protocols + RequireProtocol; neither session touches any socket. (3) Memory-bridge handshake: exampleTlsHandshake is the template of the session-layer driving loop — ClientDrive + ServerDrive advancing alternately, exampleTlsMove Feeding one side's Send-queue ciphertext Span by Span to the peer — "transport" is just the hauling between these two loops. To go on a real network, swap Move for socket reads/writes and nothing else changes. (4) Bidirectional data: exampleTlsTransfer demonstrates application plaintext's SessionWrite/Read (through the same Drive-and-Move loop); finally SessionProtocol queries the negotiated ALPN — RequireProtocol guarantees it non-empty. (5) The cleanup destroys both sessions → releases verifier and identity → frees file buffers — the session layer's objects all have the simple "Destroy/Release once" lifecycle.
Second complete program: the session-layer full-feature tour
The second program comes from examples/tls/session_tour/main.c — a six-line self-check of the public session API:
*/ #include <stdio.h> #include <string.h> #include <xrt.h> #include "../embedded_identity.h" /* FeedRef 的释放回调:记录次数。 */ static void exampleRelease(ptr pContext, cbytes pData, size_t iSize) { int* pCount = (int*)pContext; (void)pData; (void)iSize; ++(*pCount); } /* 验证器回调:只接管信任决策,协议签名仍由 XRT 验证。 */ static xtlsverifydecision exampleAccept( const xtlspeer* pPeer, ptr pContext ) { (void)pContext; return ((pPeer != NULL) && (pPeer->CertificateCount != 0u)) ? XTLS_VERIFY_ACCEPT : XTLS_VERIFY_REJECT; } /* 用 SendSpans 聚集全部待发密文,按指定形态喂给对端。 */ typedef enum examplefeed { EXAMPLE_FEED_BORROW, EXAMPLE_FEED_TAKE, EXAMPLE_FEED_REF, EXAMPLE_FEED_BUFFER } examplefeed; /* 按角色驱动会话推进。 */ static xtlsresult exampleDrive(xtlssession* pSession) { return (xrtTlsSessionRole(pSession) == XTLS_SERVER) ? xrtTlsServerDrive(pSession) : xrtTlsClientDrive(pSession); } static bool exampleMove( xtlssession* pSource, xtlssession* pTarget, examplefeed Kind, int* pReleased ) { xnetspan Spans[8]; xnetbuf Chain; * 顺序固定为:喂入 → 驱动对端 → 消费源 Span。 */ while ( xrtTlsSessionSendSize(pSource) != 0u ) { xtlsresult Result; if ( (xrtTlsSessionSendSpans(pSource, Spans, 8u) == 0u) || (Spans[0].Size == 0u) ) { return false; } switch ( Kind ) { case EXAMPLE_FEED_BORROW: Result = xrtTlsSessionFeedBorrow(pTarget, Spans[0].Data, Spans[0].Size); break; case EXAMPLE_FEED_TAKE: { ptr pCopy = xrtMalloc(Spans[0].Size); if ( pCopy == NULL ) { return false; } memcpy(pCopy, Spans[0].Data, Spans[0].Size); Result = xrtTlsSessionFeedTake(pTarget, pCopy, Spans[0].Size); break; } case EXAMPLE_FEED_REF: Result = xrtTlsSessionFeedRef(pTarget, Spans[0].Data, Spans[0].Size, exampleRelease, (ptr)pReleased); break; case EXAMPLE_FEED_BUFFER: default: if ( !xrtNetBufInit(&Chain, NULL) || !xrtNetBufAppend(&Chain, Spans[0].Data, Spans[0].Size) ) { return false; } Result = xrtTlsSessionFeedBuffer(pTarget, &Chain); break; } if ( Result != XTLS_OK ) { return false; } Result = exampleDrive(pTarget); if ( (Result != XTLS_OK) && (Result != XTLS_AGAIN) ) { return false; } if ( !xrtTlsSessionSendConsume(pSource, Spans[0].Size) ) { return false; } } return true; } /* 双向驱动 + 搬运,直到双方 READY。 */ static bool exampleHandshake( xtlssession* pClient, xtlssession* pServer, int* pReleased ) { for ( size_t i = 0; i < 4096u; i++ ) { xtlsresult C = xrtTlsClientDrive(pClient); xtlsresult S = xrtTlsServerDrive(pServer); if ( ((C != XTLS_OK) && (C != XTLS_AGAIN)) || ((S != XTLS_OK) && (S != XTLS_AGAIN)) || !exampleMove(pClient, pServer, EXAMPLE_FEED_BORROW, pReleased) || !exampleMove(pServer, pClient, EXAMPLE_FEED_BORROW, pReleased) ) { return false; } /* exampleMove 内部已驱动对端;此处再补一轮源侧驱动。 */ if ( (xrtTlsSessionState(pClient) == XTLS_STATE_READY) && (xrtTlsSessionState(pServer) == XTLS_STATE_READY) ) { return true; } } return false; } /* 单向:写应用数据 → 搬运 → 驱动对端。 */ static bool exampleSend( xtlssession* pSource, xtlssession* pTarget, bool bTargetServer, cstr sText, examplefeed Kind, int* pReleased ) { size_t iWritten = 0; xtlsresult Result; (void)bTargetServer; (void)Result; if ( (xrtTlsSessionWrite(pSource, sText, strlen(sText), &iWritten) != XTLS_OK) || (iWritten != strlen(sText)) || !exampleMove(pSource, pTarget, Kind, pReleased) ) { return false; } return true; } int main(void) { xtlsidentity* pIdentity = NULL; xtlsverifier* pVerifier = NULL; xtlsclientconfig ClientConfig; xtlsserverconfig ServerConfig; xtlssession* pClient = NULL; xtlssession* pServer = NULL; xnetspan Spans[4]; int iReleased = 0; int iResult = 1; /* ---- 建立双会话:嵌入证书 + 接受式验证器。 ---- */ pIdentity = exampleEmbeddedIdentity(); { xtlsverifierconfig VerifierConfig; xrtTlsVerifierConfigInit(&VerifierConfig); VerifierConfig.Verify = exampleAccept; pVerifier = xrtTlsVerifierCreate(&VerifierConfig); } if ( (pIdentity == NULL) || (pVerifier == NULL) ) { goto Cleanup; } xrtTlsClientConfigInit(&ClientConfig); ClientConfig.ServerName = XRT_STR_LITERAL("localhost"); ClientConfig.VerifyName = XRT_STR_LITERAL("localhost"); ClientConfig.Verifier = pVerifier; xrtTlsServerConfigInit(&ServerConfig); ServerConfig.Identity = pIdentity; pClient = xrtTlsClientCreate(&ClientConfig, NULL); pServer = xrtTlsServerCreate(&ServerConfig, NULL); if ( (pClient == NULL) || (pServer == NULL) || !exampleHandshake(pClient, pServer, &iReleased) || (xrtTlsSessionFeedSize(pClient) != 0u) ) { goto Cleanup; } printf("session: handshake via feed-borrow -> both ready ok\n"); /* ---- 会话属性:角色/版本/套件/事件位/上下文。 ---- */ if ( (xrtTlsSessionRole(pClient) != XTLS_CLIENT) || (xrtTlsSessionRole(pServer) != XTLS_SERVER) || (xrtTlsSessionVersion(pClient) != XTLS_VERSION_13) || (xrtTlsSessionCipher(pClient) == 0u) || (xrtTlsSessionWait(pClient) == 0u) || /* 未显式配置 Context 时会话仍挂在共享默认上下文上。 */ (xrtTlsSessionContext(pClient) == NULL) || (xrtTlsSessionContext(pServer) == NULL) ) { goto Cleanup; } printf("session: role/version/cipher/wait/context ok\n"); /* ---- Take/Ref 喂入:接管与释放回调恰好一次。 ---- */ if ( !exampleSend(pClient, pServer, true, "take", EXAMPLE_FEED_TAKE, &iReleased) || (xrtTlsSessionPlainSize(pServer) != 4u) || !exampleSend(pClient, pServer, true, "ref!", EXAMPLE_FEED_REF, &iReleased) ) { goto Cleanup; } { /* 驱动消费 Ref 喂入的数据后释放回调恰好一次。 */ for ( int i = 0; (i < 100) && (iReleased == 0); i++ ) { (void)xrtTlsServerDrive(pServer); xrtSleep(1u); } if ( iReleased != 1 ) { goto Cleanup; } } printf("session: feed-take + feed-ref (release once) ok\n"); /* ---- Buffer 零复制:密文缓冲链整体移交。 ---- */ if ( !exampleSend(pServer, pClient, false, "chain", EXAMPLE_FEED_BUFFER, &iReleased) || (xrtTlsSessionPlainSize(pClient) != 5u) ) { goto Cleanup; } printf("session: feed-buffer zero-copy chain ok\n"); /* ---- 明文队列零复制检查 + 消费。 ---- */ { char arrText[16]; size_t iCount; size_t iSize; /* 发送侧 Span 计数与聚集同族——上一轮 Move 已清空。 */ if ( (xrtTlsSessionSendSpanCount(pClient) != 0u) ) { goto Cleanup; } iSize = xrtTlsSessionPlainSize(pClient); iCount = xrtTlsSessionPlainSpanCount(pClient); if ( (iSize != 5u) || (iCount == 0u) || !xrtTlsSessionPlainFront(pClient, &Spans[0]) || (Spans[0].Size == 0u) || (xrtTlsSessionPlainSpans(pClient, Spans, 4u) != iCount) ) { goto Cleanup; } memcpy(arrText, Spans[0].Data, Spans[0].Size < 5u ? Spans[0].Size : 5u); /* 前缀可能分散在多个 Span——按总长复制拼合。 */ { size_t iOffset = 0; for ( size_t i = 0; (i < iCount) && (iOffset < 5u); i++ ) { size_t n = Spans[i].Size; if ( n > 5u - iOffset ) { n = 5u - iOffset; } memcpy(arrText + iOffset, Spans[i].Data, n); iOffset += n; } if ( (iOffset != 5u) || (memcmp(arrText, "chain", 5u) != 0) || !xrtTlsSessionPlainConsume(pClient, 5u) || (xrtTlsSessionPlainSize(pClient) != 0u) ) { goto Cleanup; } } } printf("session: send spans gathered + plain spans consumed ok\n"); /* ---- 关闭:Close 排队 close_notify,对端驱动后 Eof。 ---- */ if ( xrtTlsSessionClose(pClient) != XTLS_OK ) { goto Cleanup; } if ( !exampleMove(pClient, pServer, EXAMPLE_FEED_BORROW, &iReleased) ) { goto Cleanup; } { xtlsresult Result = xrtTlsServerDrive(pServer); if ( (Result != XTLS_OK) && (Result != XTLS_AGAIN) ) { goto Cleanup; } } { xtlsalertlevel Level; xtlsalert Alert; * 干净关闭后 PeerAlert 为真且代码恰为 close_notify。 */ if ( (xrtTlsSessionEof(pServer) != XTLS_OK) || !xrtTlsSessionPeerAlert(pServer, &Level, &Alert) || (Level != XTLS_ALERT_WARNING) || (Alert != XTLS_ALERT_CLOSE_NOTIFY) ) { goto Cleanup; } } printf("session: close_notify -> peer eof ok\n"); iResult = 0; Cleanup: xrtTlsSessionDestroy(pServer); xrtTlsSessionDestroy(pClient); xrtTlsVerifierRelease(pVerifier); xrtTlsIdentityRelease(pIdentity); return iResult; }
What just happened. (1) Line 1 is the server example's memory-bridge handshake (the borrow-form Feed); line 3 verifies the other three feed forms one by one — Take (taking ownership), Ref (feed by reference, released exactly once), Buffer (Chapter 65's buffer chain hung on directly, zero-copy) — all four ownerships covered. (2) Line 2 queries the session's public facts: role (client/server the same object type), negotiated version and suite, wait reason (input or output — the two-state decomposition of XTLS_AGAIN), shared context. (3) Line 5 verifies bidirectional Spans: the send side's Span family gathers sends, the receive side consumes plaintext by Span — the application layer's zero-copy vocabulary extended to the session boundary. (4) Line 6 walks the authenticated close: close_notify sent, the peer receives EOF — Chapter 82's close protocol as it looks at the session layer. Six lines together are "this chapter's entire session-layer API running" — treat it as the chapter's executable cheat sheet.
86.4契约
- Identity mandate: a static
Identityor a synchronousSelect— at least one; unknown SNI and resumption fallbacks still have a clear authentication path. - Driving isomorphism:
Feed's four forms (borrow/Take/Ref/Buffer zero-copy) + Drive + the Send queue — client and server share the same public session API. - Atomic switching: 1.3 atomically switches to the application epoch and READY after the client's Finished verifies; 1.2 atomically switches at the CCS boundary; failure publishes no half state.
- Lazy Arena: first-flight temporary memory allocated on demand, securely zeroed and released after use; idle connections carry zero handshake buffers.
- Select timing: after the ClientHello's strict parsing, before any output; SNI/ALPN borrowed only during the callback; Cookie is a 64-bit host routing identifier (XRT never interprets it); the context borrowed until the first flight completes; recursive driving forbidden.
- Ticket issuance: object ownership transfers to the caller; no global cache; on AGAIN the state is unchanged and retryable; hard caps pre-checked.
- Resumption acceptance: not-found/route-mismatch falls back to the full handshake; a binder error is an authentication failure (fatal, no degradation); always PSK+DHE; no pure PSK/0-RTT.
- 1.2 boundary: EMS mandatory; no resumption/renegotiation/KeyUpdate.
- KeyUpdate: old epoch fully queued, new epoch committed once; backpressure and OOM leave no half-update.
- Unimplemented: client-certificate authentication, 0-RTT, asynchronous identity selection; TCP composition entries belong to Chapter 87's Stream layer.
Common Mistakes
Symptoms: casually calling xrtTlsServerDrive inside Select to "advance early" — scrambled state or assertion failures.
Cause: Select runs inside Drive (before first-flight generation); recursively Driving the same session then re-enters the state machine. The callback's duty is only "pick identity, pick protocol, write the Cookie"; advancing belongs to the outer loop.
static void onSelect(xtlsserverchoice* pChoice, ...) { pick_identity(pChoice); xrtTlsServerDrive(pSession); /* re-entering the state machine: undefined behavior */ }
static void onSelect(xtlsserverchoice* pChoice, ...) { pick_identity(pChoice); /* selection only */ pChoice->Cookie = tenant_generation(); /* the routing identifier */ /* advancing is done by the outer feed/drive/send loop */ }
Symptoms: for "high availability", the Resume callback returns fallback when the ticket isn't found and also when binder validation fails — an attacker forging ticket metadata bypasses PSK authentication probing.
Cause: the two failure classes have entirely different security semantics. Not found/route mismatch: the client may hold an old ticket — a normal business path where fallback is reasonable. Binder error: the client claims to hold a ticket's PSK but cannot prove it — either the ticket was stolen or it is an attack; a fatal Alert is mandatory.
static const xtlsresume* onResume(...) { const xtlsresume* pFound = cache_lookup(Ticket); if ( (pFound == NULL) || !quick_check(Ticket) ) { return NULL; /* fallback for everything: binder errors swallowed too */ } return pFound; }
your job is simply never to forge a "found" inside the callback */Symptoms: a server with tens of thousands of idle connections balloons in memory — a few hundred KB of "pre-allocated handshake area" per connection, times the connection count.
Cause: misreading the session layer's memory model. First-flight temporary memory is a lazy Arena: not allocated before the handshake, securely zeroed and released right after; only the session state itself stays resident long-term.
/* hand each connection its own malloc'ed "handshake workspace" */ struct conn { uint8 HandshakeArena[262144]; ... }; /* all wasted */
idle connections carry near-zero handshake memory — capacity planning watches active handshakes only */ pSession = xrtTlsServerCreate(&Config, pPool);
Programming Exercises
Generate an Ed25519 self-signed certificate with OpenSSL, export DER, and run server ed25519 cert.der key.der — confirm the output ready, protocol=h2, client=ping, server=pong (the ALPN preference hit). Switch RequireProtocol to false and trim Protocols to h2 only, watching the protocol index's selection change. Acceptance: both configurations' negotiated protocols match hand derivation.
Implement the Select callback: prepare two identities (self-signed certificates for two domains), selecting by the ClientHello's SNI; write the "tenant generation" into the Cookie and read it back after READY via xrtTlsServerCookie to compare. Acceptance: both SNIs each receive the right certificate; Cookie write and read agree; an unknown SNI takes your defined default policy (static identity or explicit failure).
Change exampleTlsMove's "haul whole Spans" into "at most 7 bytes per haul" random fragmentation — simulating a hostile transport. Tally the upper bound of Drive rounds to handshake completion. Acceptance: at any fragment size (1 to a whole flight) the handshake succeeds (the protocol machine supports cross-record reassembly — Chapter 84); the worst round count is bounded and explainable (flight size ÷ fragment-size order); no memory growth (Chapter 6's stats).
Summary
| Topic | Quick reference |
|---|---|
| Session-layer position | the bare protocol machine: Feed ciphertext / Drive to advance / take flights from Send, transport-agnostic |
| Driving loop | Feed×4 (borrow/Take/Ref/Buffer) → Drive → Send (Size/Front/Consume/Spans) |
| Server configuration | Identity or Select, one required; ALPN deep-copied; RequireProtocol makes ambiguity a failure |
| First-flight Arena | lazy allocation, zeroed and released after use; idle connections zero handshake buffers; 1.2/1.3 share the path |
| Select | after ClientHello parsing, before output; SNI/ALPN borrowed during the callback; Cookie = 64-bit routing identifier |
| Queries | ServerName (deep-copied SNI) / SessionProtocol (final ALPN) / ServerCookie |
| Ticket issuance | Ticket/TicketNew; object ownership transferred; no global cache; AGAIN leaves state unchanged |
| Resumption acceptance | not-found falls back to the full handshake; binder error fatal, no degradation; PSK+DHE; no 0-RTT |
| 1.2 boundary | EMS mandatory; no resumption/renegotiation/KeyUpdate |
| Isomorphism | client and server share the public session API — learn one side, get both |
📖 API references in this chapter:tls (201 entries)tls_verify (17 entries)tls_server (16 entries)crypto (140 entries)
Click for full signatures, parameter constraints, return values and error notes