Empty NEW_TOKEN Is Accepted Instead of FRAME_ENCODING_ERROR
Summary
RFC 9000 requires a client that receives a NEW_TOKEN frame with an empty Token field to treat it as a connection error of type FRAME_ENCODING_ERROR.
In quiche, Token Length == 0 is parsed successfully as an empty string and forwarded through the client connection path. The connection remains open, and the empty token is ignored later by the TLS client handshaker instead of causing the required transport error.
Standard Requirement
An opaque blob that the client can use with a future Initial packet. The token
MUST NOT be empty. A client MUST treat receipt of a NEW_TOKEN frame with an
empty Token field as a connection error of type FRAME_ENCODING_ERROR.
The triggering condition is receipt of a NEW_TOKEN frame whose Token field is empty. The required outcome is a client connection error using FRAME_ENCODING_ERROR.
Relevant Source Code
Source: quiche-main/quiche-main/quiche/quic/core/quic_framer.cc:3082-3090
3082: case IETF_NEW_TOKEN: {
3083: QuicNewTokenFrame frame;
3084: if (!ProcessNewTokenFrame(reader, &frame)) {
3085: return RaiseError(QUIC_INVALID_NEW_TOKEN);
3086: }
3087: QUIC_DVLOG(2) << ENDPOINT << "Processing IETF new token frame " << frame;
3088: if (!visitor_->OnNewTokenFrame(frame)) {
3089: QUIC_DVLOG(1) << "Visitor asked to stop further processing.";
3090: return true;
After successful parsing, the framer delivers the NEW_TOKEN frame to the visitor.
Source: quiche-main/quiche-main/quiche/quic/core/quic_framer.cc:5195-5214
5195: bool QuicFramer::ProcessNewTokenFrame(QuicDataReader* reader,
5196: QuicNewTokenFrame* frame) {
5197: uint64_t length;
5198: if (!reader->ReadVarInt62(&length)) {
5199: set_detailed_error("Unable to read new token length.");
5200: return false;
5201: }
5202: if (length > kMaxNewTokenTokenLength) {
5203: set_detailed_error("Token length larger than maximum.");
5204: return false;
5205: }
5206:
5207: absl::string_view data;
5208: if (!reader->ReadStringPiece(&data, length)) {
5209: set_detailed_error("Unable to read new token data.");
5210: return false;
5211: }
5212: frame->token = std::string(data);
5213: return true;
5214: }
ProcessNewTokenFrame() does not reject length == 0. A zero-length token parses successfully and becomes frame->token == "".
Source: quiche-main/quiche-main/quiche/quic/core/quic_connection.cc:2162-2182
2162: bool QuicConnection::OnNewTokenFrame(const QuicNewTokenFrame& frame) {
2163: QUIC_BUG_IF(quic_bug_12714_15, !connected_)
2164: << "Processing NEW_TOKEN frame when connection is closed. Received "
2165: "packet info: "
2166: << last_received_packet_info_;
2167: if (!UpdatePacketContent(NEW_TOKEN_FRAME)) {
2168: return false;
2169: }
2170:
2171: if (debug_visitor_ != nullptr) {
2172: debug_visitor_->OnNewTokenFrame(frame);
2173: }
2174: if (perspective_ == Perspective::IS_SERVER) {
2175: CloseConnection(QUIC_INVALID_NEW_TOKEN, "Server received new token frame.",
2176: ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET);
2177: return false;
2178: }
2179: MaybeUpdateAckTimeout();
2180: visitor_->OnNewTokenReceived(frame.token);
2181: return true;
2182: }
The client path does not check frame.token.empty() before delivering the token to the visitor.
Source: quiche-main/quiche-main/quiche/quic/core/tls_client_handshaker.cc:512-518
512: void TlsClientHandshaker::OnNewTokenReceived(absl::string_view token) {
513: if (token.empty()) {
514: return;
515: }
516: if (session_cache_ != nullptr) {
517: session_cache_->OnNewTokenReceived(server_id_, token);
518: }
The TLS client handshaker ignores an empty token after it has already been accepted by the connection layer.
Source: quiche-main/quiche-main/quiche/quic/core/quic_error_codes.cc:580-581
580: case QUIC_INVALID_NEW_TOKEN:
581: return {true, static_cast<uint64_t>(PROTOCOL_VIOLATION)};
The generic QUIC_INVALID_NEW_TOKEN mapping is PROTOCOL_VIOLATION, not FRAME_ENCODING_ERROR.
Implementation Behavior
When a client receives an IETF NEW_TOKEN frame with Token Length == 0, quiche parses the frame successfully, stores an empty string in frame.token, and delivers it through QuicConnection::OnNewTokenFrame().
The connection layer does not close the connection. The default TLS client handshaker later ignores the empty token and does not cache it.
Inconsistency Reason
The standard requires an empty NEW_TOKEN token to cause a client connection error of type FRAME_ENCODING_ERROR.
quiche treats the empty token as a valid parsed frame and only ignores it after visitor delivery. Ignoring the token at the handshaker layer is not equivalent to generating the required transport error.
Impact
The default client does not cache the empty token, so the direct security impact is limited. The observable issue is protocol behavior: a peer or conformance test expects FRAME_ENCODING_ERROR, but quiche keeps the connection open and silently drops the token later.
Fix Direction
Reject empty NEW_TOKEN tokens on the client receive path.
After reading the token length, if length == 0, quiche should close the connection with an IETF transport error of FRAME_ENCODING_ERROR. The fix should preserve the existing server-side behavior for receiving a NEW_TOKEN frame, which can still be treated as a protocol violation.
Empty NEW_TOKEN Is Accepted Instead of FRAME_ENCODING_ERROR
Summary
RFC 9000 requires a client that receives a
NEW_TOKENframe with an emptyTokenfield to treat it as a connection error of typeFRAME_ENCODING_ERROR.In quiche,
Token Length == 0is parsed successfully as an empty string and forwarded through the client connection path. The connection remains open, and the empty token is ignored later by the TLS client handshaker instead of causing the required transport error.Standard Requirement
The triggering condition is receipt of a
NEW_TOKENframe whoseTokenfield is empty. The required outcome is a client connection error usingFRAME_ENCODING_ERROR.Relevant Source Code
Source:
quiche-main/quiche-main/quiche/quic/core/quic_framer.cc:3082-3090After successful parsing, the framer delivers the
NEW_TOKENframe to the visitor.Source:
quiche-main/quiche-main/quiche/quic/core/quic_framer.cc:5195-5214ProcessNewTokenFrame()does not rejectlength == 0. A zero-length token parses successfully and becomesframe->token == "".Source:
quiche-main/quiche-main/quiche/quic/core/quic_connection.cc:2162-2182The client path does not check
frame.token.empty()before delivering the token to the visitor.Source:
quiche-main/quiche-main/quiche/quic/core/tls_client_handshaker.cc:512-518The TLS client handshaker ignores an empty token after it has already been accepted by the connection layer.
Source:
quiche-main/quiche-main/quiche/quic/core/quic_error_codes.cc:580-581The generic
QUIC_INVALID_NEW_TOKENmapping isPROTOCOL_VIOLATION, notFRAME_ENCODING_ERROR.Implementation Behavior
When a client receives an IETF
NEW_TOKENframe withToken Length == 0, quiche parses the frame successfully, stores an empty string inframe.token, and delivers it throughQuicConnection::OnNewTokenFrame().The connection layer does not close the connection. The default TLS client handshaker later ignores the empty token and does not cache it.
Inconsistency Reason
The standard requires an empty
NEW_TOKENtoken to cause a client connection error of typeFRAME_ENCODING_ERROR.quiche treats the empty token as a valid parsed frame and only ignores it after visitor delivery. Ignoring the token at the handshaker layer is not equivalent to generating the required transport error.
Impact
The default client does not cache the empty token, so the direct security impact is limited. The observable issue is protocol behavior: a peer or conformance test expects
FRAME_ENCODING_ERROR, but quiche keeps the connection open and silently drops the token later.Fix Direction
Reject empty
NEW_TOKENtokens on the client receive path.After reading the token length, if
length == 0, quiche should close the connection with an IETF transport error ofFRAME_ENCODING_ERROR. The fix should preserve the existing server-side behavior for receiving aNEW_TOKENframe, which can still be treated as a protocol violation.