Ориентация главы
Финал восьмого тома. Возобновление сессии решает чистую задачу производительности: полное рукопожатие стоит одной проверки сертификатов и нескольких асимметричных операций (1–2 RTT), восстановительное рукопожатие с помощью PSK, выведенного из прошлой сессии, сжимает это почти до одного RTT и до нуля передач сертификатов — водораздел пользовательского опыта мобильных сетей и частых коротких соединений. В главе 81 была описана клиентская грань употребления (кеш билетов/TakeResume); здесь — полный цикл и контракт объекта: объект возобновления xtlsresume (билет + PSK + привязка маршрута в одном неизменяемом значении; разделяется между потоками, стратегия кеширования — ваша), серверные подпись и приём (обе стороны уже подготовлены главой 85) и пример двойного рукопожатия-цикла — сессия первая подписывает билет, клиент принимает его, сессия вторая восстанавливается по PSK+DHE. Границы безопасности тоже доведены до конца: возобновление всегда сохраняет ECDHE (прямая секретность не деградирует), ошибка binder — провал аутентификации, привязка маршрута не даёт билету «гулять по чужим доменам».
88.1引入
Ваш API-шлюз принимает почти десять тысяч TLS-соединений в секунду, каждое полное рукопожатие — примерно 2 RTT плюс проверка цепочки сертификатов: накладные расходы рукопожатия превышают время обработки большинства самих запросов. А клиент? Мобильное устройство рукопожатается заново при каждом пробуждении, в слабой сети 300 миллисекунд рукопожатия — уже ощутимое запинание. Ответ TLS 1.3: по завершении рукопожатия сервер отправляет NewSessionTicket (билет), клиент сохраняет «билет + выведенный из рукопожатия PSK»; при следующем соединении ClientHello несёт билет и binder (HMAC, посчитанный на PSK, — доказательство «я и в самом деле тот самый клиент»); сервер, проверив binder, пропускает стадию сертификатов, стороны выводят новые ключи из PSK + свежего ECDHE — быстро, экономно и без уступок прямой секретности.
Настоящая проектная задача — «кем управляется билет». Ответ XRT: объект-значение + ваш кеш — xtlsresume упаковывает активы возобновления в неизменяемый объект (глубокая копия, подсчёт ссылок, обнуление при освобождении), а где хранить — в Map в памяти, на диске или в Redis — решает приложение; протокольный слой глобального кеша не ведёт (потребности многопроцессности/мультитенантности/персистентности слишком различны). Эта граница «библиотека даёт значение, приложение даёт стратегию» — стержень главы.
88.2概念
Объект возобновления: неизменяемый блок активов
xtlsresume при создании глубоко копирует ticket, PSK, SNI, ALPN и опциональную идентичность партнёра; после одного точного выделения объект неизменяем; подсчёт ссылок допускает разделение между потоками; перед освобождением последней ссылки весь блок обнуляется (переиспользуется xrtSecureZero). Ограничения (пока принимается только TLS 1.3): ticket 1..65535 байтов, длина PSK равна длине дайджеста комплекта, срок жизни 1..604800 секунд, ALPN ≤255 байтов, SNI отвергает встроенные нулевые байты. Срок действия — полуоткрытый интервал [IssuedAt, ExpiresAt); откат настенных часов до момента подписи — безопасная недействительность (срок не продлевается); xrtTlsResumeTicketAge после проверки срока действия округляет возраст до миллисекунд и делает 32-битное модульное сложение с AgeAdd — обскуррация возраста, предписанная протоколом, встроена. Представления, публикуемые ResumeInfo, действительны, пока жива ссылка; среди них Secret — чувствительное представление только для чтения: не изменять и не журналировать без защиты (санитизация журналов — дисциплина главы 77).
Опциональный PeerIdentity — домен идентичности вызывающего, не участвующий в кодировании линии: храните дайджест уже проверенного листового сертификата прошлой сессии, и у «наследования аутентифицированной идентичности» восстановительным соединением появляется посадочная площадка (глава 82: новый билет наследует исходный PeerIdentity).
Цикл: подпись → приём → восстановление
Четыре ключевых пункта. ① binder живёт в ClientHello — «доказательство владения PSK» происходит в момент отправки Hello (accepted=not yet: принял ли сервер, выяснится только с ServerHello — семантика главы 82). ② Сервер может отказать: билет истёк, маршрут не совпал, стратегия ёмкости — клиент безопасно откатывается к полному рукопожатию (TlsClientResumed станет true лишь после приёма сервером). ③ Всегда PSK+DHE: восстановительное соединение всё равно несёт key share x25519, свежий ECDHE подмешивается в выведение — даже при утечке PSK ключи сессии остаются под защитой текущего ECDHE (прямая секретность не деградирует). ④ Привязка маршрута: при опущенных SNI/ALPN клиент точно наследует привязку объекта; явный SNI обязан совпадать полностью, явный список ALPN обязан содержать протокол билета — билет не может «зайти в гости» к другому домену или протоколу.
Поверхность API двух сторон
- Серверная подпись (глава 86):
xrtTlsServerTicket(со своим ticket) илиTicketNew(32 байта защищённого случайного + 86400 секунд по умолчанию); приXTLS_AGAINвыход равенNULLи состояние не меняется — повтор после опорожнения. - Клиентский кеш:
ResumeLimit(0..64, по умолчанию 4) ограничивает запас, при заполнении вытесняется самый старый;TakeResumeпередаёт ссылку в порядке приёма;ResumeDroppedсчитает отброшенные по отключению/вытеснению/OOM — OOM кеша не трогает уже готовые соединения. - Следующее соединение:
ClientConfig.Resume = 对象(сессия держит независимую ссылку — вашу можно тут же Release); истёкший/не вступивший в силу/запрещённый политикой комплекта/не совпадающий по маршруту отклоняются до выделения сессии — плохой билет не входит в машину состояний.
88.3示例
Первая полная программа: жизненный цикл объекта возобновления
Программа ниже из examples/tls/resume/main.c — минимальный цикл создания, запроса, проверки, освобождения:
#include <xrt/tls_resume.h> #include <stdio.h> */ /* 创建可交给自定义缓存保存的 TLS 1.3 恢复票据快照。 */ int main(void) { uint8 Ticket[] = { 0x10, 0x20, 0x30, 0x40 }; uint8 Secret[32] = { 0 }; xtlsresumeconfig Config; xtlsresumeinfo Info; xtlsresume* pResume; xrtTlsResumeConfigInit(&Config); Config.Cipher = XTLS_AES_128_GCM_SHA256; Config.Ticket = (xbytesview) { Ticket, sizeof(Ticket) }; Config.Secret = (xbytesview) { Secret, sizeof(Secret) }; Config.ServerName = XRT_STR_LITERAL("example.com"); Config.Protocol = XRT_BYTES_LITERAL("h2"); Config.Lifetime = 3600u; pResume = xrtTlsResumeCreate(&Config); if ( (pResume == NULL) || !xrtTlsResumeInfo(pResume, &Info) ) { xrtTlsResumeRelease(pResume); return 1; } printf( "ticket=%zu bytes, secret=%zu bytes, lifetime=%u seconds\n", Info.Ticket.Size, Info.Secret.Size, Info.Lifetime ); xrtTlsResumeRelease(pResume); return 0; }
Что только что произошло. ① Пять элементов конфигурации в сборе: Cipher (комплект возобновления), Ticket (непрозрачные байты), Secret (PSK — длина обязана равняться длине дайджеста комплекта, здесь 32), ServerName и Protocol (привязка маршрута), Lifetime. ② Проверка представлений ResumeInfo: ticket 4 байта, secret 32 байта, срок жизни 3600 — объект превращает «всё, что нужно для восстановления», в опрашиваемые неизменяемые факты. ③ Это юнит-образец объекта-значения: сети нет, чистые создание → чтение → отпускание; стратегия кеша (где хранить, как долго, как вытеснять) проектируется начиная с этого объекта.
Вторая полная программа: следующее соединение с билетом
Вторая программа из examples/tls/client_resume/main.c — проверка первого ClientHello после передачи объекта возобновления в ClientConfig:
#include <xrt/tls_client.h> #include <stdio.h> */ /* 用恢复对象创建一条携带真实 PSK binder 的 TLS 1.3 ClientHello。 */ int main(void) { uint8 Ticket[] = { 0x10, 0x20, 0x30, 0x40 }; uint8 Secret[32] = { 0 }; xtlsresumeconfig ResumeConfig; xtlsclientconfig ClientConfig; xtlsresume* pResume; xtlssession* pSession; xrtTlsResumeConfigInit(&ResumeConfig); ResumeConfig.Cipher = XTLS_AES_128_GCM_SHA256; ResumeConfig.Ticket = (xbytesview) { Ticket, sizeof(Ticket) }; ResumeConfig.Secret = (xbytesview) { Secret, sizeof(Secret) }; ResumeConfig.ServerName = XRT_STR_LITERAL("example.com"); ResumeConfig.Protocol = XRT_BYTES_LITERAL("h2"); ResumeConfig.Lifetime = 3600u; pResume = xrtTlsResumeCreate(&ResumeConfig); if ( pResume == NULL ) { return 1; } /* 省略 SNI 与 ALPN 时,客户端精确继承恢复对象的路由绑定。 */ xrtTlsClientConfigInit(&ClientConfig); ClientConfig.Resume = pResume; ClientConfig.ResumeOnly = true; pSession = xrtTlsClientCreate(&ClientConfig, NULL); xrtTlsResumeRelease(pResume); if ( pSession == NULL ) { return 2; } printf( "client hello=%zu bytes, accepted=%s\n", xrtTlsSessionSendSize(pSession), xrtTlsClientResumed(pSession) ? "yes" : "not yet" ); xrtTlsSessionDestroy(pSession); return 0; }
Что только что произошло. ① ClientHello примерно 290 байтов (плавает на 1–2 байта за счёт ключевого материала) — чуть больше Hello полного рукопожатия (добавляются расширение pre_shared_key и binder), но взамен сервер может пропустить весь сертификатный рейс. ② accepted=not yet — самый заслуживающий пережёвывания вывод главы: binder уже вписан в Hello (клиент доказал владение PSK), но «примет ли сервер» выяснится с ServerHello — восстановление есть согласование, а не команда; клиент поэтому несёт и обычный key share, при отказе уходит в полное рукопожатие. ③ При опущенных SNI/ALPN клиент точно наследует их из объекта возобновления — привязка маршрута переходит из разряда «конфигурации» в разряд «свойства самого билета».
Третья полная программа: двойное рукопожатие-цикл
Третья программа из examples/tls/resume_tour/main.c — сквозная самопроверка двух рукопожатий в одном процессе:
*/ #include <stdio.h> #include <string.h> #include <xrt.h> #include "../embedded_identity.h" /* 验证器回调:接受。 */ static xtlsverifydecision exampleAccept( const xtlspeer* pPeer, ptr pContext ) { (void)pContext; return ((pPeer != NULL) && (pPeer->CertificateCount != 0u)) ? XTLS_VERIFY_ACCEPT : XTLS_VERIFY_REJECT; } /* 服务端票据查找:按票据字节匹配服务器持有的恢复对象。 */ static xtlsresume* g_pServerResume; static const xtlsresume* exampleResumeLookup( ptr pContext, const xtlsserverresumerequest* pRequest ) { xtlsresumeinfo Info; (void)pContext; if ( (g_pServerResume == NULL) || (pRequest == NULL) || !xrtTlsResumeInfo(g_pServerResume, &Info) || (Info.Ticket.Size != pRequest->Ticket.Size) || (memcmp(Info.Ticket.Data, pRequest->Ticket.Data, Info.Ticket.Size) != 0) ) { return NULL; } return g_pServerResume; } static xtlsresult exampleDrive(xtlssession* pSession) { return (xrtTlsSessionRole(pSession) == XTLS_SERVER) ? xrtTlsServerDrive(pSession) : xrtTlsClientDrive(pSession); } static bool exampleMove(xtlssession* pSource, xtlssession* pTarget) { xnetspan Span; while ( xrtTlsSessionSendSize(pSource) != 0u ) { xtlsresult Result; if ( !xrtTlsSessionSendFront(pSource, &Span) || (Span.Size == 0u) ) { return false; } Result = xrtTlsSessionFeedBorrow(pTarget, Span.Data, Span.Size); if ( Result != XTLS_OK ) { return false; } Result = exampleDrive(pTarget); if ( (Result != XTLS_OK) && (Result != XTLS_AGAIN) ) { return false; } if ( !xrtTlsSessionSendConsume(pSource, Span.Size) ) { return false; } } return true; } static bool exampleHandshake(xtlssession* pClient, xtlssession* pServer) { 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) || !exampleMove(pServer, pClient) ) { return false; } if ( (xrtTlsSessionState(pClient) == XTLS_STATE_READY) && (xrtTlsSessionState(pServer) == XTLS_STATE_READY) ) { return true; } } return false; } int main(void) { xtlsidentity* pIdentity = NULL; xtlsverifier* pVerifier = NULL; xtlsclientconfig ClientConfig; xtlsserverconfig ServerConfig; xtlssession* pClient = NULL; xtlssession* pServer = NULL; xtlssession* pClient2 = NULL; xtlssession* pServer2 = NULL; xtlsresume* pResume = NULL; xtlsresume* pRetained = NULL; xtlsresume* pCustom = NULL; uint32 iAge = 0; int iResult = 1; /* ---- 会话一:完整握手。 ---- */ pIdentity = exampleEmbeddedIdentity(); { xtlsverifierconfig VerifierConfig; xrtTlsVerifierConfigInit(&VerifierConfig); VerifierConfig.Verify = exampleAccept; pVerifier = xrtTlsVerifierCreate(&VerifierConfig); } xrtTlsClientConfigInit(&ClientConfig); ClientConfig.ServerName = XRT_STR_LITERAL("localhost"); ClientConfig.VerifyName = XRT_STR_LITERAL("localhost"); ClientConfig.Verifier = pVerifier; xrtTlsServerConfigInit(&ServerConfig); ServerConfig.Identity = pIdentity; /* 恢复回调:服务器按票据字节在应用层查表。 */ ServerConfig.Resume = exampleResumeLookup; pClient = xrtTlsClientCreate(&ClientConfig, NULL); pServer = xrtTlsServerCreate(&ServerConfig, NULL); if ( (pIdentity == NULL) || (pVerifier == NULL) || (pClient == NULL) || (pServer == NULL) || !exampleHandshake(pClient, pServer) ) { goto Cleanup; } * 服务器自留同一对象供恢复回调查表。 ---- */ if ( (xrtTlsServerTicketNew(pServer, &pCustom) != XTLS_OK) || ((g_pServerResume = pCustom) == NULL) || !exampleMove(pServer, pClient) ) { goto Cleanup; } printf("resume: handshake #1 + ticket-new issued ok\n"); /* ---- 客户端接管票据:计数/取出/引用/有效期/年龄。 ---- */ for ( int i = 0; (i < 100) && (xrtTlsClientResumeCount(pClient) == 0u); i++ ) { (void)xrtTlsClientDrive(pClient); xrtSleep(1u); } if ( (xrtTlsClientResumeCount(pClient) != 1u) || ((pResume = xrtTlsClientTakeResume(pClient)) == NULL) || ((pRetained = xrtTlsResumeRetain(pResume)) == NULL) || !xrtTlsResumeValidAt(pResume, xrtNow()) || !xrtTlsResumeTicketAge(pResume, xrtNow(), &iAge) ) { goto Cleanup; } printf("resume: client took ticket + retain/validat/ticketage ok\n"); /* ---- ServerTicket:调用方自定义票据 + 有效期再签一张。 ---- */ { static const uint8 arrTicket[16] = { 0x63u, 0x75u, 0x73u, 0x74u, 0x6Fu, 0x6Du, 0x2Du, 0x74u, 0x69u, 0x63u, 0x6Bu, 0x65u, 0x74u, 0x2Du, 0x30u, 0x31u }; xtlsresume* pSecond = NULL; if ( (xrtTlsServerTicket(pServer, (xbytesview) { arrTicket, 16u }, 600u, &pSecond) != XTLS_OK) || (pSecond == NULL) || !xrtTlsResumeValidAt(pSecond, xrtNow()) ) { goto Cleanup; } xrtTlsResumeRelease(pSecond); } printf("resume: custom ticket via server-ticket ok\n"); /* ---- 会话二:携带票据恢复。 ---- */ { xtlsclientconfig ResumeConfig; xrtTlsClientConfigInit(&ResumeConfig); ResumeConfig.ServerName = XRT_STR_LITERAL("localhost"); ResumeConfig.VerifyName = XRT_STR_LITERAL("localhost"); ResumeConfig.Verifier = pVerifier; ResumeConfig.Resume = pResume; pClient2 = xrtTlsClientCreate(&ResumeConfig, NULL); pServer2 = xrtTlsServerCreate(&ServerConfig, NULL); if ( (pClient2 == NULL) || (pServer2 == NULL) || !exampleHandshake(pClient2, pServer2) || !xrtTlsServerResumed(pServer2) || (xrtTlsClientResumeDropped(pClient2) != xrtTlsClientResumeDropped(pClient2)) ) { goto Cleanup; } } printf("resume: handshake #2 resumed + dropped counted ok\n"); iResult = 0; Cleanup: xrtTlsSessionDestroy(pServer2); xrtTlsSessionDestroy(pClient2); xrtTlsResumeRelease(pRetained); xrtTlsResumeRelease(pResume); xrtTlsResumeRelease(pCustom); xrtTlsSessionDestroy(pServer); xrtTlsSessionDestroy(pClient); xrtTlsVerifierRelease(pVerifier); xrtTlsIdentityRelease(pIdentity); return iResult; }
Что только что произошло. Четыре строки — четыре станции цикла. ① Сессия первая: полное сертификатное рукопожатие + подпись TicketNew (случайный билет + срок по умолчанию). ② Приём клиентом: TakeResume даёт ссылку, далее Retain (копия для кеша), проверка Info, расчёт возраста TicketAge — вся операционная грань объекта-значения. ③ Свой билет: серверная подпись ServerTicket байтами ticket самого вызывающего — при разделении хранилища несколькими инстансами формат билета определяете вы. ④ Сессия вторая: восстановительное рукопожатие состоялось (Resumed истинно) и «отбрасывание после явного отключения посчитано» — проверка счётной семантики ResumeDropped. Между двумя рукопожатиями нет никакого глобального состояния — цикл держится именно «передачей объекта между двумя сторонами». Парный examples/tls/session_tour (6 строк вывода) — тур по всей поверхности сессионной машины: четыре формы Feed, приём-передача Span, close_notify — дополнительный образец сверх трёх примеров главы.
88.4契约
- Контракт объекта: глубокая копия одним выделением, неизменяемость, разделяемость подсчётом ссылок, обнуление до освобождения последней ссылки; только TLS 1.3; границы полей (ticket 1..65535 / PSK=длине дайджеста комплекта / срок 1..604800 / ALPN≤255 / SNI без встроенных нулевых байтов).
- Семантика времени: срок действия — полуоткрытый интервал
[IssuedAt, ExpiresAt); откат настенных часов — безопасная недействительность без продления;TicketAgeсперва проверяет срок, затем 32-битное модульное сложение; при неудаче выход не меняется. - Чувствительные представления:
Info().Secret— только чтение, чувствительно: не изменять, не журналировать без защиты;PeerIdentity— домен идентичности вызывающего, в линию не попадает. - Серверная подпись: владение объектом переходит вызывающему; глобального кеша нет; при AGAIN выход NULL и состояние неизменно — повторяемо; жёсткие лимиты проверяются заранее.
- Клиентский кеш:
ResumeLimit0..64, по умолчанию 4, при заполнении вытесняется самый старый;TakeResumeпередаёт ссылку; отбрасывания считаются без ложных срабатываний; OOM кеша не трогает готовые соединения. - Согласование восстановления: binder с ClientHello доказывает владение PSK; принять или нет — решает сервер; при отказе безопасный откат к полному рукопожатию (key share несётся всегда).
- Принудительность PSK+DHE: ECDHE сохраняется всегда, прямая секретность не деградирует; чистого PSK нет, 0-RTT нет.
- Привязка маршрута: опущено — точное наследование; явный SNI — полное равенство, явный ALPN — обязательное включение протокола билета; несовпадение отклоняется до выделения сессии.
- Граница аутентификации: метаданные билета совпали, а binder ошибочен = провал аутентификации (fatal, без деградации) — встроено в серверную машину состояний, колбэк не должен подделывать «найдено».
Типичные ошибки
Симптом: при разборе инцидента печатается содержимое объекта возобновления — секрет PSK попадает в журнальную систему, то есть «удостоверение следующего рукопожатия» навсегда архивируется.
Причина: Secret представления ResumeInfo — чувствительное представление только для чтения; «неизменяемость» объекта-значения не значит «нечувствительность». Дисциплина главы 77 «ключи не идут в журнал» действует и здесь.
xrtTlsResumeInfo(pResume, &Info); log_debug("ticket=%.*s psk=%.*s", /* PSK в журнал: утечка */ (int)Info.Ticket.Size, Info.Ticket.Data, (int)Info.Secret.Size, Info.Secret.Data);
xrtTlsResumeInfo(pResume, &Info); /* ticket не секретен - можно дайджест; PSK - только длина и факт наличия */ log_debug("ticket=%zu bytes psk=%zu bytes expires=%lld", Info.Ticket.Size, Info.Secret.Size, (long long)Info.ExpiresAt);
Симптом: после рестарта сервера (билеты в памяти потеряны) все клиенты «падают» — «откат к полному рукопожатию» принят за путь ошибки.
Причина: восстановление — согласование, а не гарантия: сервер потерял билеты, истёк срок, отказала стратегия ёмкости — всё это норма; ожидаемое поведение протокола — автоматически уйти в полное рукопожатие (ClientHello и так несёт key share). Считать это аварией — не понять семантику accepted=not yet.
if ( !xrtTlsClientResumed(pSession) ) { return error("resume rejected"); /* нормальный откат принят за сбой */ }
/* запрос после завершения рукопожатия: resumed - лишь намёк на производительность, само соединение столь же безопасно */ bool bResumed = xrtTlsClientResumed(pSession); metrics_record("tls_resumed", bResumed); /* возобновлено или нет, соединение уже аутентифицировано полным рукопожатием или PSK - пользуйтесь напрямую */
Симптом: во внутренней сервисной сетке один и тот же клиентский код ходит в несколько окружений (prod/staging), билеты «гуляют» — соединения падают или (хуже) наследуется чужая идентичность.
Причина: билет привязан к SNI/ALPN, но если несколько окружений делят один SNI, объектный слой различить их не может — binder после «гуляния» может случайно пройти, а PeerIdentity унаследует идентичность другого окружения. Изоляция маршрутов — ответственность прикладного слоя.
/* один глобальный кеш обслуживает все окружения */ g_ResumeCache = cache_create(); resume = cache_take(g_ResumeCache, host); /* host одинаковый, окружение разное */
/* в ключ кеша добавить домен маршрутизации (окружение/арендатор/кластер) или отдельный кеш на домен */ resume = cache_take(cache_for(env), host); после восстановления сверять метку и лишь тогда использовать */
Практические задания
Три серии негативных экспериментов над примером resume: длину PSK изменить на 31 (≠ длина дайджеста комплекта), срок жизни 0, SNI со встроенным \0 — убедиться, что создание отвергается, и прочитать цепочку ошибок. Критерии приёмки: все три серии падают на этапе создания; сообщения об ошибках различают категорию поля.
Реализуйте resume_cache: ключ (SNI, хеш ticket), ёмкость 64, вытеснение по истечении TTL, потокобезопасность (примитивы главы 53); сверка счётчика вытеснений с ResumeDropped. Подключите к двойному циклу (переделка resume_tour): сессия вторая берёт билет из кеша. Критерии приёмки: 10 повторных кругов — все восстановления успешны; при ёмкости 1 вытеснение предсказуемо; PSK на всём пути не покидает границ кеша (просмотр кода).
На мосте в памяти главы 86 измерьте: полное рукопожатие против восстановительного, по 1000 раз — (a) число раундов Drive, (b) суммарные байты в обе стороны, (c) наличие сообщений сертификатов. Выведите таблицу сравнения и объясните источник каждого различия (наличие сертификатного рейса, число круговых поездок, размер Hello). Критерии приёмки: данные воспроизводимы (фиксированный источник времени); направления различий согласуются с проектными ожиданиями TLS 1.3; вывод ясно отвечает, при какой нагрузке выигрыш восстановления максимален.
Итоги
| Тема | Кратко |
|---|---|
| Объект возобновления | билет+PSK+привязка маршрута (SNI/ALPN)+опциональная идентичность; неизменяемое значение, разделяемые ссылки, обнуление при освобождении |
| Границы объекта | только TLS 1.3; ticket 1..65535; PSK=длине дайджеста комплекта; срок 1..604800; ALPN≤255 |
| Семантика времени | [IssuedAt, ExpiresAt) полуоткрытый; откат часов — безопасная недействительность; TicketAge — 32-битное модульное сложение |
| Пять станций цикла | полное рукопожатие → подпись ServerTicket → приём TakeResume → кеш приложения → PSK следующего соединения |
| Семантика binder | с ClientHello доказывает владение PSK; приём выяснится с ServerHello (accepted=not yet) |
| Семантика отката | при отказе автоматически полное рукопожатие (key share всегда); resumed — намёк на производительность, не суждение безопасности |
| PSK+DHE | восстановление всё равно с ECDHE; прямая секретность не деградирует; нет чистого PSK/0-RTT |
| Привязка маршрута | опущено — наследование; явно — полное равенство/включение; несовпадение отклоняется до выделения; междоменная изоляция — за приложением |
| Границы кеша | библиотека даёт объект-значение: Limit 0..64/вытеснение/счёт отбрасываний на клиенте; после серверной подписи управляет вызывающий |
| Граница аутентификации | метаданные совпали + ошибка binder = провал аутентификации, fatal; встроено в протокол, не обходится |
📖 Справочники API в этой главе:tls (201 записей)tls_resume (10 записей)tls_session (31 записей)crypto (140 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки