Skip to content

tls, quic, httpclient: a non-ASCII ALPN protocol name is refused at configuration instead of sent as other bytes - #370

Merged
MDA2AV merged 1 commit into
mainfrom
fix/340-ascii-alpn
Oct 14, 2026
Merged

MDA2AV merged 1 commit into
mainfrom
fix/340-ascii-alpn

Conversation

@MDA2AV

@MDA2AV MDA2AV commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

Fixes #340.

A configured ALPN protocol name with a non-ASCII character did not go on the wire as configured, on the TLS server, the TLS client or QUIC:

  • TLS server and client: BuildAlpnWire (in TlsService and in TlsClientContext) cast each UTF-16 unit to a byte. U+0168 became 0x68, so the configured Ũ2 went out as the real h2. The connection negotiated, and CaptureAlpn / NegotiatedAlpn reported, a protocol nobody configured.
  • QUIC: QuicEngine.AlpnWire encoded with Encoding.ASCII, which substitutes ?. Ũ2 became ?2, and a client offering the literal ?2 was admitted and served.

What changed

Each of the three builders now refuses a name that is not ASCII (System.Text.Ascii.IsValid) with an ArgumentException, right next to its existing 1..255 length check:

  • TLS server and client: ALPN protocol 'Ũ2' must be ASCII.
  • QUIC: ALPN token 'Ũ2' must be ASCII

Why refuse rather than encode as UTF-8:

  • Every reader of the negotiated name (CaptureAlpn, the client's ReadNegotiatedAlpn, QuicEngineConnection's read-back) decodes ASCII.
  • With ASCII names, the existing length check counts bytes exactly.
  • Each builder runs once, when its configuration is built (TlsService.Start, TlsClientContext.Create, the QuicEngine constructor), before anything is allocated, so a refusal leaks nothing.

The TlsService change is in the core and was approved for this fix.

Proof

The three pending tests are now Test()s. Each already accepted a refusal at configuration as a resolution.

suite the promoted test, with the src/ change reverted with the fix
Tls FAIL alpn: a non-ascii protocol name must not negotiate as a different protocol: a server configured to speak only 'U+0168 2' ACKed h2 - ...: 174 passed, 1 failed, 7 pending PASS: 175 passed, 0 failed, 7 pending
E2E FAIL quic/alpn: a non-ascii allow-list token must not admit a protocol nobody configured: a client offering '?2' completed the handshake against an allow list of 'Ũ2': 285 passed, 1 failed, 5 pending PASS: 286 passed, 0 failed, 5 pending
Http FAIL tls client: a non-ascii alpn protocol must not be offered as a different protocol: ... body: 200|hello over h2, origin saw alpn=h2: 83 passed, 1 failed, 3 pending PASS: 84 passed, 0 failed, 3 pending

The pending tests left in each suite are the ones that were already pending, unrelated to this fix. In Http they are the tests for #342, #343 and #344.

TlsClientPostureTests' remarks listed both of that file's Pending defects, this one and #344's handshake deadline. The paragraph is removed rather than edited down to one bullet, because each Pending test states its own reason. #344's PR removes the same lines, so the two merge cleanly in either order. AlpnNegotiationTests' one-line "live defect" remark goes too.

With its siblings

This batch is #356 (#342), #358 (#343), #366 (#344) and #370 (#340). They merge with each other without conflicts, checked pairwise and all four together. #358 and #366 both edit RunHandshakeAsync, in separate hunks. With all four merged, the Http suite is 88 passed, 0 failed, 0 pending.

…onfiguration instead of sent as other bytes

The three ALPN wire builders did not send a non-ASCII name as configured. The TLS server's and the
TLS client's BuildAlpnWire cast each UTF-16 unit to a byte, so U+0168 went out as 0x68 and the
configured "Ũ2" was the real "h2": the connection negotiated, and CaptureAlpn / NegotiatedAlpn
reported, a protocol nobody configured. QUIC's AlpnWire encoded with Encoding.ASCII, which
substitutes '?', so "Ũ2" became "?2" and a client offering the literal "?2" was admitted and served.

Each builder now refuses a name that is not ASCII with an ArgumentException, next to its existing
1..255 length check. Refusing rather than encoding as UTF-8 because every reader of the negotiated
name (CaptureAlpn, ReadNegotiatedAlpn, QuicEngineConnection's read-back) decodes ASCII, and with
ASCII the existing length check counts bytes exactly.

The pending tests "alpn: a non-ascii protocol name must not negotiate as a different protocol"
(Tls), "quic/alpn: a non-ascii allow-list token must not admit a protocol nobody configured" (E2E)
and "tls client: a non-ascii alpn protocol must not be offered as a different protocol" (Http) are
now Tests; each already accepted a refusal at configuration.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

alpn: a non-ASCII protocol name goes on the wire as other bytes, so a protocol nobody configured is negotiated

1 participant