Skip to content

Commit 7b45b39

Browse files
author
Richard Wheeler
committed
net.quic: 13a - ServerHello + EncryptedExtensions construction
First slice of the TLS 1.3 server handshake (Phase 13a): build_server_hello and build_encrypted_extensions, the server-role mirror of the existing client-role build_client_hello. Both round-trip through this file's own parse_server_hello/parse_encrypted_extensions, which is a real cross-check since build and parse were each derived independently from the RFC 8446 text rather than from each other. Caught one real bug via the round-trip test before committing: a server's key_share extension is a bare KeyShareEntry (RFC 8446 SS4.2.8), not the list-wrapped KeyShareClientHello shape build_client_hello's own encode_key_share_extension produces -- needed a dedicated encode_key_share_extension_server. Same asymmetry already existed and was handled correctly for supported_versions (bare 2 bytes server-side vs. a length-prefixed list client-side); this is the identical class for key_share, not a new discovery about the protocol. build_encrypted_extensions enforces the two mandatory transport parameters RFC 9000 SS7.3 requires an endpoint (initial_source_connection_id) and a server specifically (original_destination_connection_id) to always include, mirroring build_client_hello's identical enforcement of its own mandatory field. Still to come within 13a: Certificate presentation, CertificateVerify signing, server Finished, HelloRetryRequest generation.
1 parent 4165068 commit 7b45b39

2 files changed

Lines changed: 308 additions & 0 deletions

File tree

vlib/net/quic/tls13_server_hello.v

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,182 @@ pub fn parse_server_hello(body []u8) !ServerHelloMessage {
302302
}
303303
}
304304

