Loading src/quic.cpp +922 −37 File changed.Preview size limit exceeded, changes collapsed. Show changes src/socket.h +162 −2 Original line number Diff line number Diff line Loading @@ -575,6 +575,11 @@ namespace netplus { bool fin; // carried FIN flag bool ack_eliciting; // true for STREAM frames std::vector<uint8_t> stream_data; // copy of payload for retransmit // True if this packet was originally sent as 0-RTT (long-header, // _early_keys) rather than 1-RTT (_app_keys) — see // checkLossAndRetransmit()'s branch on this for why a retransmit // must not just always re-encrypt under _app_keys.send. bool sent_as_early_data = false; }; // Stream state Loading Loading @@ -744,6 +749,22 @@ namespace netplus { // Retry case — only for reusing a NEW_TOKEN across connections). void setToken(const std::vector<uint8_t>& token) { _pending_token = token; } // RFC 8446 §4.6.1 session ticket, issued by the server after a // successful handshake (via NewSessionTicket), carrying a // resumption PSK. Save it with getSessionTicket() and hand it to // setSessionTicket() on a later, separate connection attempt // (before calling connect()) to request 0-RTT (early data) on that // attempt. An opaque blob to the application, like getStoredToken()/ // setToken() above — combine both on the same reconnect to skip // Retry too. Empty if no NewSessionTicket has arrived yet. const std::vector<uint8_t>& getSessionTicket() const { return _received_session_ticket; } // Must be called before connect(). 0-RTT is only actually attempted // when connect() is used in nonblock mode — see sendStreamData()'s // early-data gate — since the default blocking connect() only // returns once the full handshake (and thus any early-data window) // has already completed. void setSessionTicket(const std::vector<uint8_t>& ticket) { _pending_session_ticket = ticket; } // Stream data callback (for HTTP/3 handling in application layer) // Override base class method - internally wraps to quic-specific callback Loading Loading @@ -829,7 +850,11 @@ namespace netplus { // parameter type for protectPacket()/unprotectPacket() below, and a // nested type must be declared before it's named in a member // function's parameter list. enum class EncryptionLevel { Initial, Handshake, Application }; // EarlyData (0-RTT) is asymmetric, unlike the other three levels: // only one direction is ever populated (client fills // _early_keys.send, server fills _early_keys.recv) — see // deriveEarlyDataKeys(). enum class EncryptionLevel { Initial, Handshake, Application, EarlyData }; // Key material + resolved ciphers for one direction (send or recv) // at one encryption level. Filled once, in deriveInitialKeys()/ Loading Loading @@ -879,6 +904,13 @@ namespace netplus { void processShortHeaderPacket(const uint8_t* data, size_t len); void processInitialPacket(const uint8_t* data, size_t len, size_t pn_offset_hint = SIZE_MAX); void processHandshakePacket(const uint8_t* data, size_t len, size_t pn_offset_hint = SIZE_MAX); // Server-only: decrypt and process an incoming 0-RTT (early data) // packet using _early_keys.recv. Shares the Application // packet-number space with 1-RTT (RFC 9000 §12.3) — tracked via // _app_pn_recv/_app_pn_recv_ranges, not a space of its own. Only // ever reached when _early_keys.recv is populated (see the dispatch // guard in processLongHeaderPacket()). void processZeroRTTPacket(const uint8_t* data, size_t len, size_t pn_offset_hint = SIZE_MAX); void processApplicationPacket(const uint8_t* data, size_t len); // Client-side: handle an incoming Retry packet (RFC 9000 §8.1.2) — // verify its RFC 9001 §5.8 Integrity Tag, capture the token + new Loading Loading @@ -931,11 +963,32 @@ namespace netplus { // Loss detection and retransmission void checkLossAndRetransmit(); void recordSentPacket(uint64_t pn, uint64_t stream_id, uint64_t stream_offset, const uint8_t* data, size_t len, bool fin, size_t wire_size = 0); const uint8_t* data, size_t len, bool fin, size_t wire_size = 0, bool sent_as_early_data = false); // Client-only 0-RTT send path, used by sendStreamData() in place of // its normal (heavily optimized, batched, congestion-window-gated) // logic whenever the handshake hasn't completed yet but early data // keys are ready. Deliberately simple/unbatched/uncongestion-gated: // 0-RTT only ever runs once, for a modest amount of data sent // optimistically before any handshake response, so keeping the // well-tested 1-RTT hot path untouched matters more here than raw // throughput. Packets are recorded via recordSentPacket(..., // sent_as_early_data=true) so checkLossAndRetransmit() retransmits // correctly (as 0-RTT while still possible, else falling back to // 1-RTT) if the server never acknowledges them — including the // case where the server declines 0-RTT entirely. size_t sendStreamDataEarly(uint64_t stream_id, const uint8_t* data, size_t len, bool fin); // Packet building std::vector<uint8_t> buildInitialPacket(const std::vector<uint8_t>& payload); std::vector<uint8_t> buildHandshakePacket(const std::vector<uint8_t>& payload); // Client-only: builds a 0-RTT (early data) long-header packet — // same layout as buildHandshakePacket() (no token field) but with // packet-type bits 0x01 and protected under _early_keys.send. Packet // number is drawn from the shared _app_pn_send counter (RFC 9000 // §12.3 — 0-RTT and 1-RTT share the Application packet-number space). std::vector<uint8_t> buildZeroRTTPacket(const std::vector<uint8_t>& payload); std::vector<uint8_t> buildShortHeaderPacket(const std::vector<uint8_t>& payload); // Server-side: build and send a Retry packet (RFC 9000 §17.2.5) in // response to an Initial that arrived without a valid Loading Loading @@ -971,6 +1024,17 @@ namespace netplus { void deriveInitialKeys(const std::vector<uint8_t>& dcid, bool is_server); void deriveHandshakeKeys(const std::vector<uint8_t>& shared_secret); void deriveApplicationKeys(); // RFC 8446 §7.1 / RFC 9001 §4.6.1 early (0-RTT) traffic secret, // derived from a resumption PSK rather than the (EC)DHE shared // secret. Unlike the three functions above, only one direction is // ever populated per call (`is_send`: true fills _early_keys.send — // client, offering the ticket; false fills _early_keys.recv — // server, having just accepted it) — see EncryptionLevel's comment. // `psk` is the ticket's resumption PSK (_early_data_psk); the // transcript used is whatever `_tls_transcript` holds at the call // site, which callers must ensure is exactly the ClientHello bytes // (RFC 8446: Derive-Secret(early_secret, "c e traffic", ClientHello)). void deriveEarlyDataKeys(const std::vector<uint8_t>& psk, bool is_send); // `mat` is the already-role-resolved key material for this call — // the send-direction struct (e.g. _app_keys.send) to protect/apply Loading Loading @@ -1062,6 +1126,28 @@ namespace netplus { ParsedToken validateToken(const std::vector<uint8_t>& token, const sockaddr_storage& peer_addr, socklen_t peer_len) const; // Session tickets (RFC 8446 §4.6.1): a self-encrypted, AEAD-sealed // opaque blob carrying a resumption PSK, mirroring generateToken()/ // validateToken()'s wire format exactly (12-byte nonce + AES-128-GCM // ciphertext + 16-byte tag) but sealed under _ticket_secret instead // of _token_secret, since this payload (a real key) is more // sensitive than an address-validation token. `psk`/`cipher_suite` // are the already-derived per-ticket PSK (see sendNewSessionTicket()) // and the cipher suite it was issued under. struct ParsedTicket { bool valid = false; std::vector<uint8_t> ticket_id; uint16_t cipher_suite = 0; std::vector<uint8_t> psk; uint32_t max_early_data_size = 0; }; std::vector<uint8_t> generateSessionTicket(const std::vector<uint8_t>& ticket_id, uint16_t cipher_suite, const std::vector<uint8_t>& psk, uint32_t lifetime_secs, uint32_t max_early_data_size) const; ParsedTicket validateSessionTicket(const std::vector<uint8_t>& ticket) const; // RFC 9001 §5.8 Retry Integrity Tag: fixed, RFC-specified key/nonce // (public, not secret — authenticates packet *format*, not origin; // real protection comes from the token above plus the transport Loading Loading @@ -1092,6 +1178,16 @@ namespace netplus { // after HANDSHAKE_DONE, letting this client skip Retry on a future // reconnect by presenting the token via setToken(). void sendNewTokenFrame(); // Server-only, RFC 8446 §4.6.1: issued once per connection right // after HANDSHAKE_DONE (alongside sendNewTokenFrame()), carrying a // resumption PSK the client can present on a later connection to // request 0-RTT. Sent as a CRYPTO frame at the Application level. void sendNewSessionTicket(); // Client-only: handles the post-handshake NewSessionTicket message // (handshake type 0x04) arriving over the Application-level CRYPTO // stream — independently rederives the same resumption PSK the // server used and stores the ticket for getSessionTicket(). void processNewSessionTicket(const std::vector<uint8_t>& msg); void sendHttp3ControlStreams(); void completeHandshake(); Loading Loading @@ -1143,6 +1239,38 @@ namespace netplus { // computed over. std::vector<uint8_t> _initial_crypto_frame; // Client-only 0-RTT/session-ticket state (RFC 8446 4.6.1, // RFC 9001 4.6.1). _pending_session_ticket is offered in the next // ClientHello's pre_shared_key extension (set via // setSessionTicket(), mirroring _pending_token/setToken()); // _received_session_ticket is the last NewSessionTicket this // connection processed, for getSessionTicket(). Opaque to the // application, same as the NEW_TOKEN pattern, but NOT simply the // server's wire ticket bytes internally: unlike the server (whose // ticket is self-encrypted, so any server instance can recover the // PSK from the ticket alone), a client presenting this on a later, // separate quic object has no shared state with the connection that // received it, so the PSK has to travel with the blob — the actual // format (built in processNewSessionTicket(), consumed in // buildClientHello()) is 2-byte cipher_suite + 2-byte psk length + // psk + the server's own ticket bytes. std::vector<uint8_t> _pending_session_ticket; std::vector<uint8_t> _received_session_ticket; // Resumption PSK recovered from _pending_session_ticket (client) or // from the validated ticket offered by the peer (server) — set // right before deriveEarlyDataKeys() is called. Cleared once no // longer needed (it is sensitive key material). std::vector<uint8_t> _early_data_psk; // Cipher suite the offered/accepted ticket was issued under — // determines which hash (SHA-256/SHA-384) the PSK binder and early // traffic secret use, independent of _selected_cipher (RFC 8446 // 4.2.11: a PSK's hash must match the negotiated cipher's hash, // checked before accepting it — see processClientHello()). uint16_t _early_data_cipher_suite = 0x1301; bool _early_data_offered = false; // client sent pre_shared_key + early_data bool _early_data_accepted = false; // peer confirmed early_data (ServerHello PSK / EncryptedExtensions) bool _early_data_rejected = false; // handshake completed without confirmation — stop using 0-RTT, retransmit unacked bytes at 1-RTT // Anti-amplification limit (RFC 9000 §8.1): until the peer's // address is validated, this endpoint must never send more than // 3x the bytes it has received from that address — otherwise it Loading Loading @@ -1197,10 +1325,24 @@ namespace netplus { LevelKeys _initial_keys; // Initial: derived from connection ID, always AES-128 LevelKeys _hs_keys; // Handshake: size depends on negotiated cipher LevelKeys _app_keys; // Application: size depends on negotiated cipher // 0-RTT (RFC 9001 §4.6.1): asymmetric — client only ever fills // .send, server only ever fills .recv (see EncryptionLevel's // comment). Populated by deriveEarlyDataKeys() when a session // ticket's PSK is offered (client) or accepted (server). LevelKeys _early_keys; // TLS handshake traffic secrets (for computing Finished) std::vector<uint8_t> _c_hs_traffic_secret; std::vector<uint8_t> _s_hs_traffic_secret; // Master secret from the main (EC)DHE key schedule (RFC 8446 §7.1) — // needed after the handshake completes to derive // resumption_master_secret for session tickets. Set at the end of // deriveApplicationKeys(); was previously just a local temporary. std::vector<uint8_t> _master_secret; // Derive-Secret(_master_secret, "res master", transcript through the // client's Finished) — set (server-side only) in processFinished(), // consumed by sendNewSessionTicket() to derive each ticket's PSK. std::vector<uint8_t> _resumption_master_secret; // TLS handshake state. // Loading Loading @@ -1231,6 +1373,9 @@ namespace netplus { std::vector<uint8_t> _crypto_recv_app; std::vector<uint8_t> _crypto_send_initial; std::vector<uint8_t> _crypto_send_handshake; // Server-only: post-handshake CRYPTO stream (NewSessionTicket), // sent at the Application level — see sendNewSessionTicket(). std::vector<uint8_t> _crypto_send_app; // CRYPTO stream reassembly - stores received ranges to handle out-of-order delivery struct CryptoRange { Loading Loading @@ -1417,6 +1562,21 @@ namespace netplus { // of persisting/rotating a secret across restarts. std::vector<uint8_t> _token_secret; // Secret backing session tickets (RFC 8446 4.6.1), same // generated-once-per-listener/reached-via-_parent lifetime as // _token_secret above, but a distinct secret — a ticket AEAD-seals // the actual resumption PSK, a more sensitive payload than an // address-validation token, so it gets its own key rather than // reusing _token_secret under a different label. std::vector<uint8_t> _ticket_secret; // Anti-replay for session-ticket PSKs (RFC 8446 8.1): a ticket's // resumption PSK is single-use for early data. Keyed by ticket_id // (the same 16 random bytes doubling as the wire ticket_nonce), // value is the ticket's expiry — swept lazily on lookup, same // pattern as _recent_retries below. Guarded by _registry_mutex. std::unordered_map<std::string, std::chrono::steady_clock::time_point> _used_ticket_ids; // Mutex for this connection's own state (recursive: musl returns // EDEADLK on double-lock by same thread, which happens when // processFrame callbacks re-enter locking functions like Loading Loading
src/socket.h +162 −2 Original line number Diff line number Diff line Loading @@ -575,6 +575,11 @@ namespace netplus { bool fin; // carried FIN flag bool ack_eliciting; // true for STREAM frames std::vector<uint8_t> stream_data; // copy of payload for retransmit // True if this packet was originally sent as 0-RTT (long-header, // _early_keys) rather than 1-RTT (_app_keys) — see // checkLossAndRetransmit()'s branch on this for why a retransmit // must not just always re-encrypt under _app_keys.send. bool sent_as_early_data = false; }; // Stream state Loading Loading @@ -744,6 +749,22 @@ namespace netplus { // Retry case — only for reusing a NEW_TOKEN across connections). void setToken(const std::vector<uint8_t>& token) { _pending_token = token; } // RFC 8446 §4.6.1 session ticket, issued by the server after a // successful handshake (via NewSessionTicket), carrying a // resumption PSK. Save it with getSessionTicket() and hand it to // setSessionTicket() on a later, separate connection attempt // (before calling connect()) to request 0-RTT (early data) on that // attempt. An opaque blob to the application, like getStoredToken()/ // setToken() above — combine both on the same reconnect to skip // Retry too. Empty if no NewSessionTicket has arrived yet. const std::vector<uint8_t>& getSessionTicket() const { return _received_session_ticket; } // Must be called before connect(). 0-RTT is only actually attempted // when connect() is used in nonblock mode — see sendStreamData()'s // early-data gate — since the default blocking connect() only // returns once the full handshake (and thus any early-data window) // has already completed. void setSessionTicket(const std::vector<uint8_t>& ticket) { _pending_session_ticket = ticket; } // Stream data callback (for HTTP/3 handling in application layer) // Override base class method - internally wraps to quic-specific callback Loading Loading @@ -829,7 +850,11 @@ namespace netplus { // parameter type for protectPacket()/unprotectPacket() below, and a // nested type must be declared before it's named in a member // function's parameter list. enum class EncryptionLevel { Initial, Handshake, Application }; // EarlyData (0-RTT) is asymmetric, unlike the other three levels: // only one direction is ever populated (client fills // _early_keys.send, server fills _early_keys.recv) — see // deriveEarlyDataKeys(). enum class EncryptionLevel { Initial, Handshake, Application, EarlyData }; // Key material + resolved ciphers for one direction (send or recv) // at one encryption level. Filled once, in deriveInitialKeys()/ Loading Loading @@ -879,6 +904,13 @@ namespace netplus { void processShortHeaderPacket(const uint8_t* data, size_t len); void processInitialPacket(const uint8_t* data, size_t len, size_t pn_offset_hint = SIZE_MAX); void processHandshakePacket(const uint8_t* data, size_t len, size_t pn_offset_hint = SIZE_MAX); // Server-only: decrypt and process an incoming 0-RTT (early data) // packet using _early_keys.recv. Shares the Application // packet-number space with 1-RTT (RFC 9000 §12.3) — tracked via // _app_pn_recv/_app_pn_recv_ranges, not a space of its own. Only // ever reached when _early_keys.recv is populated (see the dispatch // guard in processLongHeaderPacket()). void processZeroRTTPacket(const uint8_t* data, size_t len, size_t pn_offset_hint = SIZE_MAX); void processApplicationPacket(const uint8_t* data, size_t len); // Client-side: handle an incoming Retry packet (RFC 9000 §8.1.2) — // verify its RFC 9001 §5.8 Integrity Tag, capture the token + new Loading Loading @@ -931,11 +963,32 @@ namespace netplus { // Loss detection and retransmission void checkLossAndRetransmit(); void recordSentPacket(uint64_t pn, uint64_t stream_id, uint64_t stream_offset, const uint8_t* data, size_t len, bool fin, size_t wire_size = 0); const uint8_t* data, size_t len, bool fin, size_t wire_size = 0, bool sent_as_early_data = false); // Client-only 0-RTT send path, used by sendStreamData() in place of // its normal (heavily optimized, batched, congestion-window-gated) // logic whenever the handshake hasn't completed yet but early data // keys are ready. Deliberately simple/unbatched/uncongestion-gated: // 0-RTT only ever runs once, for a modest amount of data sent // optimistically before any handshake response, so keeping the // well-tested 1-RTT hot path untouched matters more here than raw // throughput. Packets are recorded via recordSentPacket(..., // sent_as_early_data=true) so checkLossAndRetransmit() retransmits // correctly (as 0-RTT while still possible, else falling back to // 1-RTT) if the server never acknowledges them — including the // case where the server declines 0-RTT entirely. size_t sendStreamDataEarly(uint64_t stream_id, const uint8_t* data, size_t len, bool fin); // Packet building std::vector<uint8_t> buildInitialPacket(const std::vector<uint8_t>& payload); std::vector<uint8_t> buildHandshakePacket(const std::vector<uint8_t>& payload); // Client-only: builds a 0-RTT (early data) long-header packet — // same layout as buildHandshakePacket() (no token field) but with // packet-type bits 0x01 and protected under _early_keys.send. Packet // number is drawn from the shared _app_pn_send counter (RFC 9000 // §12.3 — 0-RTT and 1-RTT share the Application packet-number space). std::vector<uint8_t> buildZeroRTTPacket(const std::vector<uint8_t>& payload); std::vector<uint8_t> buildShortHeaderPacket(const std::vector<uint8_t>& payload); // Server-side: build and send a Retry packet (RFC 9000 §17.2.5) in // response to an Initial that arrived without a valid Loading Loading @@ -971,6 +1024,17 @@ namespace netplus { void deriveInitialKeys(const std::vector<uint8_t>& dcid, bool is_server); void deriveHandshakeKeys(const std::vector<uint8_t>& shared_secret); void deriveApplicationKeys(); // RFC 8446 §7.1 / RFC 9001 §4.6.1 early (0-RTT) traffic secret, // derived from a resumption PSK rather than the (EC)DHE shared // secret. Unlike the three functions above, only one direction is // ever populated per call (`is_send`: true fills _early_keys.send — // client, offering the ticket; false fills _early_keys.recv — // server, having just accepted it) — see EncryptionLevel's comment. // `psk` is the ticket's resumption PSK (_early_data_psk); the // transcript used is whatever `_tls_transcript` holds at the call // site, which callers must ensure is exactly the ClientHello bytes // (RFC 8446: Derive-Secret(early_secret, "c e traffic", ClientHello)). void deriveEarlyDataKeys(const std::vector<uint8_t>& psk, bool is_send); // `mat` is the already-role-resolved key material for this call — // the send-direction struct (e.g. _app_keys.send) to protect/apply Loading Loading @@ -1062,6 +1126,28 @@ namespace netplus { ParsedToken validateToken(const std::vector<uint8_t>& token, const sockaddr_storage& peer_addr, socklen_t peer_len) const; // Session tickets (RFC 8446 §4.6.1): a self-encrypted, AEAD-sealed // opaque blob carrying a resumption PSK, mirroring generateToken()/ // validateToken()'s wire format exactly (12-byte nonce + AES-128-GCM // ciphertext + 16-byte tag) but sealed under _ticket_secret instead // of _token_secret, since this payload (a real key) is more // sensitive than an address-validation token. `psk`/`cipher_suite` // are the already-derived per-ticket PSK (see sendNewSessionTicket()) // and the cipher suite it was issued under. struct ParsedTicket { bool valid = false; std::vector<uint8_t> ticket_id; uint16_t cipher_suite = 0; std::vector<uint8_t> psk; uint32_t max_early_data_size = 0; }; std::vector<uint8_t> generateSessionTicket(const std::vector<uint8_t>& ticket_id, uint16_t cipher_suite, const std::vector<uint8_t>& psk, uint32_t lifetime_secs, uint32_t max_early_data_size) const; ParsedTicket validateSessionTicket(const std::vector<uint8_t>& ticket) const; // RFC 9001 §5.8 Retry Integrity Tag: fixed, RFC-specified key/nonce // (public, not secret — authenticates packet *format*, not origin; // real protection comes from the token above plus the transport Loading Loading @@ -1092,6 +1178,16 @@ namespace netplus { // after HANDSHAKE_DONE, letting this client skip Retry on a future // reconnect by presenting the token via setToken(). void sendNewTokenFrame(); // Server-only, RFC 8446 §4.6.1: issued once per connection right // after HANDSHAKE_DONE (alongside sendNewTokenFrame()), carrying a // resumption PSK the client can present on a later connection to // request 0-RTT. Sent as a CRYPTO frame at the Application level. void sendNewSessionTicket(); // Client-only: handles the post-handshake NewSessionTicket message // (handshake type 0x04) arriving over the Application-level CRYPTO // stream — independently rederives the same resumption PSK the // server used and stores the ticket for getSessionTicket(). void processNewSessionTicket(const std::vector<uint8_t>& msg); void sendHttp3ControlStreams(); void completeHandshake(); Loading Loading @@ -1143,6 +1239,38 @@ namespace netplus { // computed over. std::vector<uint8_t> _initial_crypto_frame; // Client-only 0-RTT/session-ticket state (RFC 8446 4.6.1, // RFC 9001 4.6.1). _pending_session_ticket is offered in the next // ClientHello's pre_shared_key extension (set via // setSessionTicket(), mirroring _pending_token/setToken()); // _received_session_ticket is the last NewSessionTicket this // connection processed, for getSessionTicket(). Opaque to the // application, same as the NEW_TOKEN pattern, but NOT simply the // server's wire ticket bytes internally: unlike the server (whose // ticket is self-encrypted, so any server instance can recover the // PSK from the ticket alone), a client presenting this on a later, // separate quic object has no shared state with the connection that // received it, so the PSK has to travel with the blob — the actual // format (built in processNewSessionTicket(), consumed in // buildClientHello()) is 2-byte cipher_suite + 2-byte psk length + // psk + the server's own ticket bytes. std::vector<uint8_t> _pending_session_ticket; std::vector<uint8_t> _received_session_ticket; // Resumption PSK recovered from _pending_session_ticket (client) or // from the validated ticket offered by the peer (server) — set // right before deriveEarlyDataKeys() is called. Cleared once no // longer needed (it is sensitive key material). std::vector<uint8_t> _early_data_psk; // Cipher suite the offered/accepted ticket was issued under — // determines which hash (SHA-256/SHA-384) the PSK binder and early // traffic secret use, independent of _selected_cipher (RFC 8446 // 4.2.11: a PSK's hash must match the negotiated cipher's hash, // checked before accepting it — see processClientHello()). uint16_t _early_data_cipher_suite = 0x1301; bool _early_data_offered = false; // client sent pre_shared_key + early_data bool _early_data_accepted = false; // peer confirmed early_data (ServerHello PSK / EncryptedExtensions) bool _early_data_rejected = false; // handshake completed without confirmation — stop using 0-RTT, retransmit unacked bytes at 1-RTT // Anti-amplification limit (RFC 9000 §8.1): until the peer's // address is validated, this endpoint must never send more than // 3x the bytes it has received from that address — otherwise it Loading Loading @@ -1197,10 +1325,24 @@ namespace netplus { LevelKeys _initial_keys; // Initial: derived from connection ID, always AES-128 LevelKeys _hs_keys; // Handshake: size depends on negotiated cipher LevelKeys _app_keys; // Application: size depends on negotiated cipher // 0-RTT (RFC 9001 §4.6.1): asymmetric — client only ever fills // .send, server only ever fills .recv (see EncryptionLevel's // comment). Populated by deriveEarlyDataKeys() when a session // ticket's PSK is offered (client) or accepted (server). LevelKeys _early_keys; // TLS handshake traffic secrets (for computing Finished) std::vector<uint8_t> _c_hs_traffic_secret; std::vector<uint8_t> _s_hs_traffic_secret; // Master secret from the main (EC)DHE key schedule (RFC 8446 §7.1) — // needed after the handshake completes to derive // resumption_master_secret for session tickets. Set at the end of // deriveApplicationKeys(); was previously just a local temporary. std::vector<uint8_t> _master_secret; // Derive-Secret(_master_secret, "res master", transcript through the // client's Finished) — set (server-side only) in processFinished(), // consumed by sendNewSessionTicket() to derive each ticket's PSK. std::vector<uint8_t> _resumption_master_secret; // TLS handshake state. // Loading Loading @@ -1231,6 +1373,9 @@ namespace netplus { std::vector<uint8_t> _crypto_recv_app; std::vector<uint8_t> _crypto_send_initial; std::vector<uint8_t> _crypto_send_handshake; // Server-only: post-handshake CRYPTO stream (NewSessionTicket), // sent at the Application level — see sendNewSessionTicket(). std::vector<uint8_t> _crypto_send_app; // CRYPTO stream reassembly - stores received ranges to handle out-of-order delivery struct CryptoRange { Loading Loading @@ -1417,6 +1562,21 @@ namespace netplus { // of persisting/rotating a secret across restarts. std::vector<uint8_t> _token_secret; // Secret backing session tickets (RFC 8446 4.6.1), same // generated-once-per-listener/reached-via-_parent lifetime as // _token_secret above, but a distinct secret — a ticket AEAD-seals // the actual resumption PSK, a more sensitive payload than an // address-validation token, so it gets its own key rather than // reusing _token_secret under a different label. std::vector<uint8_t> _ticket_secret; // Anti-replay for session-ticket PSKs (RFC 8446 8.1): a ticket's // resumption PSK is single-use for early data. Keyed by ticket_id // (the same 16 random bytes doubling as the wire ticket_nonce), // value is the ticket's expiry — swept lazily on lookup, same // pattern as _recent_retries below. Guarded by _registry_mutex. std::unordered_map<std::string, std::chrono::steady_clock::time_point> _used_ticket_ids; // Mutex for this connection's own state (recursive: musl returns // EDEADLK on double-lock by same thread, which happens when // processFrame callbacks re-enter locking functions like Loading