ech: construct ECH client hello

This commit continues the implementation of client-side ECH support. We
now act on the provided ECH configuration when available to produce an
appropriate outer/inner client hello.

Adding this support requires introduction of a new `EchState`
struct for maintaining the required handshake state specific to ECH. We
use this in combination with new types for representing the ECH
extensions to produce the outer and inner client hello messages.

We do not yet properly handle confirmation of ECH acceptance, resulting
in decrypt errors when offering ECH. Subsequent commits will update
hello retry request and server hello handling to fix this.
This commit is contained in:
Daniel McCarney 2024-04-12 11:05:14 -04:00
parent 5a03d5b4c9
commit 2ebacadad0
7 changed files with 575 additions and 29 deletions

View File

@ -1,13 +1,29 @@
use alloc::boxed::Box;
use alloc::vec;
use alloc::vec::Vec;
use pki_types::EchConfigListBytes;
use pki_types::{DnsName, EchConfigListBytes, ServerName};
use crate::crypto::hpke::{HpkeProvider, HpkeSuite};
use crate::client::tls13;
use crate::crypto::hpke::{EncapsulatedSecret, HpkeProvider, HpkePublicKey, HpkeSealer, HpkeSuite};
use crate::crypto::SecureRandom;
use crate::hash_hs::HandshakeHashBuffer;
#[cfg(feature = "logging")]
use crate::log::{debug, warn};
use crate::log::{debug, trace, warn};
use crate::msgs::base::PayloadU16;
use crate::msgs::codec::{Codec, Reader};
use crate::msgs::handshake::EchConfig as EchConfigMsg;
use crate::{EncryptedClientHelloError, Error};
use crate::msgs::enums::ExtensionType;
use crate::msgs::handshake::{
ClientExtension, ClientHelloPayload, EchConfig as EchConfigMsg, EncryptedClientHello,
EncryptedClientHelloOuter, HandshakeMessagePayload, HandshakePayload, HelloRetryRequest,
HpkeSymmetricCipherSuite, PresharedKeyBinder, PresharedKeyOffer, Random, SessionId,
};
use crate::msgs::message::{Message, MessagePayload};
use crate::msgs::persist;
use crate::msgs::persist::Retrieved;
use crate::tls13::key_schedule::KeyScheduleEarly;
use crate::CipherSuite::TLS_EMPTY_RENEGOTIATION_INFO_SCSV;
use crate::{EncryptedClientHelloError, Error, HandshakeType, ProtocolVersion};
/// Configuration for performing encrypted client hello.
///
@ -56,6 +72,17 @@ impl EchConfig {
})
}
/// Compute the HPKE `SetupBaseS` `info` parameter for this ECH configuration.
///
/// See <https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-17#section-6.1>.
pub(crate) fn hpke_info(&self) -> Vec<u8> {
let mut info = Vec::with_capacity(128);
// "tls ech" || 0x00 || ECHConfig
info.extend_from_slice(b"tls ech\0");
self.config.encode(&mut info);
info
}
fn select_config_and_suite(
configs: Vec<EchConfigMsg>,
hpke_provider: &'static dyn HpkeProvider,
@ -109,3 +136,368 @@ impl EchConfig {
Err(EncryptedClientHelloError::NoCompatibleConfig.into())
}
}
/// Contextual data for a TLS client handshake that has offered encrypted client hello (ECH).
pub(crate) struct EchState {
// The public DNS name from the ECH configuration we've chosen - this is included as the SNI
// value for the "outer" client hello. It can only be a DnsName, not an IP address.
pub(crate) outer_name: DnsName<'static>,
// If we're resuming in the inner hello, this is the early key schedule to use for encrypting
// early data if the ECH offer is accepted.
pub(crate) early_data_key_schedule: Option<KeyScheduleEarly>,
// A source of secure random data.
secure_random: &'static dyn SecureRandom,
// An HPKE sealer context that can be used for encrypting ECH data.
sender: Box<dyn HpkeSealer>,
// The ID of the ECH configuration we've chosen - this is included in the outer ECH extension.
config_id: u8,
// The private server name we'll use for the inner protected hello.
inner_name: ServerName<'static>,
// The advertised maximum name length from the ECH configuration we've chosen - this is used
// for padding calculations.
maximum_name_length: u8,
// A supported symmetric cipher suite from the ECH configuration we've chosen - this is
// included in the outer ECH extension.
cipher_suite: HpkeSymmetricCipherSuite,
// A secret encapsulated to the public key of the remote server. This is included in the
// outer ECH extension for non-retry outer hello messages.
enc: EncapsulatedSecret,
// A random value we use for the inner hello.
inner_hello_random: Random,
// A transcript buffer maintained for the inner hello. Once ECH is confirmed we switch to
// using this transcript for the handshake.
inner_hello_transcript: HandshakeHashBuffer,
// Whether the inner client hello should contain a server name indication (SNI) extension.
enable_sni: bool,
// The extensions sent in the inner hello.
sent_extensions: Vec<ExtensionType>,
}
impl EchState {
pub(crate) fn new(
config: &EchConfig,
inner_name: ServerName<'static>,
client_auth_enabled: bool,
secure_random: &'static dyn SecureRandom,
enable_sni: bool,
) -> Result<Self, Error> {
let EchConfigMsg::V18(config_contents) = &config.config else {
// the public EchConfig::new() constructor ensures we only have supported
// configurations.
unreachable!("ECH config version mismatch");
};
let key_config = &config_contents.key_config;
// Encapsulate a secret for the server's public key, and set up a sender context
// we can use to seal messages.
let (enc, sender) = config
.hpke_provider
.start(&config.suite)?
.setup_sealer(
&config.hpke_info(),
&HpkePublicKey(key_config.public_key.0.clone()),
)?;
// Start a new transcript buffer for the inner hello.
let mut inner_hello_transcript = HandshakeHashBuffer::new();
if client_auth_enabled {
inner_hello_transcript.set_client_auth_enabled();
}
Ok(Self {
secure_random,
sender,
config_id: key_config.config_id,
inner_name,
outer_name: config_contents.public_name.clone(),
maximum_name_length: config_contents.maximum_name_length,
cipher_suite: config.suite.sym,
enc,
inner_hello_random: Random::new(secure_random)?,
inner_hello_transcript,
early_data_key_schedule: None,
enable_sni,
sent_extensions: Vec::new(),
})
}
/// Construct a ClientHelloPayload offering ECH.
///
/// An outer hello, with a protected inner hello for the `inner_name` will be returned, and the
/// ECH context will be updated to reflect the inner hello that was offered.
///
/// If `retry_req` is `Some`, then the outer hello will be constructed for a hello retry request.
///
/// If `resuming` is `Some`, then the inner hello will be constructed for a resumption handshake.
pub(crate) fn ech_hello(
&mut self,
mut outer_hello: ClientHelloPayload,
retry_req: Option<&HelloRetryRequest>,
resuming: &Option<Retrieved<&persist::Tls13ClientSessionValue>>,
) -> Result<ClientHelloPayload, Error> {
trace!(
"Preparing ECH offer {}",
if retry_req.is_some() { "for retry" } else { "" }
);
// Construct the encoded inner hello and update the transcript.
let encoded_inner_hello = self.encode_inner_hello(&outer_hello, retry_req, resuming);
// Complete the ClientHelloOuterAAD with an ech extension, the payload should be a placeholder
// of size L, all zeroes. L == length of encrypting encoded client hello inner w/ the selected
// HPKE AEAD. (sum of plaintext + tag length, typically).
let payload_len = encoded_inner_hello.len()
+ self
.cipher_suite
.aead_id
.tag_len()
// Safety: we've already verified this AEAD is supported when loading the config
// that was used to create the ECH context. All supported AEADs have a tag length.
.unwrap();
// Outer hello's created in response to a hello retry request omit the enc value.
let enc = match retry_req.is_some() {
true => Vec::default(),
false => self.enc.0.clone(),
};
fn outer_hello_ext(ctx: &EchState, enc: Vec<u8>, payload: Vec<u8>) -> ClientExtension {
ClientExtension::EncryptedClientHello(EncryptedClientHello::Outer(
EncryptedClientHelloOuter {
cipher_suite: ctx.cipher_suite,
config_id: ctx.config_id,
enc: PayloadU16::new(enc),
payload: PayloadU16::new(payload),
},
))
}
// The outer handshake is not permitted to resume a session. If we're resuming in the
// inner handshake we remove the PSK extension from the outer hello, replacing it
// with a GREASE PSK to implement the "ClientHello Malleability Mitigation" mentioned
// in 10.12.3.
if let Some(ClientExtension::PresharedKey(psk_offer)) = outer_hello.extensions.last_mut() {
self.grease_psk(psk_offer)?;
}
// To compute the encoded AAD we add a placeholder extension with an empty payload.
outer_hello
.extensions
.push(outer_hello_ext(self, enc.clone(), vec![0; payload_len]));
// Next we compute the proper extension payload.
let payload = self
.sender
.seal(&outer_hello.get_encoding(), &encoded_inner_hello)?;
// And then we replace the placeholder extension with the real one.
outer_hello.extensions.pop();
outer_hello
.extensions
.push(outer_hello_ext(self, enc, payload));
Ok(outer_hello)
}
fn encode_inner_hello(
&mut self,
outer_hello: &ClientHelloPayload,
retryreq: Option<&HelloRetryRequest>,
resuming: &Option<Retrieved<&persist::Tls13ClientSessionValue>>,
) -> Vec<u8> {
// Start building an inner hello by cloning the initial outer hello.
let mut inner_hello = outer_hello.clone();
inner_hello.extensions.retain(|ext| {
match ext.ext_type() {
// SNI is unconditionally removed - it was copied from the outer hello and
// we will conditionally re-add our own SNI for the inner hello later.
ExtensionType::ServerName |
// We may have copied extensions that are only useful in the context where a TLS 1.3
// connection allows TLS 1.2. This isn't the case for ECH and so we must remove these
// to satisfy a bogo test.
ExtensionType::ExtendedMasterSecret |
ExtensionType::SessionTicket |
ExtensionType::ECPointFormats => false,
// Retain all other extensions.
_ => true,
}
});
// Remove the empty renegotiation info SCSV from the inner hello. Similar to the TLS 1.2
// specific extensions we remove above, this is seen as a TLS 1.2 only feature by bogo.
inner_hello
.cipher_suites
.retain(|cs| *cs != TLS_EMPTY_RENEGOTIATION_INFO_SCSV);
// Add the correct inner SNI - we only do this when the inner name is a DnsName and SNI
// is enabled. IP addresses should not be used in an SNI extensions.
if self.enable_sni {
if let ServerName::DnsName(inner_name) = &self.inner_name {
inner_hello
.extensions
.insert(0, ClientExtension::make_sni(&inner_name.borrow()));
}
}
// Add the inner variant extension to the inner hello.
// Section 6.1 rule 4.
let inner_ech_ext = ClientExtension::EncryptedClientHello(EncryptedClientHello::Inner);
if let Some(ClientExtension::PresharedKey(_)) = inner_hello.extensions.last() {
// Insert it before the PSK - this ext always needs to be last.
inner_hello
.extensions
.insert(inner_hello.extensions.len() - 1, inner_ech_ext);
} else {
// Insert it at the end. No PSK to worry about.
inner_hello
.extensions
.push(inner_ech_ext);
}
// Note which extensions we're sending in the inner hello. This may differ from
// the outer hello (e.g. the inner hello may omit SNI while the outer hello will
// always have the ECH cover name in SNI).
self.sent_extensions = inner_hello
.extensions
.iter()
.map(|ext| ext.ext_type())
.collect();
// Set the inner hello random to the one we generated when creating the ECH state.
// We hold on to the inner_hello_random in the ECH state to use later for confirming
// whether ECH was accepted or not.
inner_hello.random = self.inner_hello_random;
// If we're resuming, we need to update the PSK binder in the inner hello.
if let Some(resuming) = resuming.as_ref() {
let mut chp = HandshakeMessagePayload {
typ: HandshakeType::ClientHello,
payload: HandshakePayload::ClientHello(inner_hello),
};
// Retain the early key schedule we get from processing the binder.
self.early_data_key_schedule = Some(tls13::fill_in_psk_binder(
resuming,
&self.inner_hello_transcript,
&mut chp,
));
// fill_in_psk_binder works on an owned HandshakeMessagePayload, so we need to
// extract our inner hello back out of it to retain ownership.
inner_hello = match chp.payload {
HandshakePayload::ClientHello(chp) => chp,
// Safety: we construct the HMP above and know its type unconditionally.
_ => unreachable!(),
};
}
// Repeating large extensions between ClientHelloInner and ClientHelloOuter can lead to excessive
// size. To reduce the size impact, the client MAY substitute extensions which it knows will be
// duplicated in ClientHelloOuter.
// TODO(@cpu): Extension compression would be handled here-ish.
// 5.1 "Encoding the ClientHelloInner"
// Setting the legacy_session_id field to the empty string.
// Preserve these for reuse
let original_session_id = inner_hello.session_id;
// SessionID is required to be empty in the EncodedClientHelloInner.
inner_hello.session_id = SessionId::empty();
// Encode the inner hello with the empty session ID.
let mut encoded_hello = inner_hello.get_encoding();
// Restore session ID.
inner_hello.session_id = original_session_id;
trace!("ECH Inner Hello: {:#?}", inner_hello);
// Calculate padding
// max_name_len = L
let max_name_len = self.maximum_name_length;
let max_name_len = if max_name_len > 0 { max_name_len } else { 255 };
let padding_len = match &self.inner_name {
ServerName::DnsName(name) => {
// name.len() = D
// max(0, L - D)
core::cmp::max(
0,
max_name_len.saturating_sub(name.as_ref().len() as u8) as usize,
)
}
_ => {
// L + 9
// "This is the length of a "server_name" extension with an L-byte name."
// We widen to usize here to avoid overflowing u8 + u8.
max_name_len as usize + 9
}
};
// Let L be the length of the EncodedClientHelloInner with all the padding computed so far
// Let N = 31 - ((L - 1) % 32) and add N bytes of padding.
let padding_len = 31 - ((encoded_hello.len() + (padding_len) - 1) % 32);
encoded_hello.extend(vec![0; padding_len]);
// Construct the inner hello message that will be used for the transcript.
let inner_hello_msg = Message {
version: match retryreq {
// <https://datatracker.ietf.org/doc/html/rfc8446#section-5.1>:
// "This value MUST be set to 0x0303 for all records generated
// by a TLS 1.3 implementation ..."
Some(_) => ProtocolVersion::TLSv1_2,
// "... other than an initial ClientHello (i.e., one not
// generated after a HelloRetryRequest), where it MAY also be
// 0x0301 for compatibility purposes"
//
// (retryreq == None means we're in the "initial ClientHello" case)
None => ProtocolVersion::TLSv1_0,
},
payload: MessagePayload::handshake(HandshakeMessagePayload {
typ: HandshakeType::ClientHello,
payload: HandshakePayload::ClientHello(inner_hello),
}),
};
// Update the inner transcript buffer with the inner hello message.
self.inner_hello_transcript
.add_message(&inner_hello_msg);
encoded_hello
}
// See https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-18#name-grease-psk
fn grease_psk(&self, psk_offer: &mut PresharedKeyOffer) -> Result<(), Error> {
for ident in psk_offer.identities.iter_mut() {
// "For each PSK identity advertised in the ClientHelloInner, the
// client generates a random PSK identity with the same length."
self.secure_random
.fill(&mut ident.identity.0)?;
// "It also generates a random, 32-bit, unsigned integer to use as
// the obfuscated_ticket_age."
let mut ticket_age = [0_u8; 4];
self.secure_random
.fill(&mut ticket_age)?;
ident.obfuscated_ticket_age = u32::from_be_bytes(ticket_age);
}
// "Likewise, for each inner PSK binder, the client generates a random string
// of the same length."
psk_offer.binders = psk_offer
.binders
.iter()
.map(|old_binder| {
// We can't access the wrapped binder PresharedKeyBinder's PayloadU8 mutably,
// so we construct new PresharedKeyBinder's from scratch with the same length.
let mut new_binder = vec![0; old_binder.as_ref().len()];
self.secure_random
.fill(&mut new_binder)?;
Ok::<PresharedKeyBinder, Error>(PresharedKeyBinder::from(new_binder))
})
.collect::<Result<_, _>>()?;
Ok(())
}
}