305+
// encode_key_share_extension_server encodes the SERVER-side key_share
306+
// payload (RFC 8446 §4.2.8): a BARE KeyShareEntry (group(2) +
307+
// key_exchange_len(2) + key_exchange), with no outer list-length wrapper.
308+
// This is NOT the same shape as tls13_client_hello.v's
309+
// encode_key_share_extension, which wraps its entry in an extra
310+
// client_shares<0..2^16-1> list-length prefix -- the two directions share an
311+
// extension type but not a wire shape, the same asymmetry
312+
// encode_supported_versions_extension_server documents for
313+
// supported_versions. Confirmed against parse_server_hello's own parsing
314+
// (this file), which reads ks_ext.data[0..2] directly as the group with no
315+
// list-length prefix to skip first.
316+
fn encode_key_share_extension_server(group u16, key_exchange []u8) ![]u8 {
317+
if key_exchange.len == 0 || key_exchange.len > 0xffff - 4 {
318+
return error('quic: key_exchange length ${key_exchange.len} out of range')
319+
}
320+
mut data := []u8{}
321+
data << u8(group >> 8)
322+
data << u8(group)
323+
data << u8(key_exchange.len >> 8)
324+
data << u8(key_exchange.len)
325+
data << key_exchange
326+
return encode_extension(ext_key_share, data)
327+
}
328+
329+
// encode_supported_versions_extension_server encodes the SERVER-side
330+
// supported_versions payload (RFC 8446 §4.2.1): a bare 2-byte
331+
// selected_version, not the ClientHello's length-prefixed version list --
332+
// see parse_supported_versions_from_server's identical distinction on the
333+
// parse side. v1 only ever selects TLS 1.3, matching the single version
334+
// build_client_hello offers.
335+
fn encode_supported_versions_extension_server() ![]u8 {
336+
mut data := []u8{}
337+
data << u8(tls_version_1_3 >> 8)
338+
data << u8(tls_version_1_3)
339+
return encode_extension(ext_supported_versions, data)
340+
}
341+
342+
// ServerHelloParams is everything build_server_hello needs beyond what's
343+
// fixed by v1's scope decisions (single cipher suite, single selected
344+
// version, single named group).
345+
pub struct ServerHelloParams {
346+
pub:
347+
// Exactly 32 bytes. Caller supplies so a real caller can use a genuine
348+
// CSPRNG while tests stay deterministic -- same convention as
349+
// ClientHelloParams.random. MUST NOT equal the RFC 8446 §4.1.3 magic
350+
// HelloRetryRequest value; a caller that wants to send an HRR uses
351+
// build_hello_retry_request (below) instead, never this function with a
352+
// hand-picked random.
353+
random []u8
354+
// This SERVER's own ephemeral ECDHE public key for the selected group
355+
// (Phase 1 PublicKey.uncompressed_bytes() output, 65 bytes for P-256).
356+
// v1 only ever selects named_group_secp256r1, matching the single group
357+
// build_client_hello offers -- a real caller has already confirmed the
358+
// ClientHello's own key_share offered this group before calling here.
359+
ecdhe_public_key []u8
360+
}
361+
362+
// build_server_hello constructs a complete, real (non-HelloRetryRequest)
363+
// TLS 1.3 ServerHello handshake message (RFC 8446 §4.1.3), framed via
364+
// encode_handshake_message. Sends exactly two extensions: supported_versions
365+
// and key_share -- the full server_hello_allowed set this same file's
366+
// parse_server_hello enforces on the client side, kept in sync by
367+
// construction rather than duplicated as a separate list. legacy_session_id
368+
// is always echoed as empty: RFC 9001 §8.4 states a server "SHOULD treat the
369+
// receipt of a TLS ClientHello with a non-empty legacy_session_id field as a
370+
// connection error" -- a spec-compliant server that reached this point has
371+
// already rejected any handshake where the client sent a non-empty session
372+
// ID, so there is never a non-empty value to echo back.
373+
pub fn build_server_hello(p ServerHelloParams) ![]u8 {
374+
if p.random.len != 32 {
375+
return error('quic: ServerHello random must be exactly 32 bytes, got ${p.random.len}')
376+
}
377+
if p.random == hello_retry_request_random[..] {
378+
return error('quic: ServerHello random must not equal the RFC 8446 §4.1.3 HelloRetryRequest magic value -- use build_hello_retry_request to send an HRR')
379+
}
380+
381+
mut body := []u8{}
382+
// legacy_version MUST be 0x0303 (RFC 8446 §4.1.3), matching
383+
// build_client_hello's identical fixed value -- the real version is
384+
// negotiated via supported_versions below.
385+
body << u8(0x03)
386+
body << u8(0x03)
387+
body << p.random
388+
// legacy_session_id_echo: always empty, see the doc comment above.
389+
body << u8(0)
390+
body << u8(cipher_suite_tls_aes_128_gcm_sha256 >> 8)
391+
body << u8(cipher_suite_tls_aes_128_gcm_sha256)
392+
// legacy_compression_method MUST be 0 (null), RFC 8446 §4.1.3.
393+
body << u8(0)
394+
395+
mut extensions := []u8{}
396+
extensions << encode_key_share_extension_server(named_group_secp256r1, p.ecdhe_public_key)!
397+
extensions << encode_supported_versions_extension_server()!
398+
399+
if extensions.len > 0xffff {
400+
return error('quic: ServerHello extensions block too large: ${extensions.len} bytes')
401+
}
402+
body << u8(extensions.len >> 8)
403+
body << u8(extensions.len)
404+
body << extensions
405+
406+
return encode_handshake_message(.server_hello, body)!
407+
}
408+
409+
// EncryptedExtensionsParams is everything build_encrypted_extensions needs.
410+
pub struct EncryptedExtensionsParams {
411+
pub:
412+
// This SERVER's own transport parameters (RFC 9001 §8.2). Every field is
413+
// the server's own value, not the client's -- e.g.
414+
// initial_max_stream_data_bidi_local/_remote here describe streams from
415+
// THIS server's perspective, resolved the same way flow_control.v's
416+
// initial_send_limit_for_stream/initial_receive_limit_for_stream already
417+
// do for the connection's actual flow-control windows.
418+
transport_parameters QuicTransportParameters
419+
// The single protocol this server selected from the client's ALPN offer
420+
// list (RFC 7301 §3.2: "the server SHALL include only one protocol name
421+
// in the ProtocolNameList"). Never empty -- a server with no matching
422+
// protocol MUST fail the handshake with no_application_protocol (RFC
423+
// 9001 §8.1) rather than reach this function at all.
424+
selected_alpn string
425+
// True when the ClientHello carried a server_name extension this server
426+
// wants to acknowledge. RFC 6066 §3: the acknowledgement's
427+
// extension_data is always empty -- this server never echoes the
428+
// hostname back, matching what parse_encrypted_extensions (this same
429+
// file, client-role) already requires of a peer.
430+
acknowledge_server_name bool
431+
}
432+
433+
// build_encrypted_extensions constructs a complete EncryptedExtensions
434+
// handshake message (RFC 8446 §4.3.1: a length-prefixed extension list,
435+
// nothing else), framed via encode_handshake_message. Sends only extensions
436+
// this client's own parse_encrypted_extensions (this same file) actually
437+
// permits -- alpn and quic_transport_parameters unconditionally,
438+
// server_name only when acknowledging one. supported_groups is
439+
// deliberately never sent: v1 offers no session resumption or 0-RTT (Phase
440+
// 14, out of scope), so there is nothing for a future-connection group hint
441+
// to usefully inform.
442+
pub fn build_encrypted_extensions(p EncryptedExtensionsParams) ![]u8 {
443+
if p.selected_alpn.len == 0 {
444+
return error('quic: EncryptedExtensions must select exactly one ALPN protocol (RFC 7301 §3.2) -- a server with no match must fail the handshake before reaching here (RFC 9001 §8.1)')
445+
}
446+
// RFC 9000 §7.3 / §18.2: "An endpoint MUST treat the absence of the
447+
// initial_source_connection_id transport parameter from either endpoint
448+
// ... as a connection error of type TRANSPORT_PARAMETER_ERROR" -- this
449+
// server's own SCID choice, mirroring build_client_hello's identical
450+
// check for the client's own value.
451+
if p.transport_parameters.initial_source_connection_id == none {
452+
return error('quic: EncryptedExtensions transport parameters must include initial_source_connection_id (RFC 9000 §7.3)')
453+
}
454+
// RFC 9000 §7.3/§18.2: "...or the absence of the
455+
// original_destination_connection_id transport parameter from the
456+
// server as a connection error of type TRANSPORT_PARAMETER_ERROR" --
457+
// unlike initial_source_connection_id, this one is server-only and has
458+
// no client-side analog to mirror.
459+
if p.transport_parameters.original_destination_connection_id == none {
460+
return error('quic: EncryptedExtensions transport parameters must include original_destination_connection_id (RFC 9000 §7.3, server-only)')
461+
}
462+
463+
mut extensions := []u8{}
464+
if p.acknowledge_server_name {
465+
extensions << encode_extension(ext_server_name, []u8{})!
466+
}
467+
extensions << encode_alpn_extension([p.selected_alpn])!
468+
extensions << encode_quic_transport_parameters_extension(p.transport_parameters)!
469+
470+
if extensions.len > 0xffff {
471+
return error('quic: EncryptedExtensions block too large: ${extensions.len} bytes')
472+
}
473+
mut body := []u8{}
474+
body << u8(extensions.len >> 8)
475+
body << u8(extensions.len)
476+
body << extensions
477+
478+
return encode_handshake_message(.encrypted_extensions, body)!
479+
}
480+
305481
// encrypted_extensions_allowed is the intersection of RFC 8446 §4.2's own
306482
// per-message applicability table (only server_name, max_fragment_length,
307483
// supported_groups, use_srtp, heartbeat, alpn, client_certificate_type,

