|
| 1 | +module quic |
| 2 | + |
| 3 | +import crypto.ecdsa |
| 4 | +import crypto.rand |
| 5 | + |
| 6 | +// accept.v: the server-role counterpart to dial() (conn.v). Split into its |
| 7 | +// own file rather than added to conn.v directly for one purely mechanical |
| 8 | +// reason: this file needs crypto.rand (a real CSPRNG) for the server's own |
| 9 | +// connection ID and ServerHello random, and conn.v already imports the |
| 10 | +// plain `rand` module (non-cryptographic, used by dial() -- a known, |
| 11 | +// separately-tracked gap, not repeated here) under the same bare name -- |
| 12 | +// V requires disambiguating two same-named imports, and a fresh file needing |
| 13 | +// only the secure one is simpler than an alias in an already-large file. |
| 14 | +// |
| 15 | +// Everything else server-role lives in conn.v itself (the struct fields, |
| 16 | +// dispatch_handshake_message's role branch, the own/peer_*_keys and |
| 17 | +// is_handshake_confirmed role-aware helpers, drain_outgoing's HANDSHAKE_DONE |
| 18 | +// send) -- accept() is deliberately thin: it does just enough to construct |
| 19 | +// a QuicConn with the right identity (original_dcid/dcid/scid/peer_scid) |
| 20 | +// and Initial-space keys to make the FIRST incoming datagram decryptable, |
| 21 | +// then hands that exact datagram to poll() -- the same, already-tested |
| 22 | +// entry point every SUBSEQUENT datagram goes through. Everything else |
| 23 | +// (decrypting, reassembling the ClientHello's CRYPTO frame(s), running |
| 24 | +// Tls13ServerHandshake.respond_to_client_hello, deriving Handshake/ |
| 25 | +// Application keys, queuing the response flight, and draining it into |
| 26 | +// actual outgoing datagrams) happens exactly where it already happens for |
| 27 | +// every OTHER handshake message, via dispatch_handshake_message's own |
| 28 | +// server branch -- there is no separate, parallel decrypt/dispatch path |
| 29 | +// here to keep in sync with process_initial_or_handshake's. |
| 30 | + |
| 31 | +// AcceptParams is everything accept() needs beyond what it decides for |
| 32 | +// itself (this server's own connection ID, ServerHello random) -- the |
| 33 | +// server-role mirror of DialParams. |
| 34 | +pub struct AcceptParams { |
| 35 | +pub: |
| 36 | + // This server's own OFFERED transport parameters. accept() overrides |
| 37 | + // initial_source_connection_id and original_destination_connection_id |
| 38 | + // with values it derives itself (RFC 9000 §7.3) regardless of what the |
| 39 | + // caller set there, the same override dial() already applies to its own |
| 40 | + // initial_source_connection_id. |
| 41 | + transport_parameters QuicTransportParameters |
| 42 | + // Application protocols this server supports, most preferred first |
| 43 | + // (RFC 7301 §3.2 -- the server picks, in ITS OWN preference order, from |
| 44 | + // among what the client also offered). Named to match DialParams' |
| 45 | + // alpn_protocols field, even though ServerHandshakeParams' own |
| 46 | + // equivalent field is named supported_alpn_protocols. |
| 47 | + alpn_protocols []string |
| 48 | + // This server's own certificate chain, leaf-first, and the long-lived |
| 49 | + // private key matching the leaf's public key -- see |
| 50 | + // ServerHandshakeParams' own doc comments (tls13_server_handshake.v) |
| 51 | + // for what accept() does NOT own here (loading these from a PEM file is |
| 52 | + // a future caller's job, same scope note that struct already states). |
| 53 | + certificate_chain []CertificateEntry |
| 54 | + signing_key ecdsa.PrivateKey |
| 55 | +} |
| 56 | + |
| 57 | +// accept constructs a new server-role QuicConn from `raw_datagram` -- the |
| 58 | +// first UDP datagram of a new connection attempt, expected to contain |
| 59 | +// exactly one Initial packet carrying the client's ClientHello (this |
| 60 | +// codebase's own dial() never sends anything else in its first flight; |
| 61 | +// see the loop below for what happens if that assumption doesn't hold). |
| 62 | +// Returns the new connection AND the PollResult from processing that same |
| 63 | +// datagram through it -- typically the response flight (ServerHello under |
| 64 | +// Initial protection, EncryptedExtensions..this server's own Finished |
| 65 | +// under Handshake protection) queued in PollResult.outgoing, ready for the |
| 66 | +// caller to actually send. |
| 67 | +// |
| 68 | +// Deliberately does NOT decide whether to accept directly or send a Retry |
| 69 | +// first (RFC 9000 §8.1's address-validation policy) -- that decision needs |
| 70 | +// state (has this source address been seen before? is the anti- |
| 71 | +// amplification budget already exhausted?) that only a caller tracking |
| 72 | +// MANY connection attempts across MANY source addresses can have; a single |
| 73 | +// accept() call has no such context. 13b's encode_retry_packet/ |
| 74 | +// generate_retry_token/AntiAmplificationLimiter already exist for a caller |
| 75 | +// to make and act on that decision BEFORE ever calling accept() -- wiring |
| 76 | +// them together is 13d-2's job (the UDP listener), not this constructor's. |
| 77 | +// |
| 78 | +// KNOWN SCOPE LIMIT, not fixed here: the server's Handshake-space CRYPTO |
| 79 | +// flight (EncryptedExtensions+Certificate+CertificateVerify+Finished, |
| 80 | +// dispatch_handshake_message's server bootstrap branch queues it as one |
| 81 | +// blob in pending_handshake_crypto) is flushed by drain_outgoing's |
| 82 | +// pre-existing logic as a SINGLE CRYPTO frame in a SINGLE Handshake packet |
| 83 | +// -- correct for this repo's own small test certificate, but not |
| 84 | +// fragmented across multiple packets if a real-world certificate chain |
| 85 | +// doesn't fit one packet's payload. dial()'s own ClientHello flush has the |
| 86 | +// identical shape but was never previously exercised with anything large |
| 87 | +// enough to expose it, since a ClientHello (no certificate) is always |
| 88 | +// small. Splitting a CRYPTO stream write across multiple packets is a |
| 89 | +// real, separate piece of work, not attempted here. |
| 90 | +pub fn accept(raw_datagram []u8, params AcceptParams, now u64) !(&QuicConn, PollResult) { |
| 91 | + // RFC 9000 §14.1: "A server MUST discard an Initial packet that is |
| 92 | + // carried in a UDP datagram with a payload that is smaller than the |
| 93 | + // smallest allowed maximum datagram size of 1200 bytes" -- an |
| 94 | + // anti-amplification measure: without this, a spoofed-source, undersized |
| 95 | + // trigger datagram gets a full ServerHello+EncryptedExtensions+ |
| 96 | + // Certificate+CertificateVerify+Finished response flight, routinely |
| 97 | + // several times larger than the trigger, aimed at whatever address the |
| 98 | + // attacker claimed. coalesce.v's split_coalesced_datagram deliberately |
| 99 | + // does NOT enforce this itself (see its own doc comment) -- it's a |
| 100 | + // stateless, role-agnostic splitter also used for datagrams this |
| 101 | + // endpoint SENDS, where a legitimate reply smaller than 1200 bytes |
| 102 | + // (e.g. ACK-only) is allowed; that comment explicitly defers the real, |
| 103 | + // role-aware check to "a later phase with that visibility," which is |
| 104 | + // this one: the single call site that always knows a datagram reaching |
| 105 | + // it is being RECEIVED, by a SERVER, before address validation. Found |
| 106 | + // by 13d-1's adversarial review (v-quality lens). |
| 107 | + if raw_datagram.len < min_initial_datagram_size { |
| 108 | + return error('quic: accept: datagram (${raw_datagram.len} bytes) is smaller than the RFC 9000 §14.1 anti-amplification floor of ${min_initial_datagram_size} bytes') |
| 109 | + } |
| 110 | + packets := split_coalesced_datagram(raw_datagram)! |
| 111 | + mut initial_header := ?QuicLongHeader(none) |
| 112 | + for p in packets { |
| 113 | + if p.form != .long { |
| 114 | + continue |
| 115 | + } |
| 116 | + h, _ := parse_long_header(p.bytes) or { continue } |
| 117 | + if h.typ == .initial { |
| 118 | + initial_header = h |
| 119 | + break |
| 120 | + } |
| 121 | + } |
| 122 | + header := initial_header or { |
| 123 | + return error('quic: accept: raw_datagram contains no Initial packet') |
| 124 | + } |
| 125 | + |
| 126 | + // header.dcid is the client's freshly-chosen original_dcid -- RFC 9001 |
| 127 | + // §5.2 derives Initial secrets from it, identically on both sides (the |
| 128 | + // same derive_initial_secrets dial() itself calls for the client half |
| 129 | + // of this exact derivation). |
| 130 | + initial_secrets := derive_initial_secrets(header.dcid)! |
| 131 | + initial_keys_client := derive_packet_protection_keys(initial_secrets.client)! |
| 132 | + initial_keys_server := derive_packet_protection_keys(initial_secrets.server)! |
| 133 | + |
| 134 | + scid := rand.bytes(local_cid_len) or { |
| 135 | + return error('quic: accept: failed to generate this server\'s own connection ID: ${err.msg()}') |
| 136 | + } |
| 137 | + server_hello_random := rand.bytes(32) or { |
| 138 | + return error('quic: accept: failed to generate ServerHello random: ${err.msg()}') |
| 139 | + } |
| 140 | + |
| 141 | + mut own_params := params.transport_parameters |
| 142 | + own_params.initial_source_connection_id = scid.clone() |
| 143 | + // RFC 9000 §7.3: a server MUST send original_destination_connection_id, |
| 144 | + // echoing the DCID the client's own Initial packet used -- the value |
| 145 | + // ServerHandshakeParams' own doc comment names as this caller's |
| 146 | + // responsibility to set (build_encrypted_extensions itself validates |
| 147 | + // it's present, but does not know what value to fill in). |
| 148 | + own_params.original_destination_connection_id = header.dcid.clone() |
| 149 | + |
| 150 | + own_max_idle_timeout_ms := own_params.max_idle_timeout or { u64(0) } |
| 151 | + |
| 152 | + mut c := &QuicConn{ |
| 153 | + role: .server |
| 154 | + state: .handshaking |
| 155 | + original_dcid: header.dcid.clone() |
| 156 | + dcid: header.scid.clone() |
| 157 | + scid: scid |
| 158 | + peer_scid: header.scid.clone() |
| 159 | + token: []u8{} |
| 160 | + server_handshake: none |
| 161 | + server_accept_params: ServerHandshakeParams{ |
| 162 | + transport_parameters: own_params |
| 163 | + supported_alpn_protocols: params.alpn_protocols |
| 164 | + certificate_chain: params.certificate_chain |
| 165 | + signing_key: params.signing_key |
| 166 | + server_hello_random: server_hello_random |
| 167 | + } |
| 168 | + handshake_completion: new_handshake_completion_state() |
| 169 | + pn_spaces: new_packet_number_spaces() |
| 170 | + initial_keys_client: initial_keys_client |
| 171 | + initial_keys_server: initial_keys_server |
| 172 | + initial_crypto: new_crypto_stream_reassembler() |
| 173 | + handshake_crypto: new_crypto_stream_reassembler() |
| 174 | + loss_detection: new_quic_loss_detection_timer() |
| 175 | + congestion_control: new_newreno_congestion_control() |
| 176 | + own_max_idle_timeout_ms: own_max_idle_timeout_ms |
| 177 | + stateless_reset: new_stateless_reset_tracker() |
| 178 | + connection_start: now |
| 179 | + own_transport_parameters: own_params |
| 180 | + streams: new_quic_stream_set(.server) |
| 181 | + conn_send_window: new_flow_control_window(0) |
| 182 | + conn_recv_window: new_receive_window(own_params.initial_max_data or { u64(0) }) |
| 183 | + local_max_streams_bidi: own_params.initial_max_streams_bidi or { u64(0) } |
| 184 | + local_max_streams_uni: own_params.initial_max_streams_uni or { u64(0) } |
| 185 | + } |
| 186 | + |
| 187 | + result := c.poll(raw_datagram, now)! |
| 188 | + return c, result |
| 189 | +} |
0 commit comments