fix(quic): thread offered ALPN list through TlsClient → ClientHello

QuicConnection.alpnList → TlsClient.offeredAlpns was captured but never
threaded into buildQuicClientHello. The builder accepted only an
"additionalAlpn" parameter and hardcoded "h3" first, so our wire
ClientHello always carried just [h3] regardless of caller intent.
quic-go enforces strictly with TLS alert 120 (no_application_protocol,
CRYPTO_ERROR 376) when none of the offered ALPNs match its server
config — handshake failed at the very first server response against
quic-go's hq-interop testcases.

Replaced the awkward additionalAlpn shape with `alpns: List<ByteArray>`
(default [h3] for backward-compat) and threaded TlsClient.offeredAlpns
through.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vitor Pamplona
2026-05-07 15:48:45 -04:00
parent 8fb560d818
commit 2a4c07ae5e
2 changed files with 12 additions and 6 deletions
@@ -154,6 +154,7 @@ class TlsClient(
serverName = serverName,
x25519PublicKey = keyPair!!.publicKey,
quicTransportParams = transportParameters,
alpns = offeredAlpns,
random = random,
cipherSuites = cipherSuites,
)
@@ -83,14 +83,22 @@ class TlsClientHello(
* - signature_algorithms covering ECDSA / RSA-PSS / Ed25519
* - key_share with the caller's X25519 public
* - psk_key_exchange_modes = [ psk_dhe_ke ]
* - ALPN = [ h3 ]
* - ALPN = caller-supplied list (default `[ h3 ]`)
* - quic_transport_parameters = (caller-supplied opaque bytes)
*
* Caller must pass the FULL list of ALPNs to offer. Earlier shape took an
* `additionalAlpn` parameter and forced `h3` first — that silently dropped
* any caller-supplied list (e.g. `[hq-interop, h3]` for the interop
* runner's hq-interop testcases) because the production call site never
* threaded the list through. quic-go enforces strictly with TLS alert
* 120 (`no_application_protocol`, CRYPTO_ERROR 376) when the offered
* ALPNs don't include one its server is configured for.
*/
fun buildQuicClientHello(
serverName: String,
x25519PublicKey: ByteArray,
quicTransportParams: ByteArray,
additionalAlpn: List<ByteArray> = emptyList(),
alpns: List<ByteArray> = listOf(TlsConstants.ALPN_H3),
random: ByteArray = RandomInstance.bytes(32),
cipherSuites: IntArray =
intArrayOf(
@@ -98,9 +106,6 @@ fun buildQuicClientHello(
TlsConstants.CIPHER_TLS_CHACHA20_POLY1305_SHA256,
),
): TlsClientHello {
val alpn = mutableListOf<ByteArray>()
alpn += TlsConstants.ALPN_H3
alpn += additionalAlpn
val exts =
listOf(
TlsExtension(TlsConstants.EXT_SERVER_NAME, encodeServerNameExtension(serverName)),
@@ -109,7 +114,7 @@ fun buildQuicClientHello(
TlsExtension(TlsConstants.EXT_SIGNATURE_ALGORITHMS, encodeSignatureAlgorithms()),
TlsExtension(TlsConstants.EXT_KEY_SHARE, encodeKeyShareClientX25519(x25519PublicKey)),
TlsExtension(TlsConstants.EXT_PSK_KEY_EXCHANGE_MODES, encodePskKeyExchangeModesDhe()),
TlsExtension(TlsConstants.EXT_ALPN, encodeAlpn(alpn)),
TlsExtension(TlsConstants.EXT_ALPN, encodeAlpn(alpns)),
TlsExtension(TlsConstants.EXT_QUIC_TRANSPORT_PARAMETERS, quicTransportParams),
)
return TlsClientHello(random = random, cipherSuites = cipherSuites, extensions = exts)