1 | use alloc::boxed::Box; |
2 | use alloc::vec; |
3 | use alloc::vec::Vec; |
4 | |
5 | use pki_types::{DnsName, EchConfigListBytes, ServerName}; |
6 | use subtle::ConstantTimeEq; |
7 | |
8 | use crate::CipherSuite::TLS_EMPTY_RENEGOTIATION_INFO_SCSV; |
9 | use crate::client::tls13; |
10 | use crate::crypto::SecureRandom; |
11 | use crate::crypto::hash::Hash; |
12 | use crate::crypto::hpke::{EncapsulatedSecret, Hpke, HpkePublicKey, HpkeSealer, HpkeSuite}; |
13 | use crate::hash_hs::{HandshakeHash, HandshakeHashBuffer}; |
14 | use crate::log::{debug, trace, warn}; |
15 | use crate::msgs::base::{Payload, PayloadU16}; |
16 | use crate::msgs::codec::{Codec, Reader}; |
17 | use crate::msgs::enums::{ExtensionType, HpkeKem}; |
18 | use crate::msgs::handshake::{ |
19 | ClientExtension, ClientHelloPayload, EchConfigContents, EchConfigPayload, Encoding, |
20 | EncryptedClientHello, EncryptedClientHelloOuter, HandshakeMessagePayload, HandshakePayload, |
21 | HelloRetryRequest, HpkeKeyConfig, HpkeSymmetricCipherSuite, PresharedKeyBinder, |
22 | PresharedKeyOffer, Random, ServerHelloPayload, |
23 | }; |
24 | use crate::msgs::message::{Message, MessagePayload}; |
25 | use crate::msgs::persist; |
26 | use crate::msgs::persist::Retrieved; |
27 | use crate::tls13::key_schedule::{ |
28 | KeyScheduleEarly, KeyScheduleHandshakeStart, server_ech_hrr_confirmation_secret, |
29 | }; |
30 | use crate::{ |
31 | AlertDescription, CommonState, EncryptedClientHelloError, Error, HandshakeType, |
32 | PeerIncompatible, PeerMisbehaved, ProtocolVersion, Tls13CipherSuite, |
33 | }; |
34 | |
35 | /// Controls how Encrypted Client Hello (ECH) is used in a client handshake. |
36 | #[derive (Clone, Debug)] |
37 | pub enum EchMode { |
38 | /// ECH is enabled and the ClientHello will be encrypted based on the provided |
39 | /// configuration. |
40 | Enable(EchConfig), |
41 | |
42 | /// No ECH configuration is available but the client should act as though it were. |
43 | /// |
44 | /// This is an anti-ossification measure, sometimes referred to as "GREASE"[^0]. |
45 | /// [^0]: <https://www.rfc-editor.org/rfc/rfc8701> |
46 | Grease(EchGreaseConfig), |
47 | } |
48 | |
49 | impl EchMode { |
50 | /// Returns true if the ECH mode will use a FIPS approved HPKE suite. |
51 | pub fn fips(&self) -> bool { |
52 | match self { |
53 | Self::Enable(ech_config: &EchConfig) => ech_config.suite.fips(), |
54 | Self::Grease(grease_config: &EchGreaseConfig) => grease_config.suite.fips(), |
55 | } |
56 | } |
57 | } |
58 | |
59 | impl From<EchConfig> for EchMode { |
60 | fn from(config: EchConfig) -> Self { |
61 | Self::Enable(config) |
62 | } |
63 | } |
64 | |
65 | impl From<EchGreaseConfig> for EchMode { |
66 | fn from(config: EchGreaseConfig) -> Self { |
67 | Self::Grease(config) |
68 | } |
69 | } |
70 | |
71 | /// Configuration for performing encrypted client hello. |
72 | /// |
73 | /// Note: differs from the protocol-encoded EchConfig (`EchConfigMsg`). |
74 | #[derive (Clone, Debug)] |
75 | pub struct EchConfig { |
76 | /// The selected EchConfig. |
77 | pub(crate) config: EchConfigPayload, |
78 | |
79 | /// An HPKE instance corresponding to a suite from the `config` we have selected as |
80 | /// a compatible choice. |
81 | pub(crate) suite: &'static dyn Hpke, |
82 | } |
83 | |
84 | impl EchConfig { |
85 | /// Construct an EchConfig by selecting a ECH config from the provided bytes that is compatible |
86 | /// with one of the given HPKE suites. |
87 | /// |
88 | /// The config list bytes should be sourced from a DNS-over-HTTPS lookup resolving the `HTTPS` |
89 | /// resource record for the host name of the server you wish to connect via ECH, |
90 | /// and extracting the ECH configuration from the `ech` parameter. The extracted bytes should |
91 | /// be base64 decoded to yield the `EchConfigListBytes` you provide to rustls. |
92 | /// |
93 | /// One of the provided ECH configurations must be compatible with the HPKE provider's supported |
94 | /// suites or an error will be returned. |
95 | /// |
96 | /// See the [`ech-client.rs`] example for a complete example of fetching ECH configs from DNS. |
97 | /// |
98 | /// [`ech-client.rs`]: https://github.com/rustls/rustls/blob/main/examples/src/bin/ech-client.rs |
99 | pub fn new( |
100 | ech_config_list: EchConfigListBytes<'_>, |
101 | hpke_suites: &[&'static dyn Hpke], |
102 | ) -> Result<Self, Error> { |
103 | let ech_configs = Vec::<EchConfigPayload>::read(&mut Reader::init(&ech_config_list)) |
104 | .map_err(|_| { |
105 | Error::InvalidEncryptedClientHello(EncryptedClientHelloError::InvalidConfigList) |
106 | })?; |
107 | |
108 | // Note: we name the index var _i because if the log feature is disabled |
109 | // it is unused. |
110 | #[cfg_attr (not(feature = "std" ), allow(clippy::unused_enumerate_index))] |
111 | for (_i, config) in ech_configs.iter().enumerate() { |
112 | let contents = match config { |
113 | EchConfigPayload::V18(contents) => contents, |
114 | EchConfigPayload::Unknown { |
115 | version: _version, .. |
116 | } => { |
117 | warn!( |
118 | "ECH config {} has unsupported version {:?}" , |
119 | _i + 1, |
120 | _version |
121 | ); |
122 | continue; // Unsupported version. |
123 | } |
124 | }; |
125 | |
126 | if contents.has_unknown_mandatory_extension() || contents.has_duplicate_extension() { |
127 | warn!("ECH config has duplicate, or unknown mandatory extensions: {contents:?}" ,); |
128 | continue; // Unsupported, or malformed extensions. |
129 | } |
130 | |
131 | let key_config = &contents.key_config; |
132 | for cipher_suite in &key_config.symmetric_cipher_suites { |
133 | if cipher_suite.aead_id.tag_len().is_none() { |
134 | continue; // Unsupported EXPORT_ONLY AEAD cipher suite. |
135 | } |
136 | |
137 | let suite = HpkeSuite { |
138 | kem: key_config.kem_id, |
139 | sym: *cipher_suite, |
140 | }; |
141 | if let Some(hpke) = hpke_suites |
142 | .iter() |
143 | .find(|hpke| hpke.suite() == suite) |
144 | { |
145 | debug!( |
146 | "selected ECH config ID {:?} suite {:?} public_name {:?}" , |
147 | key_config.config_id, suite, contents.public_name |
148 | ); |
149 | return Ok(Self { |
150 | config: config.clone(), |
151 | suite: *hpke, |
152 | }); |
153 | } |
154 | } |
155 | } |
156 | |
157 | Err(EncryptedClientHelloError::NoCompatibleConfig.into()) |
158 | } |
159 | |
160 | /// Compute the HPKE `SetupBaseS` `info` parameter for this ECH configuration. |
161 | /// |
162 | /// See <https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-17#section-6.1>. |
163 | pub(crate) fn hpke_info(&self) -> Vec<u8> { |
164 | let mut info = Vec::with_capacity(128); |
165 | // "tls ech" || 0x00 || ECHConfig |
166 | info.extend_from_slice(b"tls ech \0" ); |
167 | self.config.encode(&mut info); |
168 | info |
169 | } |
170 | } |
171 | |
172 | /// Configuration for GREASE Encrypted Client Hello. |
173 | #[derive (Clone, Debug)] |
174 | pub struct EchGreaseConfig { |
175 | pub(crate) suite: &'static dyn Hpke, |
176 | pub(crate) placeholder_key: HpkePublicKey, |
177 | } |
178 | |
179 | impl EchGreaseConfig { |
180 | /// Construct a GREASE ECH configuration. |
181 | /// |
182 | /// This configuration is used when the client wishes to offer ECH to prevent ossification, |
183 | /// but doesn't have a real ECH configuration to use for the remote server. In this case |
184 | /// a placeholder or "GREASE"[^0] extension is used. |
185 | /// |
186 | /// Returns an error if the HPKE provider does not support the given suite. |
187 | /// |
188 | /// [^0]: <https://www.rfc-editor.org/rfc/rfc8701> |
189 | pub fn new(suite: &'static dyn Hpke, placeholder_key: HpkePublicKey) -> Self { |
190 | Self { |
191 | suite, |
192 | placeholder_key, |
193 | } |
194 | } |
195 | |
196 | /// Build a GREASE ECH extension based on the placeholder configuration. |
197 | /// |
198 | /// See <https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-18#name-grease-ech> for |
199 | /// more information. |
200 | pub(crate) fn grease_ext( |
201 | &self, |
202 | secure_random: &'static dyn SecureRandom, |
203 | inner_name: ServerName<'static>, |
204 | outer_hello: &ClientHelloPayload, |
205 | ) -> Result<ClientExtension, Error> { |
206 | trace!("Preparing GREASE ECH extension" ); |
207 | |
208 | // Pick a random config id. |
209 | let mut config_id: [u8; 1] = [0; 1]; |
210 | secure_random.fill(&mut config_id[..])?; |
211 | |
212 | let suite = self.suite.suite(); |
213 | |
214 | // Construct a dummy ECH state - we don't have a real ECH config from a server since |
215 | // this is for GREASE. |
216 | let mut grease_state = EchState::new( |
217 | &EchConfig { |
218 | config: EchConfigPayload::V18(EchConfigContents { |
219 | key_config: HpkeKeyConfig { |
220 | config_id: config_id[0], |
221 | kem_id: HpkeKem::DHKEM_P256_HKDF_SHA256, |
222 | public_key: PayloadU16(self.placeholder_key.0.clone()), |
223 | symmetric_cipher_suites: vec![suite.sym], |
224 | }, |
225 | maximum_name_length: 0, |
226 | public_name: DnsName::try_from("filler" ).unwrap(), |
227 | extensions: Vec::default(), |
228 | }), |
229 | suite: self.suite, |
230 | }, |
231 | inner_name, |
232 | false, |
233 | secure_random, |
234 | false, // Does not matter if we enable/disable SNI here. Inner hello is not used. |
235 | )?; |
236 | |
237 | // Construct an inner hello using the outer hello - this allows us to know the size of |
238 | // dummy payload we should use for the GREASE extension. |
239 | let encoded_inner_hello = grease_state.encode_inner_hello(outer_hello, None, &None); |
240 | |
241 | // Generate a payload of random data equivalent in length to a real inner hello. |
242 | let payload_len = encoded_inner_hello.len() |
243 | + suite |
244 | .sym |
245 | .aead_id |
246 | .tag_len() |
247 | // Safety: we have confirmed the AEAD is supported when building the config. All |
248 | // supported AEADs have a tag length. |
249 | .unwrap(); |
250 | let mut payload = vec![0; payload_len]; |
251 | secure_random.fill(&mut payload)?; |
252 | |
253 | // Return the GREASE extension. |
254 | Ok(ClientExtension::EncryptedClientHello( |
255 | EncryptedClientHello::Outer(EncryptedClientHelloOuter { |
256 | cipher_suite: suite.sym, |
257 | config_id: config_id[0], |
258 | enc: PayloadU16(grease_state.enc.0), |
259 | payload: PayloadU16::new(payload), |
260 | }), |
261 | )) |
262 | } |
263 | } |
264 | |
265 | /// An enum representing ECH offer status. |
266 | #[derive (Debug, Clone, Copy, Eq, PartialEq)] |
267 | pub enum EchStatus { |
268 | /// ECH was not offered - it is a normal TLS handshake. |
269 | NotOffered, |
270 | /// GREASE ECH was sent. This is not considered offering ECH. |
271 | Grease, |
272 | /// ECH was offered but we do not yet know whether the offer was accepted or rejected. |
273 | Offered, |
274 | /// ECH was offered and the server accepted. |
275 | Accepted, |
276 | /// ECH was offered and the server rejected. |
277 | Rejected, |
278 | } |
279 | |
280 | /// Contextual data for a TLS client handshake that has offered encrypted client hello (ECH). |
281 | pub(crate) struct EchState { |
282 | // The public DNS name from the ECH configuration we've chosen - this is included as the SNI |
283 | // value for the "outer" client hello. It can only be a DnsName, not an IP address. |
284 | pub(crate) outer_name: DnsName<'static>, |
285 | // If we're resuming in the inner hello, this is the early key schedule to use for encrypting |
286 | // early data if the ECH offer is accepted. |
287 | pub(crate) early_data_key_schedule: Option<KeyScheduleEarly>, |
288 | // A random value we use for the inner hello. |
289 | pub(crate) inner_hello_random: Random, |
290 | // A transcript buffer maintained for the inner hello. Once ECH is confirmed we switch to |
291 | // using this transcript for the handshake. |
292 | pub(crate) inner_hello_transcript: HandshakeHashBuffer, |
293 | // A source of secure random data. |
294 | secure_random: &'static dyn SecureRandom, |
295 | // An HPKE sealer context that can be used for encrypting ECH data. |
296 | sender: Box<dyn HpkeSealer>, |
297 | // The ID of the ECH configuration we've chosen - this is included in the outer ECH extension. |
298 | config_id: u8, |
299 | // The private server name we'll use for the inner protected hello. |
300 | inner_name: ServerName<'static>, |
301 | // The advertised maximum name length from the ECH configuration we've chosen - this is used |
302 | // for padding calculations. |
303 | maximum_name_length: u8, |
304 | // A supported symmetric cipher suite from the ECH configuration we've chosen - this is |
305 | // included in the outer ECH extension. |
306 | cipher_suite: HpkeSymmetricCipherSuite, |
307 | // A secret encapsulated to the public key of the remote server. This is included in the |
308 | // outer ECH extension for non-retry outer hello messages. |
309 | enc: EncapsulatedSecret, |
310 | // Whether the inner client hello should contain a server name indication (SNI) extension. |
311 | enable_sni: bool, |
312 | // The extensions sent in the inner hello. |
313 | sent_extensions: Vec<ExtensionType>, |
314 | } |
315 | |
316 | impl EchState { |
317 | pub(crate) fn new( |
318 | config: &EchConfig, |
319 | inner_name: ServerName<'static>, |
320 | client_auth_enabled: bool, |
321 | secure_random: &'static dyn SecureRandom, |
322 | enable_sni: bool, |
323 | ) -> Result<Self, Error> { |
324 | let EchConfigPayload::V18(config_contents) = &config.config else { |
325 | // the public EchConfig::new() constructor ensures we only have supported |
326 | // configurations. |
327 | unreachable!("ECH config version mismatch" ); |
328 | }; |
329 | let key_config = &config_contents.key_config; |
330 | |
331 | // Encapsulate a secret for the server's public key, and set up a sender context |
332 | // we can use to seal messages. |
333 | let (enc, sender) = config.suite.setup_sealer( |
334 | &config.hpke_info(), |
335 | &HpkePublicKey(key_config.public_key.0.clone()), |
336 | )?; |
337 | |
338 | // Start a new transcript buffer for the inner hello. |
339 | let mut inner_hello_transcript = HandshakeHashBuffer::new(); |
340 | if client_auth_enabled { |
341 | inner_hello_transcript.set_client_auth_enabled(); |
342 | } |
343 | |
344 | Ok(Self { |
345 | secure_random, |
346 | sender, |
347 | config_id: key_config.config_id, |
348 | inner_name, |
349 | outer_name: config_contents.public_name.clone(), |
350 | maximum_name_length: config_contents.maximum_name_length, |
351 | cipher_suite: config.suite.suite().sym, |
352 | enc, |
353 | inner_hello_random: Random::new(secure_random)?, |
354 | inner_hello_transcript, |
355 | early_data_key_schedule: None, |
356 | enable_sni, |
357 | sent_extensions: Vec::new(), |
358 | }) |
359 | } |
360 | |
361 | /// Construct a ClientHelloPayload offering ECH. |
362 | /// |
363 | /// An outer hello, with a protected inner hello for the `inner_name` will be returned, and the |
364 | /// ECH context will be updated to reflect the inner hello that was offered. |
365 | /// |
366 | /// If `retry_req` is `Some`, then the outer hello will be constructed for a hello retry request. |
367 | /// |
368 | /// If `resuming` is `Some`, then the inner hello will be constructed for a resumption handshake. |
369 | pub(crate) fn ech_hello( |
370 | &mut self, |
371 | mut outer_hello: ClientHelloPayload, |
372 | retry_req: Option<&HelloRetryRequest>, |
373 | resuming: &Option<Retrieved<&persist::Tls13ClientSessionValue>>, |
374 | ) -> Result<ClientHelloPayload, Error> { |
375 | trace!( |
376 | "Preparing ECH offer {}" , |
377 | if retry_req.is_some() { "for retry" } else { "" } |
378 | ); |
379 | |
380 | // Construct the encoded inner hello and update the transcript. |
381 | let encoded_inner_hello = self.encode_inner_hello(&outer_hello, retry_req, resuming); |
382 | |
383 | // Complete the ClientHelloOuterAAD with an ech extension, the payload should be a placeholder |
384 | // of size L, all zeroes. L == length of encrypting encoded client hello inner w/ the selected |
385 | // HPKE AEAD. (sum of plaintext + tag length, typically). |
386 | let payload_len = encoded_inner_hello.len() |
387 | + self |
388 | .cipher_suite |
389 | .aead_id |
390 | .tag_len() |
391 | // Safety: we've already verified this AEAD is supported when loading the config |
392 | // that was used to create the ECH context. All supported AEADs have a tag length. |
393 | .unwrap(); |
394 | |
395 | // Outer hello's created in response to a hello retry request omit the enc value. |
396 | let enc = match retry_req.is_some() { |
397 | true => Vec::default(), |
398 | false => self.enc.0.clone(), |
399 | }; |
400 | |
401 | fn outer_hello_ext(ctx: &EchState, enc: Vec<u8>, payload: Vec<u8>) -> ClientExtension { |
402 | ClientExtension::EncryptedClientHello(EncryptedClientHello::Outer( |
403 | EncryptedClientHelloOuter { |
404 | cipher_suite: ctx.cipher_suite, |
405 | config_id: ctx.config_id, |
406 | enc: PayloadU16::new(enc), |
407 | payload: PayloadU16::new(payload), |
408 | }, |
409 | )) |
410 | } |
411 | |
412 | // The outer handshake is not permitted to resume a session. If we're resuming in the |
413 | // inner handshake we remove the PSK extension from the outer hello, replacing it |
414 | // with a GREASE PSK to implement the "ClientHello Malleability Mitigation" mentioned |
415 | // in 10.12.3. |
416 | if let Some(ClientExtension::PresharedKey(psk_offer)) = outer_hello.extensions.last_mut() { |
417 | self.grease_psk(psk_offer)?; |
418 | } |
419 | |
420 | // To compute the encoded AAD we add a placeholder extension with an empty payload. |
421 | outer_hello |
422 | .extensions |
423 | .push(outer_hello_ext(self, enc.clone(), vec![0; payload_len])); |
424 | |
425 | // Next we compute the proper extension payload. |
426 | let payload = self |
427 | .sender |
428 | .seal(&outer_hello.get_encoding(), &encoded_inner_hello)?; |
429 | |
430 | // And then we replace the placeholder extension with the real one. |
431 | outer_hello.extensions.pop(); |
432 | outer_hello |
433 | .extensions |
434 | .push(outer_hello_ext(self, enc, payload)); |
435 | |
436 | Ok(outer_hello) |
437 | } |
438 | |
439 | /// Confirm whether an ECH offer was accepted based on examining the server hello. |
440 | pub(crate) fn confirm_acceptance( |
441 | self, |
442 | ks: &mut KeyScheduleHandshakeStart, |
443 | server_hello: &ServerHelloPayload, |
444 | hash: &'static dyn Hash, |
445 | ) -> Result<Option<EchAccepted>, Error> { |
446 | // Start the inner transcript hash now that we know the hash algorithm to use. |
447 | let inner_transcript = self |
448 | .inner_hello_transcript |
449 | .start_hash(hash); |
450 | |
451 | // Fork the transcript that we've started with the inner hello to use for a confirmation step. |
452 | // We need to preserve the original inner_transcript to use if this confirmation succeeds. |
453 | let mut confirmation_transcript = inner_transcript.clone(); |
454 | |
455 | // Add the server hello confirmation - this differs from the standard server hello encoding. |
456 | confirmation_transcript.add_message(&Self::server_hello_conf(server_hello)); |
457 | |
458 | // Derive a confirmation secret from the inner hello random and the confirmation transcript. |
459 | let derived = ks.server_ech_confirmation_secret( |
460 | self.inner_hello_random.0.as_ref(), |
461 | confirmation_transcript.current_hash(), |
462 | ); |
463 | |
464 | // Check that first 8 digits of the derived secret match the last 8 digits of the original |
465 | // server random. This match signals that the server accepted the ECH offer. |
466 | // Indexing safety: Random is [0; 32] by construction. |
467 | |
468 | match ConstantTimeEq::ct_eq(derived.as_ref(), server_hello.random.0[24..].as_ref()).into() { |
469 | true => { |
470 | trace!("ECH accepted by server" ); |
471 | Ok(Some(EchAccepted { |
472 | transcript: inner_transcript, |
473 | random: self.inner_hello_random, |
474 | sent_extensions: self.sent_extensions, |
475 | })) |
476 | } |
477 | false => { |
478 | trace!("ECH rejected by server" ); |
479 | Ok(None) |
480 | } |
481 | } |
482 | } |
483 | |
484 | pub(crate) fn confirm_hrr_acceptance( |
485 | &self, |
486 | hrr: &HelloRetryRequest, |
487 | cs: &Tls13CipherSuite, |
488 | common: &mut CommonState, |
489 | ) -> Result<bool, Error> { |
490 | // The client checks for the "encrypted_client_hello" extension. |
491 | let ech_conf = match hrr.ech() { |
492 | // If none is found, the server has implicitly rejected ECH. |
493 | None => return Ok(false), |
494 | // Otherwise, if it has a length other than 8, the client aborts the |
495 | // handshake with a "decode_error" alert. |
496 | Some(ech_conf) if ech_conf.len() != 8 => { |
497 | return Err({ |
498 | common.send_fatal_alert( |
499 | AlertDescription::DecodeError, |
500 | PeerMisbehaved::IllegalHelloRetryRequestWithInvalidEch, |
501 | ) |
502 | }); |
503 | } |
504 | Some(ech_conf) => ech_conf, |
505 | }; |
506 | |
507 | // Otherwise the client computes hrr_accept_confirmation as described in Section |
508 | // 7.2.1 |
509 | let confirmation_transcript = self.inner_hello_transcript.clone(); |
510 | let mut confirmation_transcript = |
511 | confirmation_transcript.start_hash(cs.common.hash_provider); |
512 | confirmation_transcript.rollup_for_hrr(); |
513 | confirmation_transcript.add_message(&Self::hello_retry_request_conf(hrr)); |
514 | |
515 | let derived = server_ech_hrr_confirmation_secret( |
516 | cs.hkdf_provider, |
517 | &self.inner_hello_random.0, |
518 | confirmation_transcript.current_hash(), |
519 | ); |
520 | |
521 | match ConstantTimeEq::ct_eq(derived.as_ref(), ech_conf).into() { |
522 | true => { |
523 | trace!("ECH accepted by server in hello retry request" ); |
524 | Ok(true) |
525 | } |
526 | false => { |
527 | trace!("ECH rejected by server in hello retry request" ); |
528 | Ok(false) |
529 | } |
530 | } |
531 | } |
532 | |
533 | /// Update the ECH context inner hello transcript based on a received hello retry request message. |
534 | /// |
535 | /// This will start the in-progress transcript using the given `hash`, convert it into an HRR |
536 | /// buffer, and then add the hello retry message `m`. |
537 | pub(crate) fn transcript_hrr_update(&mut self, hash: &'static dyn Hash, m: &Message<'_>) { |
538 | trace!("Updating ECH inner transcript for HRR" ); |
539 | |
540 | let inner_transcript = self |
541 | .inner_hello_transcript |
542 | .clone() |
543 | .start_hash(hash); |
544 | |
545 | let mut inner_transcript_buffer = inner_transcript.into_hrr_buffer(); |
546 | inner_transcript_buffer.add_message(m); |
547 | self.inner_hello_transcript = inner_transcript_buffer; |
548 | } |
549 | |
550 | // 5.1 "Encoding the ClientHelloInner" |
551 | fn encode_inner_hello( |
552 | &mut self, |
553 | outer_hello: &ClientHelloPayload, |
554 | retryreq: Option<&HelloRetryRequest>, |
555 | resuming: &Option<Retrieved<&persist::Tls13ClientSessionValue>>, |
556 | ) -> Vec<u8> { |
557 | // Start building an inner hello using the outer_hello as a template. |
558 | let mut inner_hello = ClientHelloPayload { |
559 | // Some information is copied over as-is. |
560 | client_version: outer_hello.client_version, |
561 | session_id: outer_hello.session_id, |
562 | compression_methods: outer_hello.compression_methods.clone(), |
563 | |
564 | // We will build up the included extensions ourselves. |
565 | extensions: vec![], |
566 | |
567 | // Set the inner hello random to the one we generated when creating the ECH state. |
568 | // We hold on to the inner_hello_random in the ECH state to use later for confirming |
569 | // whether ECH was accepted or not. |
570 | random: self.inner_hello_random, |
571 | |
572 | // We remove the empty renegotiation info SCSV from the outer hello's ciphersuite. |
573 | // Similar to the TLS 1.2 specific extensions we will filter out, this is seen as a |
574 | // TLS 1.2 only feature by bogo. |
575 | cipher_suites: outer_hello |
576 | .cipher_suites |
577 | .iter() |
578 | .filter(|cs| **cs != TLS_EMPTY_RENEGOTIATION_INFO_SCSV) |
579 | .cloned() |
580 | .collect(), |
581 | }; |
582 | |
583 | // The inner hello will always have an inner variant of the ECH extension added. |
584 | // See Section 6.1 rule 4. |
585 | inner_hello |
586 | .extensions |
587 | .push(ClientExtension::EncryptedClientHello( |
588 | EncryptedClientHello::Inner, |
589 | )); |
590 | |
591 | let inner_sni = match &self.inner_name { |
592 | // The inner hello only gets a SNI value if enable_sni is true and the inner name |
593 | // is a domain name (not an IP address). |
594 | ServerName::DnsName(dns_name) if self.enable_sni => Some(dns_name), |
595 | _ => None, |
596 | }; |
597 | |
598 | // Now we consider each of the outer hello's extensions - we can either: |
599 | // 1. Omit the extension if it isn't appropriate (e.g. is a TLS 1.2 extension). |
600 | // 2. Add the extension to the inner hello as-is. |
601 | // 3. Compress the extension, by collecting it into a list of to-be-compressed |
602 | // extensions we'll handle separately. |
603 | let mut compressed_exts = Vec::with_capacity(outer_hello.extensions.len()); |
604 | let mut compressed_ext_types = Vec::with_capacity(outer_hello.extensions.len()); |
605 | for ext in &outer_hello.extensions { |
606 | // Some outer hello extensions are only useful in the context where a TLS 1.3 |
607 | // connection allows TLS 1.2. This isn't the case for ECH so we skip adding them |
608 | // to the inner hello. |
609 | if matches!( |
610 | ext.ext_type(), |
611 | ExtensionType::ExtendedMasterSecret |
612 | | ExtensionType::SessionTicket |
613 | | ExtensionType::ECPointFormats |
614 | ) { |
615 | continue; |
616 | } |
617 | |
618 | if ext.ext_type() == ExtensionType::ServerName { |
619 | // We may want to replace the outer hello SNI with our own inner hello specific SNI. |
620 | if let Some(sni_value) = inner_sni { |
621 | inner_hello |
622 | .extensions |
623 | .push(ClientExtension::make_sni(&sni_value.borrow())); |
624 | } |
625 | // We don't want to add, or compress, the SNI from the outer hello. |
626 | continue; |
627 | } |
628 | |
629 | // Compressed extensions need to be put aside to include in one contiguous block. |
630 | // Uncompressed extensions get added directly to the inner hello. |
631 | if ext.ext_type().ech_compress() { |
632 | compressed_exts.push(ext.clone()); |
633 | compressed_ext_types.push(ext.ext_type()); |
634 | } else { |
635 | inner_hello.extensions.push(ext.clone()); |
636 | } |
637 | } |
638 | |
639 | // We've added all the uncompressed extensions. Now we need to add the contiguous |
640 | // block of to-be-compressed extensions. Where we do this depends on whether the |
641 | // last uncompressed extension is a PSK for resumption. In this case we must |
642 | // add the to-be-compressed extensions _before_ the PSK. |
643 | let compressed_exts_index = |
644 | if let Some(ClientExtension::PresharedKey(_)) = inner_hello.extensions.last() { |
645 | inner_hello.extensions.len() - 1 |
646 | } else { |
647 | inner_hello.extensions.len() |
648 | }; |
649 | inner_hello.extensions.splice( |
650 | compressed_exts_index..compressed_exts_index, |
651 | compressed_exts, |
652 | ); |
653 | |
654 | // Note which extensions we're sending in the inner hello. This may differ from |
655 | // the outer hello (e.g. the inner hello may omit SNI while the outer hello will |
656 | // always have the ECH cover name in SNI). |
657 | self.sent_extensions = inner_hello |
658 | .extensions |
659 | .iter() |
660 | .map(|ext| ext.ext_type()) |
661 | .collect(); |
662 | |
663 | // If we're resuming, we need to update the PSK binder in the inner hello. |
664 | if let Some(resuming) = resuming.as_ref() { |
665 | let mut chp = HandshakeMessagePayload { |
666 | typ: HandshakeType::ClientHello, |
667 | payload: HandshakePayload::ClientHello(inner_hello), |
668 | }; |
669 | |
670 | // Retain the early key schedule we get from processing the binder. |
671 | self.early_data_key_schedule = Some(tls13::fill_in_psk_binder( |
672 | resuming, |
673 | &self.inner_hello_transcript, |
674 | &mut chp, |
675 | )); |
676 | |
677 | // fill_in_psk_binder works on an owned HandshakeMessagePayload, so we need to |
678 | // extract our inner hello back out of it to retain ownership. |
679 | inner_hello = match chp.payload { |
680 | HandshakePayload::ClientHello(chp) => chp, |
681 | // Safety: we construct the HMP above and know its type unconditionally. |
682 | _ => unreachable!(), |
683 | }; |
684 | } |
685 | |
686 | trace!("ECH Inner Hello: {:#?}" , inner_hello); |
687 | |
688 | // Encode the inner hello according to the rules required for ECH. This differs |
689 | // from the standard encoding in several ways. Notably this is where we will |
690 | // replace the block of contiguous to-be-compressed extensions with a marker. |
691 | let mut encoded_hello = inner_hello.ech_inner_encoding(compressed_ext_types); |
692 | |
693 | // Calculate padding |
694 | // max_name_len = L |
695 | let max_name_len = self.maximum_name_length; |
696 | let max_name_len = if max_name_len > 0 { max_name_len } else { 255 }; |
697 | |
698 | let padding_len = match &self.inner_name { |
699 | ServerName::DnsName(name) => { |
700 | // name.len() = D |
701 | // max(0, L - D) |
702 | core::cmp::max( |
703 | 0, |
704 | max_name_len.saturating_sub(name.as_ref().len() as u8) as usize, |
705 | ) |
706 | } |
707 | _ => { |
708 | // L + 9 |
709 | // "This is the length of a "server_name" extension with an L-byte name." |
710 | // We widen to usize here to avoid overflowing u8 + u8. |
711 | max_name_len as usize + 9 |
712 | } |
713 | }; |
714 | |
715 | // Let L be the length of the EncodedClientHelloInner with all the padding computed so far |
716 | // Let N = 31 - ((L - 1) % 32) and add N bytes of padding. |
717 | let padding_len = 31 - ((encoded_hello.len() + padding_len - 1) % 32); |
718 | encoded_hello.extend(vec![0; padding_len]); |
719 | |
720 | // Construct the inner hello message that will be used for the transcript. |
721 | let inner_hello_msg = Message { |
722 | version: match retryreq { |
723 | // <https://datatracker.ietf.org/doc/html/rfc8446#section-5.1>: |
724 | // "This value MUST be set to 0x0303 for all records generated |
725 | // by a TLS 1.3 implementation ..." |
726 | Some(_) => ProtocolVersion::TLSv1_2, |
727 | // "... other than an initial ClientHello (i.e., one not |
728 | // generated after a HelloRetryRequest), where it MAY also be |
729 | // 0x0301 for compatibility purposes" |
730 | // |
731 | // (retryreq == None means we're in the "initial ClientHello" case) |
732 | None => ProtocolVersion::TLSv1_0, |
733 | }, |
734 | payload: MessagePayload::handshake(HandshakeMessagePayload { |
735 | typ: HandshakeType::ClientHello, |
736 | payload: HandshakePayload::ClientHello(inner_hello), |
737 | }), |
738 | }; |
739 | |
740 | // Update the inner transcript buffer with the inner hello message. |
741 | self.inner_hello_transcript |
742 | .add_message(&inner_hello_msg); |
743 | |
744 | encoded_hello |
745 | } |
746 | |
747 | // See https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-18#name-grease-psk |
748 | fn grease_psk(&self, psk_offer: &mut PresharedKeyOffer) -> Result<(), Error> { |
749 | for ident in psk_offer.identities.iter_mut() { |
750 | // "For each PSK identity advertised in the ClientHelloInner, the |
751 | // client generates a random PSK identity with the same length." |
752 | self.secure_random |
753 | .fill(&mut ident.identity.0)?; |
754 | // "It also generates a random, 32-bit, unsigned integer to use as |
755 | // the obfuscated_ticket_age." |
756 | let mut ticket_age = [0_u8; 4]; |
757 | self.secure_random |
758 | .fill(&mut ticket_age)?; |
759 | ident.obfuscated_ticket_age = u32::from_be_bytes(ticket_age); |
760 | } |
761 | |
762 | // "Likewise, for each inner PSK binder, the client generates a random string |
763 | // of the same length." |
764 | psk_offer.binders = psk_offer |
765 | .binders |
766 | .iter() |
767 | .map(|old_binder| { |
768 | // We can't access the wrapped binder PresharedKeyBinder's PayloadU8 mutably, |
769 | // so we construct new PresharedKeyBinder's from scratch with the same length. |
770 | let mut new_binder = vec![0; old_binder.as_ref().len()]; |
771 | self.secure_random |
772 | .fill(&mut new_binder)?; |
773 | Ok::<PresharedKeyBinder, Error>(PresharedKeyBinder::from(new_binder)) |
774 | }) |
775 | .collect::<Result<_, _>>()?; |
776 | Ok(()) |
777 | } |
778 | |
779 | fn server_hello_conf(server_hello: &ServerHelloPayload) -> Message<'_> { |
780 | Self::ech_conf_message(HandshakeMessagePayload { |
781 | typ: HandshakeType::ServerHello, |
782 | payload: HandshakePayload::ServerHello(server_hello.clone()), |
783 | }) |
784 | } |
785 | |
786 | fn hello_retry_request_conf(retry_req: &HelloRetryRequest) -> Message<'_> { |
787 | Self::ech_conf_message(HandshakeMessagePayload { |
788 | typ: HandshakeType::HelloRetryRequest, |
789 | payload: HandshakePayload::HelloRetryRequest(retry_req.clone()), |
790 | }) |
791 | } |
792 | |
793 | fn ech_conf_message(hmp: HandshakeMessagePayload<'_>) -> Message<'_> { |
794 | let mut hmp_encoded = Vec::new(); |
795 | hmp.payload_encode(&mut hmp_encoded, Encoding::EchConfirmation); |
796 | Message { |
797 | version: ProtocolVersion::TLSv1_3, |
798 | payload: MessagePayload::Handshake { |
799 | encoded: Payload::new(hmp_encoded), |
800 | parsed: hmp, |
801 | }, |
802 | } |
803 | } |
804 | } |
805 | |
806 | /// Returned from EchState::check_acceptance when the server has accepted the ECH offer. |
807 | /// |
808 | /// Holds the state required to continue the handshake with the inner hello from the ECH offer. |
809 | pub(crate) struct EchAccepted { |
810 | pub(crate) transcript: HandshakeHash, |
811 | pub(crate) random: Random, |
812 | pub(crate) sent_extensions: Vec<ExtensionType>, |
813 | } |
814 | |
815 | pub(crate) fn fatal_alert_required( |
816 | retry_configs: Option<Vec<EchConfigPayload>>, |
817 | common: &mut CommonState, |
818 | ) -> Error { |
819 | common.send_fatal_alert( |
820 | desc:AlertDescription::EncryptedClientHelloRequired, |
821 | err:PeerIncompatible::ServerRejectedEncryptedClientHello(retry_configs), |
822 | ) |
823 | } |
824 | |