@@ -289,6 +289,149 @@ fn decode_alpn_response(data []u8) !string {
289289 return list[1 ..].bytestr ()
290290}
291291
292+ // decode_alpn_offer parses a CLIENT's ALPN extension_data (RFC 7301 §3.1),
293+ // returning every protocol name offered, most preferred first, in wire
294+ // order. This is the multi-entry counterpart to decode_alpn_response
295+ // (this file) -- a server reads a client's full offer list with this
296+ // function, a client reads a server's single-entry selection with that
297+ // one; the two share a wire TYPE but not a wire SHAPE, the same class of
298+ // asymmetry tls13_server_hello.v documents for supported_versions/
299+ // key_share. RFC 7301 §3.1's own bounds are enforced: the list must not be
300+ // empty, and no entry may be empty (opaque ProtocolName<1..2^8-1>).
301+ pub fn decode_alpn_offer (data []u8 ) ! []string {
302+ if data.len < 2 {
303+ return error ('quic: ALPN extension_data too short: need at least 2 bytes, have ${data .len }' )
304+ }
305+ list_len := int ((u32 (data[0 ]) << 8 ) | u32 (data[1 ]))
306+ if 2 + list_len != data.len {
307+ return error ('quic: ALPN list_length ${list_len } does not match extension_data length ${data .len - 2 }' )
308+ }
309+ list := data[2 ..]
310+ if list.len == 0 {
311+ return error ('quic: ALPN ProtocolNameList must not be empty' )
312+ }
313+ mut protocols := []string {}
314+ mut cursor := 0
315+ for cursor < list.len {
316+ name_len := int (list[cursor])
317+ cursor + = 1
318+ if name_len == 0 {
319+ return error ('quic: ALPN protocol name must not be empty' )
320+ }
321+ if cursor + name_len > list.len {
322+ return error ('quic: ALPN protocol name declares ${name_len } bytes exceeding the remaining list' )
323+ }
324+ protocols << list[cursor..cursor + name_len].bytestr ()
325+ cursor + = name_len
326+ }
327+ return protocols
328+ }
329+
330+ // parse_supported_versions_from_client parses the CLIENT-side
331+ // supported_versions payload (RFC 8446 §4.2.1): a 1-byte length prefix
332+ // followed by that many bytes of 2-byte version codepoints -- the list
333+ // shape a client OFFERS, not the server's bare 2-byte selected_version
334+ // (see parse_supported_versions_from_server, tls13_server_hello.v, for
335+ // that shape -- the same asymmetry as key_share below).
336+ fn parse_supported_versions_from_client (data []u8 ) ! []u16 {
337+ if data.len < 1 {
338+ return error ('quic: supported_versions (client) truncated: need at least 1 byte, have ${data .len }' )
339+ }
340+ list_len := int (data[0 ])
341+ if 1 + list_len != data.len {
342+ return error ('quic: supported_versions (client) list length ${list_len } does not match remaining data ${data .len - 1 }' )
343+ }
344+ if list_len == 0 || list_len % 2 != 0 {
345+ return error ('quic: supported_versions (client) list length ${list_len } must be a non-zero, even number of bytes' )
346+ }
347+ mut versions := []u16 {}
348+ mut cursor := 1
349+ for cursor < data.len {
350+ versions << u16 ((u32 (data[cursor]) << 8 ) | u32 (data[cursor + 1 ]))
351+ cursor + = 2
352+ }
353+ return versions
354+ }
355+
356+ // ClientKeyShareEntry is one offered (group, key_exchange) pair from a
357+ // client's key_share extension.
358+ pub struct ClientKeyShareEntry {
359+ pub :
360+ group u16
361+ key_exchange []u8
362+ }
363+
364+ // parse_key_share_extension_client parses the CLIENT-side key_share payload
365+ // (RFC 8446 §4.2.8's KeyShareClientHello): a 2-byte client_shares length
366+ // prefix, then zero or more KeyShareEntry values (group(2) +
367+ // key_exchange_len(2) + key_exchange) -- the list shape a client OFFERS,
368+ // not a server's single bare KeyShareEntry (see
369+ // encode_key_share_extension_server, tls13_server_hello.v, for that
370+ // shape). This codebase's own build_client_hello only ever sends exactly
371+ // one entry, but the wire format itself permits any number, so this parses
372+ // the full list rather than assuming one.
373+ pub fn parse_key_share_extension_client (data []u8 ) ! []ClientKeyShareEntry {
374+ if data.len < 2 {
375+ return error ('quic: key_share (client) truncated: need at least 2 bytes, have ${data .len }' )
376+ }
377+ list_len := int ((u32 (data[0 ]) << 8 ) | u32 (data[1 ]))
378+ if 2 + list_len != data.len {
379+ return error ('quic: key_share (client) client_shares length ${list_len } does not match remaining data ${data .len - 2 }' )
380+ }
381+ mut entries := []ClientKeyShareEntry{}
382+ mut cursor := 2
383+ end := 2 + list_len
384+ for cursor < end {
385+ if end - cursor < 4 {
386+ return error ('quic: key_share (client) truncated KeyShareEntry header' )
387+ }
388+ group := u16 ((u32 (data[cursor]) << 8 ) | u32 (data[cursor + 1 ]))
389+ ke_len := int ((u32 (data[cursor + 2 ]) << 8 ) | u32 (data[cursor + 3 ]))
390+ cursor + = 4
391+ if cursor + ke_len > end {
392+ return error ('quic: key_share (client) KeyShareEntry declares ${ke_len }-byte key_exchange exceeding client_shares' )
393+ }
394+ if ke_len == 0 {
395+ return error ('quic: key_share (client) KeyShareEntry key_exchange must not be empty (opaque key_exchange<1..2^16-1>)' )
396+ }
397+ entries << ClientKeyShareEntry{
398+ group: group
399+ key_exchange: data[cursor..cursor + ke_len].clone ()
400+ }
401+ cursor + = ke_len
402+ }
403+ return entries
404+ }
405+
406+ // parse_signature_algorithms_extension_client parses a client's
407+ // signature_algorithms payload (RFC 8446 §4.2.3's
408+ // `SignatureScheme supported_signature_algorithms<2..2^16-2>`): a 2-byte
409+ // length prefix followed by that many bytes of 2-byte SignatureScheme
410+ // codepoints -- byte-identical framing to supported_groups's NamedGroupList
411+ // (parse_encrypted_extensions, tls13_server_hello.v, validates that inner
412+ // shape the same way), but kept as its own named function rather than a
413+ // shared generic helper, matching this module's established
414+ // one-function-per-RFC-field convention.
415+ fn parse_signature_algorithms_extension_client (data []u8 ) ! []u16 {
416+ if data.len < 2 {
417+ return error ('quic: signature_algorithms (client) truncated: need at least 2 bytes, have ${data .len }' )
418+ }
419+ list_len := int ((u32 (data[0 ]) << 8 ) | u32 (data[1 ]))
420+ if 2 + list_len != data.len {
421+ return error ('quic: signature_algorithms (client) list length ${list_len } does not match remaining data ${data .len - 2 }' )
422+ }
423+ if list_len == 0 || list_len % 2 != 0 {
424+ return error ('quic: signature_algorithms (client) list length ${list_len } must be a non-zero, even number of bytes' )
425+ }
426+ mut schemes := []u16 {}
427+ mut cursor := 2
428+ for cursor < data.len {
429+ schemes << u16 ((u32 (data[cursor]) << 8 ) | u32 (data[cursor + 1 ]))
430+ cursor + = 2
431+ }
432+ return schemes
433+ }
434+
292435// ClientHelloParams is everything build_client_hello needs beyond what's
293436// fixed by v1's scope decisions (single cipher suite, single named group,
294437// a fixed signature_algorithms list).
@@ -398,3 +541,102 @@ pub fn build_client_hello(p ClientHelloParams) ![]u8 {
398541
399542 return encode_handshake_message (.client_hello, body)!
400543}
544+
545+ // ParsedClientHello is the structural parse of a ClientHello (RFC 8446
546+ // §4.1.2), scoped like ParsedServerHello (tls13_server_hello.v): a few
547+ // RFC-mandated, caller-state-independent fields pulled out directly, plus
548+ // the raw extension list for a caller to interpret with the same
549+ // find_extension/decode_* helpers process_encrypted_extensions already
550+ // uses on the client side. cipher_suites is the FULL offered list (unlike
551+ // ParsedServerHello's single cipher_suite) -- a server must find its own
552+ // suite among possibly many, not just record what a peer already chose.
553+ pub struct ParsedClientHello {
554+ pub :
555+ random []u8
556+ cipher_suites []u16
557+ extensions []TlsExtension
558+ }
559+
560+ // parse_client_hello parses a ClientHello handshake message BODY. Validates
561+ // only what has no caller-dependent state: legacy_version, the
562+ // cipher_suites/legacy_compression_methods vector shapes, and
563+ // legacy_session_id -- RFC 9001 §8.4: "A server SHOULD treat the receipt of
564+ // a TLS ClientHello with a non-empty legacy_session_id field as a
565+ // connection error of type PROTOCOL_VIOLATION" (this file's own
566+ // build_client_hello doc comment already states the identical requirement
567+ // from the sending side; the QUIC-native PROTOCOL_VIOLATION code, not a TLS
568+ // alert, is why this uses error_with_code with quic_error_protocol_violation
569+ // directly rather than a TLS alert mapping). Extension-level semantic
570+ // validation (ALPN offered-list, key_share group, quic_transport_parameters
571+ // cross-checks, and rejecting the four server-only transport parameters a
572+ // client must never send) is the caller's job -- the same division of
573+ // labor process_encrypted_extensions already uses for EncryptedExtensions.
574+ pub fn parse_client_hello (body []u8 ) ! ParsedClientHello {
575+ if body.len < 2 + 32 + 1 {
576+ return error ('quic: truncated ClientHello: need at least 35 bytes for the fixed prefix, have ${body .len }' )
577+ }
578+ if body[0 ] != 0x03 || body[1 ] != 0x03 {
579+ return error ('quic: ClientHello legacy_version must be 0x0303, got 0x${body [0 ]:02 x }${body [1 ]:02 x }' )
580+ }
581+ random := body[2 ..34 ].clone ()
582+ mut cursor := 34
583+ session_id_len := int (body[cursor])
584+ cursor + = 1
585+ if session_id_len != 0 {
586+ return error_with_code ('quic: ClientHello legacy_session_id must be empty (RFC 9001 §8.4)' ,
587+ int (quic_error_protocol_violation))
588+ }
589+
590+ if body.len < cursor + 2 {
591+ return error ('quic: truncated ClientHello after legacy_session_id' )
592+ }
593+ suites_len := int ((u32 (body[cursor]) << 8 ) | u32 (body[cursor + 1 ]))
594+ cursor + = 2
595+ if suites_len == 0 || suites_len % 2 != 0 {
596+ return error ('quic: ClientHello cipher_suites length ${suites_len } must be a non-zero, even number of bytes' )
597+ }
598+ if body.len < cursor + suites_len {
599+ return error ('quic: truncated ClientHello cipher_suites: declares ${suites_len } bytes, only ${body .len - cursor } remain' )
600+ }
601+ mut cipher_suites := []u16 {}
602+ mut suite_cursor := cursor
603+ for suite_cursor < cursor + suites_len {
604+ cipher_suites << u16 ((u32 (body[suite_cursor]) << 8 ) | u32 (body[suite_cursor + 1 ]))
605+ suite_cursor + = 2
606+ }
607+ cursor + = suites_len
608+
609+ if body.len < cursor + 1 {
610+ return error ('quic: truncated ClientHello: missing legacy_compression_methods' )
611+ }
612+ compression_len := int (body[cursor])
613+ cursor + = 1
614+ if body.len < cursor + compression_len {
615+ return error ('quic: truncated ClientHello legacy_compression_methods' )
616+ }
617+ // RFC 8446 §4.1.2: "For every TLS 1.3 ClientHello, this vector MUST
618+ // contain exactly one byte, set to zero" -- the offering side's mirror
619+ // of parse_server_hello's fixed single-byte legacy_compression_method
620+ // check (a server only ever selects, never offers, one, so that side
621+ // has no vector wrapper at all).
622+ if compression_len != 1 || body[cursor] != 0 {
623+ return error ('quic: ClientHello legacy_compression_methods must be exactly [0], got ${compression_len } bytes' )
624+ }
625+ cursor + = compression_len
626+
627+ if body.len < cursor + 2 {
628+ return error ('quic: truncated ClientHello: missing extensions length' )
629+ }
630+ extensions_len := int ((u32 (body[cursor]) << 8 ) | u32 (body[cursor + 1 ]))
631+ cursor + = 2
632+ if cursor + extensions_len != body.len {
633+ return error ('quic: ClientHello extensions length ${extensions_len } does not match remaining body ${body .len - cursor }' )
634+ }
635+ extensions := parse_extension_list (body[cursor..])!
636+
637+ return ParsedClientHello{
638+ random: random
639+ cipher_suites: cipher_suites
640+ extensions: extensions
641+ }
642+ }
0 commit comments