View File

@ -15,6 +15,7 @@ use crate::bs_debug;
use crate::check::inappropriate_handshake_message;
use crate::client::client_conn::ClientConnectionData;
use crate::client::common::ClientHelloDetails;
use crate::client::ech::EchState;
use crate::client::{tls13, ClientConfig};
use crate::common_state::{CommonState, HandshakeKind, State};
use crate::conn::ConnectionRandoms;
@ -145,17 +146,18 @@ pub(super) fn start_handshake(
let random = Random::new(config.provider.secure_random)?;
let extension_order_seed = crate::rand::random_u16(config.provider.secure_random)?;
// TODO(XXX): Construct ECH context state, use to perform ECH flow when
// emitting client hello.
let _ech_context = config
.ech_config
.as_ref()
.map(|ech_config| {
let _provider = ech_config.hpke_provider;
let _suite = &ech_config.suite;
let _config = &ech_config.config;
Some(())
});
let ech_state = match config.ech_config.as_ref() {
Some(ech_config) => Some(EchState::new(
ech_config,
server_name.clone(),
config
.client_auth_cert_resolver
.has_certs(),
config.provider.secure_random,
config.enable_sni,
)?),
None => None,
};
emit_client_hello_for_retry(
transcript_buffer,
@ -175,6 +177,7 @@ pub(super) fn start_handshake(
server_name,
},
cx,
ech_state,
)
}
@ -184,6 +187,7 @@ struct ExpectServerHello {
early_key_schedule: Option<KeyScheduleEarly>,
offered_key_share: Option<Box<dyn ActiveKeyExchange>>,
suite: Option<SupportedCipherSuite>,
ech_state: Option<EchState>,
}
struct ExpectServerHelloOrHelloRetryRequest {
@ -211,9 +215,13 @@ fn emit_client_hello_for_retry(
suite: Option<SupportedCipherSuite>,
mut input: ClientHelloInput,
cx: &mut ClientContext<'_>,
mut ech_state: Option<EchState>,
) -> NextStateOrError<'static> {
let config = &input.config;
let support_tls12 = config.supports_version(ProtocolVersion::TLSv1_2) && !cx.common.is_quic();
// Defense in depth: the ECH state should be None if ECH is disabled based on config
// builder semantics.
let forbids_tls12 = cx.common.is_quic() || ech_state.is_some();
let support_tls12 = config.supports_version(ProtocolVersion::TLSv1_2) && !forbids_tls12;
let support_tls13 = config.supports_version(ProtocolVersion::TLSv1_3);
let mut supported_versions = Vec::new();
@ -259,10 +267,24 @@ fn emit_client_hello_for_retry(
));
}
if let (ServerName::DnsName(dns), true) = (&input.server_name, config.enable_sni) {
// We only want to send the SNI extension if the server name contains a DNS name.
exts.push(ClientExtension::make_sni(dns));
}
match (ech_state.as_ref(), config.enable_sni) {
// If we have ECH state we have a "cover name" to send in the outer hello
// as the SNI domain name. This happens unconditionally so we ignore the
// `enable_sni` value. That will be used later to decide what to do for
// the protected inner hello's SNI.
(Some(ech_state), _) => exts.push(ClientExtension::make_sni(&ech_state.outer_name)),
// If we have no ECH state, and SNI is enabled, try to use the input server_name
// for the SNI domain name.
(None, true) => {
if let ServerName::DnsName(dns_name) = &input.server_name {
exts.push(ClientExtension::make_sni(dns_name))
}
}
// If we have no ECH state, and SNI is not enabled, there's nothing to do.
(None, false) => {}
};
if let Some(key_share) = &key_share {
debug_assert!(support_tls13);
@ -325,7 +347,7 @@ fn emit_client_hello_for_retry(
// We don't do renegotiation at all, in fact.
cipher_suites.push(CipherSuite::TLS_EMPTY_RENEGOTIATION_INFO_SCSV);
let chp_payload = ClientHelloPayload {
let mut chp_payload = ClientHelloPayload {
client_version: ProtocolVersion::TLSv1_2,
random: input.random,
session_id: input.session_id,
@ -334,6 +356,11 @@ fn emit_client_hello_for_retry(
extensions: exts,
};
if let Some(ech_state) = &mut ech_state {
// Replace the client hello payload with an ECH client hello payload.
chp_payload = ech_state.ech_hello(chp_payload, retryreq, &tls13_session)?;
}
// Note what extensions we sent.
input.hello.sent_extensions = chp_payload
.extensions
@ -346,11 +373,24 @@ fn emit_client_hello_for_retry(
payload: HandshakePayload::ClientHello(chp_payload),
};
let early_key_schedule = if let Some(resuming) = tls13_session {
let schedule = tls13::fill_in_psk_binder(&resuming, &transcript_buffer, &mut chp);
Some((resuming.suite(), schedule))
} else {
None
let early_key_schedule = match (ech_state.as_mut(), tls13_session) {
// If we're performing ECH and resuming, then the PSK binder will have been dealt with
// separately and we need to take the early_data_key_schedule computed for the inner hello.
// TODO(@cpu): This is an area I'm not 100% confident the implementation is correct!
(Some(ech_state), Some(tls13_session)) => ech_state
.early_data_key_schedule
.take()
.map(|schedule| (tls13_session.suite(), schedule)),
// When we're not doing ECH and resuming, then the PSK binder need to be filled in as
// normal.
(_, Some(tls13_session)) => Some((
tls13_session.suite(),
tls13::fill_in_psk_binder(&tls13_session, &transcript_buffer, &mut chp),
)),
// No early key schedule in other cases.
_ => None,
};
let ch = Message {
@ -404,6 +444,7 @@ fn emit_client_hello_for_retry(
early_key_schedule,
offered_key_share: key_share,
suite,
ech_state,
};
Ok(if support_tls13 && retryreq.is_none() {
@ -699,6 +740,8 @@ impl State<ClientConnectionData> for ExpectServerHello {
// We always send a key share when TLS 1.3 is enabled.
self.offered_key_share.unwrap(),
self.input.sent_tls13_fake_ccs,
self.ech_state,
&m,
)
}
#[cfg(feature = "tls12")]
@ -739,6 +782,11 @@ impl ExpectServerHelloOrHelloRetryRequest {
cx: &mut ClientContext<'_>,
m: Message,
) -> NextStateOrError<'static> {
// TODO(@cpu): Handle confirming ECH for HRR.
if self.next.ech_state.is_some() {
todo!("ECH confirmation handling for HRR");
}
let hrr = require_handshake_msg!(
m,
HandshakeType::HelloRetryRequest,
@ -898,6 +946,7 @@ impl ExpectServerHelloOrHelloRetryRequest {
Some(cs),
self.next.input,
cx,
None, // TODO(@cpu): handle ECH HRR
)
}
}

View File

@ -10,6 +10,7 @@ use super::client_conn::ClientConnectionData;
use super::hs::ClientContext;
use crate::check::inappropriate_handshake_message;
use crate::client::common::{ClientAuthDetails, ClientHelloDetails, ServerCertDetails};
use crate::client::ech::EchState;
use crate::client::{hs, ClientConfig, ClientSessionStore};
use crate::common_state::{CommonState, HandshakeKind, Protocol, Side, State};
use crate::conn::ConnectionRandoms;
@ -71,6 +72,8 @@ pub(super) fn handle_server_hello(
hello: ClientHelloDetails,
our_key_share: Box<dyn ActiveKeyExchange>,
mut sent_tls13_fake_ccs: bool,
ech_state: Option<EchState>,
server_hello_msg: &Message,
) -> hs::NextStateOrError<'static> {
validate_server_hello(cx.common, server_hello)?;
@ -145,6 +148,10 @@ pub(super) fn handle_server_hello(
let shared_secret = our_key_share.complete(&their_key_share.payload.0)?;
// TODO(@cpu): Handle confirmation of ECH.
let _ = ech_state;
let _ = server_hello_msg;
let key_schedule = key_schedule_pre_handshake.into_handshake(shared_secret);
// Remember what KX group the server liked for next time.

View File

@ -42,6 +42,7 @@ enum_builder! {
UnknownPSKIdentity => 0x73,
CertificateRequired => 0x74,
NoApplicationProtocol => 0x78,
EncryptedClientHelloRequired => 0x79, // https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-18#section-11.2
}
}
@ -568,6 +569,19 @@ enum_builder! {
}
}
enum_builder! {
/// The type of Encrypted Client Hello (`EchClientHelloType`).
///
/// Specified in [draft-ietf-tls-esni Section 5].
///
/// [draft-ietf-tls-esni Section 5]: <https://www.ietf.org/archive/id/draft-ietf-tls-esni-18.html#section-5>
@U8
pub enum EchClientHelloType {
ClientHelloOuter => 0,
ClientHelloInner => 1
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -6,7 +6,7 @@ use core::fmt;
use std::time::SystemTimeError;
use crate::enums::{AlertDescription, ContentType, HandshakeType};
use crate::msgs::handshake::KeyExchangeAlgorithm;
use crate::msgs::handshake::{EchConfig, KeyExchangeAlgorithm};
use crate::rand;
/// rustls reports protocol errors using this type.
@ -277,6 +277,7 @@ pub enum PeerIncompatible {
Tls12NotOfferedOrEnabled,
Tls13RequiredForQuic,
UncompressedEcPointsRequired,
ServerRejectedEncryptedClientHello(Option<Vec<EchConfig>>),
}
impl From<PeerIncompatible> for Error {
@ -480,6 +481,8 @@ pub enum EncryptedClientHelloError {
InvalidConfigList,
/// No compatible ECH configuration.
NoCompatibleConfig,
/// The client configuration has server name indication (SNI) disabled.
SniRequired,
}
impl From<EncryptedClientHelloError> for Error {

View File

@ -116,6 +116,7 @@ enum_builder! {
ChannelId => 0x754f,
RenegotiationInfo => 0xff01,
TransportParametersDraft => 0xffa5,
EncryptedClientHello => 0xfe0d, // https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-18#section-11.1
}
}

View File

@ -13,7 +13,9 @@ use pki_types::{CertificateDer, DnsName};
#[cfg(feature = "tls12")]
use crate::crypto::ActiveKeyExchange;
use crate::crypto::SecureRandom;
use crate::enums::{CipherSuite, HandshakeType, ProtocolVersion, SignatureScheme};
use crate::enums::{
CipherSuite, EchClientHelloType, HandshakeType, ProtocolVersion, SignatureScheme,
};
use crate::error::InvalidMessage;
#[cfg(feature = "tls12")]
use crate::ffdhe_groups::FfdheGroup;
@ -553,6 +555,7 @@ pub enum ClientExtension {
TransportParameters(Vec<u8>),
TransportParametersDraft(Vec<u8>),
EarlyData,
EncryptedClientHello(EncryptedClientHello),
Unknown(UnknownExtension),
}
@ -575,6 +578,7 @@ impl ClientExtension {
Self::TransportParameters(_) => ExtensionType::TransportParameters,
Self::TransportParametersDraft(_) => ExtensionType::TransportParametersDraft,
Self::EarlyData => ExtensionType::EarlyData,
Self::EncryptedClientHello(_) => ExtensionType::EncryptedClientHello,
Self::Unknown(ref r) => r.typ,
}
}
@ -604,6 +608,7 @@ impl Codec<'_> for ClientExtension {
Self::TransportParameters(ref r) | Self::TransportParametersDraft(ref r) => {
nested.buf.extend_from_slice(r);
}
Self::EncryptedClientHello(ref r) => r.encode(nested.buf),
Self::Unknown(ref r) => r.encode(nested.buf),
}
}
@ -2654,6 +2659,81 @@ impl TlsListElement for EchConfigExtension {
const SIZE_LEN: ListLength = ListLength::U16;
}
/// Representation of the `ECHClientHello` client extension specified in
/// [draft-ietf-tls-esni Section 5].
///
/// [draft-ietf-tls-esni Section 5]: <https://www.ietf.org/archive/id/draft-ietf-tls-esni-18.html#section-5>
#[derive(Clone, Debug)]
pub enum EncryptedClientHello {
/// A `ECHClientHello` with type [EchClientHelloType::ClientHelloOuter].
Outer(EncryptedClientHelloOuter),
/// An empty `ECHClientHello` with type [EchClientHelloType::ClientHelloInner].
///
/// This variant has no payload.
Inner,
}
impl Codec<'_> for EncryptedClientHello {
fn encode(&self, bytes: &mut Vec<u8>) {
match self {
Self::Outer(payload) => {
EchClientHelloType::ClientHelloOuter.encode(bytes);
payload.encode(bytes);
}
Self::Inner => {
EchClientHelloType::ClientHelloInner.encode(bytes);
// Empty payload.
}
}
}
fn read(r: &mut Reader) -> Result<Self, InvalidMessage> {
match EchClientHelloType::read(r)? {
EchClientHelloType::ClientHelloOuter => {
Ok(Self::Outer(EncryptedClientHelloOuter::read(r)?))
}
EchClientHelloType::ClientHelloInner => Ok(Self::Inner),
_ => Err(InvalidMessage::InvalidContentType),
}
}
}
/// Representation of the ECHClientHello extension with type outer specified in
/// [draft-ietf-tls-esni Section 5].
///
/// [draft-ietf-tls-esni Section 5]: <https://www.ietf.org/archive/id/draft-ietf-tls-esni-18.html#section-5>
#[derive(Clone, Debug)]
pub struct EncryptedClientHelloOuter {
/// The cipher suite used to encrypt ClientHelloInner. Must match a value from
/// ECHConfigContents.cipher_suites list.
pub cipher_suite: HpkeSymmetricCipherSuite,
/// The ECHConfigContents.key_config.config_id for the chosen ECHConfig.
pub config_id: u8,
/// The HPKE encapsulated key, used by servers to decrypt the corresponding payload field.
/// This field is empty in a ClientHelloOuter sent in response to a HelloRetryRequest.
pub enc: PayloadU16,
/// The serialized and encrypted ClientHelloInner structure, encrypted using HPKE.
pub payload: PayloadU16,
}
impl Codec<'_> for EncryptedClientHelloOuter {
fn encode(&self, bytes: &mut Vec<u8>) {
self.cipher_suite.encode(bytes);
self.config_id.encode(bytes);
self.enc.encode(bytes);
self.payload.encode(bytes);
}
fn read(r: &mut Reader) -> Result<Self, InvalidMessage> {
Ok(Self {
cipher_suite: HpkeSymmetricCipherSuite::read(r)?,
config_id: u8::read(r)?,
enc: PayloadU16::read(r)?,
payload: PayloadU16::read(r)?,
})
}
}
fn has_duplicates<I: IntoIterator<Item = E>, E: Into<T>, T: Eq + Ord>(iter: I) -> bool {
let mut seen = BTreeSet::new();