vlib/net/quic/tls13_server_hello_test.v

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,3 +485,135 @@ fn test_parse_server_hello_hello_retry_request_rejects_unsolicited_extension() {
485485
}
486486
assert false, 'expected an error for an unsolicited alpn extension in HelloRetryRequest'
487487
}
488+
489+
// build_server_hello / build_encrypted_extensions (Phase 13a, server-role
490+
// construction). Round-tripped through this same file's own parse
491+
// functions, which is a real cross-check: build_server_hello and
492+
// parse_server_hello are independently written against the RFC text, not
493+
// against each other, so a round trip that only ever validated the wire
494+
// shape it itself produced would prove nothing.
495+
496+
fn test_build_server_hello_round_trips_through_parse_server_hello() {
497+
random := []u8{len: 32, init: 0x11}
498+
key := []u8{len: 65, init: 0x04}
499+
msg := build_server_hello(random: random, ecdhe_public_key: key)!
500+
parsed_msg, consumed := parse_handshake_message(msg)!
501+
assert consumed == msg.len
502+
assert parsed_msg.typ == .server_hello
503+
result := parse_server_hello(parsed_msg.body)!
504+
match result {
505+
ParsedServerHello {
506+
assert result.random == random
507+
assert result.cipher_suite == cipher_suite_tls_aes_128_gcm_sha256
508+
assert result.selected_version == tls_version_1_3
509+
assert result.key_share_group == named_group_secp256r1
510+
assert result.key_share_key_exchange == key
511+
assert result.extensions.len == 2
512+
}
513+
ParsedHelloRetryRequest {
514+
assert false, 'expected a real ServerHello, not HRR'
515+
}
516+
}
517+
}
518+
519+
fn test_build_server_hello_rejects_wrong_random_length() {
520+
build_server_hello(random: []u8{len: 31}, ecdhe_public_key: []u8{len: 65}) or {
521+
assert err.msg().contains('32 bytes')
522+
return
523+
}
524+
assert false, 'expected an error for a 31-byte random'
525+
}
526+
527+
// A real caller must never be able to accidentally produce a ServerHello
528+
// that a peer would interpret as a HelloRetryRequest -- the two share a
529+
// wire type, distinguished ONLY by this exact 32-byte value (RFC 8446
530+
// §4.1.3), so colliding with it by construction (e.g. a broken RNG, or a
531+
// test fixture reusing the constant) must be caught here rather than
532+
// silently producing an ambiguous message.
533+
fn test_build_server_hello_rejects_hello_retry_request_random_collision() {
534+
build_server_hello(random: hello_retry_request_random[..].clone(), ecdhe_public_key: []u8{len: 65}) or {
535+
assert err.msg().contains('HelloRetryRequest')
536+
return
537+
}
538+
assert false, 'expected an error when random collides with the HelloRetryRequest magic value'
539+
}
540+
541+
fn test_build_encrypted_extensions_round_trips_through_parse_encrypted_extensions() {
542+
params := QuicTransportParameters{
543+
initial_source_connection_id: []u8{len: 8, init: 0xaa}
544+
original_destination_connection_id: []u8{len: 8, init: 0xbb}
545+
}
546+
msg := build_encrypted_extensions(
547+
transport_parameters: params
548+
selected_alpn: 'h3'
549+
acknowledge_server_name: true
550+
)!
551+
parsed_msg, consumed := parse_handshake_message(msg)!
552+
assert consumed == msg.len
553+
assert parsed_msg.typ == .encrypted_extensions
554+
extensions := parse_encrypted_extensions(parsed_msg.body)!
555+
assert extensions.len == 3
556+
557+
sn := find_extension(extensions, ext_server_name) or { panic('missing server_name') }
558+
assert sn.data.len == 0
559+
560+
alpn_ext := find_extension(extensions, ext_alpn) or { panic('missing alpn') }
561+
assert decode_alpn_response(alpn_ext.data)! == 'h3'
562+
563+
tp_ext := find_extension(extensions, ext_quic_transport_parameters) or {
564+
panic('missing quic_transport_parameters')
565+
}
566+
decoded := decode_transport_parameters(tp_ext.data)!
567+
assert decoded.initial_source_connection_id? == []u8{len: 8, init: 0xaa}
568+
assert decoded.original_destination_connection_id? == []u8{len: 8, init: 0xbb}
569+
}
570+
571+
fn test_build_encrypted_extensions_omits_server_name_when_not_acknowledging() {
572+
params := QuicTransportParameters{
573+
initial_source_connection_id: []u8{len: 8, init: 0xaa}
574+
original_destination_connection_id: []u8{len: 8, init: 0xbb}
575+
}
576+
msg := build_encrypted_extensions(transport_parameters: params, selected_alpn: 'h3')!
577+
_, consumed := parse_handshake_message(msg)!
578+
assert consumed == msg.len
579+
parsed_msg, _ := parse_handshake_message(msg)!
580+
extensions := parse_encrypted_extensions(parsed_msg.body)!
581+
assert extensions.len == 2
582+
if _ := find_extension(extensions, ext_server_name) {
583+
assert false, 'server_name must be absent when acknowledge_server_name is false'
584+
}
585+
}
586+
587+
fn test_build_encrypted_extensions_requires_selected_alpn() {
588+
params := QuicTransportParameters{
589+
initial_source_connection_id: []u8{len: 8, init: 0xaa}
590+
original_destination_connection_id: []u8{len: 8, init: 0xbb}
591+
}
592+
build_encrypted_extensions(transport_parameters: params, selected_alpn: '') or {
593+
assert err.msg().contains('ALPN')
594+
return
595+
}
596+
assert false, 'expected an error for an empty selected_alpn'
597+
}
598+
599+
fn test_build_encrypted_extensions_requires_initial_source_connection_id() {
600+
params := QuicTransportParameters{
601+
original_destination_connection_id: []u8{len: 8, init: 0xbb}
602+
}
603+
build_encrypted_extensions(transport_parameters: params, selected_alpn: 'h3') or {
604+
assert err.msg().contains('initial_source_connection_id')
605+
return
606+
}
607+
assert false, 'expected an error for a missing initial_source_connection_id'
608+
}
609+
610+
fn test_build_encrypted_extensions_requires_original_destination_connection_id() {
611+
params := QuicTransportParameters{
612+
initial_source_connection_id: []u8{len: 8, init: 0xaa}
613+
}
614+
build_encrypted_extensions(transport_parameters: params, selected_alpn: 'h3') or {
615+
assert err.msg().contains('original_destination_connection_id')
616+
return
617+
}
618+
assert false, 'expected an error for a missing original_destination_connection_id'
619+
}

0 commit comments

Comments
 (0)