ech: confirm acceptance of ECH offer

This commit continues the implementation of client-side ECH by
processing the result of offering ECH in the non-HRR case. This involves
processing the received server hello, attempting to derive and match
a shared secret indicating ECH acceptance, and forwarding on our
understanding of ECH status (not offered, offered and rejected, or
offered and accepted) to later steps of the handshake.

If we arrive at the end of the handshake and our ECH offer wasn't
accepted, then we emit an appropriate alert and return an error
potentially containing ECH configs to retry with from the server's
response.

If our offer _was_ accepted, we change out the handshake transcript,
sent extensions and client hello as if the inner client hello was used
as the outer client hello. TLS handshaking continues as normal from that
point.

This level of support is sufficient for the ech-client example to
successfully use ECH with the defo.ie test server.

Importantly we do **not** yet handle the case where we offer ECH and the
server sends a hello retry request. This support will follow in
a subsequent commit.
This commit is contained in:
Daniel McCarney 2024-04-18 15:41:12 -04:00
parent 2ebacadad0
commit 6d3a98e4f7
8 changed files with 356 additions and 51 deletions

View File

@ -9,7 +9,7 @@ use pki_types::{ServerName, UnixTime};
use super::handy::NoClientSessionStorage;
use super::hs;
use crate::builder::ConfigBuilder;
use crate::client::EchConfig;
use crate::client::{EchConfig, EchStatus};
use crate::common_state::{CommonState, Protocol, Side};
use crate::conn::{ConnectionCore, UnbufferedConnectionCommon};
use crate::crypto::{CryptoProvider, SupportedKxGroup};
@ -589,6 +589,7 @@ mod connection {
use core::ops::{Deref, DerefMut};
use std::io;
use crate::client::EchStatus;
use pki_types::ServerName;
use super::ClientConnectionData;
@ -710,6 +711,11 @@ mod connection {
self.inner.dangerous_extract_secrets()
}
/// Return the connection's Encrypted Client Hello (ECH) status.
pub fn ech_status(&self) -> EchStatus {
self.inner.core.data.ech_status
}
fn write_early_data(&mut self, data: &[u8]) -> io::Result<usize> {
self.inner
.core
@ -906,6 +912,7 @@ impl std::error::Error for EarlyDataError {}
pub struct ClientConnectionData {
pub(super) early_data: EarlyData,
pub(super) resumption_ciphersuite: Option<SupportedCipherSuite>,
pub(super) ech_status: EchStatus,
}
impl ClientConnectionData {
@ -913,6 +920,7 @@ impl ClientConnectionData {
Self {
early_data: EarlyData::new(),
resumption_ciphersuite: None,
ech_status: EchStatus::NotOffered,
}
}
}

View File

@ -3,27 +3,34 @@ use alloc::vec;
use alloc::vec::Vec;
use pki_types::{DnsName, EchConfigListBytes, ServerName};
use subtle::ConstantTimeEq;
use crate::client::tls13;
use crate::crypto::hash::Hash;
use crate::crypto::hpke::{EncapsulatedSecret, HpkeProvider, HpkePublicKey, HpkeSealer, HpkeSuite};
use crate::crypto::SecureRandom;
use crate::hash_hs::HandshakeHashBuffer;
use crate::hash_hs::{HandshakeHash, HandshakeHashBuffer};
#[cfg(feature = "logging")]
use crate::log::{debug, trace, warn};
use crate::msgs::base::PayloadU16;
use crate::msgs::base::{Payload, PayloadU16};
use crate::msgs::codec::{Codec, Reader};
use crate::msgs::enums::ExtensionType;
use crate::msgs::handshake::{
ClientExtension, ClientHelloPayload, EchConfig as EchConfigMsg, EncryptedClientHello,
ClientExtension, ClientHelloPayload, EchConfig as EchConfigMsg, Encoding, EncryptedClientHello,
EncryptedClientHelloOuter, HandshakeMessagePayload, HandshakePayload, HelloRetryRequest,
HpkeSymmetricCipherSuite, PresharedKeyBinder, PresharedKeyOffer, Random, SessionId,
HpkeSymmetricCipherSuite, PresharedKeyBinder, PresharedKeyOffer, Random, ServerHelloPayload,
SessionId,
};
use crate::msgs::message::{Message, MessagePayload};
use crate::msgs::persist;
use crate::msgs::persist::Retrieved;
use crate::tls13::key_schedule::KeyScheduleEarly;
use crate::tls13::key_schedule::KeyScheduleHandshakeStart;
use crate::CipherSuite::TLS_EMPTY_RENEGOTIATION_INFO_SCSV;
use crate::{EncryptedClientHelloError, Error, HandshakeType, ProtocolVersion};
use crate::{
AlertDescription, CommonState, EncryptedClientHelloError, Error, HandshakeType,
PeerIncompatible, ProtocolVersion,
};
/// Configuration for performing encrypted client hello.
///
@ -137,6 +144,19 @@ impl EchConfig {
}
}
/// An enum representing ECH offer status.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum EchStatus {
/// ECH was not offered - it is a normal TLS handshake.
NotOffered,
/// ECH was offered but we do not yet know whether the offer was accepted or rejected.
Offered,
/// ECH was offered and the server accepted.
Accepted,
/// ECH was offered and the server rejected.
Rejected,
}
/// 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
@ -299,6 +319,54 @@ impl EchState {
Ok(outer_hello)
}
/// Confirm whether an ECH offer was accepted based on examining the server hello.
pub(crate) fn confirm_acceptance(
self,
ks: &mut KeyScheduleHandshakeStart,
server_hello: &ServerHelloPayload,
hash: &'static dyn Hash,
) -> Result<Option<EchAccepted>, Error> {
// Start the inner transcript hash now that we know the hash algorithm to use.
let inner_transcript = self
.inner_hello_transcript
.start_hash(hash);
// Fork the transcript that we've started with the inner hello to use for a confirmation step.
// We need to preserve the original inner_transcript to use if this confirmation succeeds.
let mut confirmation_transcript = inner_transcript.clone();
// Add the server hello confirmation - this differs from the standard server hello encoding.
confirmation_transcript.add_message(&Self::server_hello_conf(server_hello));
// Derive a confirmation secret from the inner hello random and the confirmation transcript.
let derived = ks.server_ech_confirmation_secret(
self.inner_hello_random.0.as_ref(),
confirmation_transcript.current_hash(),
);
// Check that first 8 digits of the derived secret match the last 8 digits of the original
// server random. This match signals that the server accepted the ECH offer.
// Indexing safety: Random is [0; 32] by construction.
Ok(
match ConstantTimeEq::ct_eq(derived.as_ref(), server_hello.random.0[24..].as_ref())
.into()
{
true => {
trace!("ECH accepted by server");
Some(EchAccepted {
transcript: inner_transcript,
random: self.inner_hello_random,
sent_extensions: self.sent_extensions,
})
}
false => {
trace!("ECH rejected by server");
None
}
},
)
}
fn encode_inner_hello(
&mut self,
outer_hello: &ClientHelloPayload,
@ -500,4 +568,42 @@ impl EchState {
.collect::<Result<_, _>>()?;
Ok(())
}
fn server_hello_conf(server_hello: &ServerHelloPayload) -> Message {
Self::ech_conf_message(HandshakeMessagePayload {
typ: HandshakeType::ServerHello,
payload: HandshakePayload::ServerHello(server_hello.clone()),
})
}
fn ech_conf_message(hmp: HandshakeMessagePayload) -> Message {
let mut hmp_encoded = Vec::new();
hmp.payload_encode(&mut hmp_encoded, Encoding::EchConfirmation);
Message {
version: ProtocolVersion::TLSv1_3,
payload: MessagePayload::Handshake {
encoded: Payload::new(hmp_encoded),
parsed: hmp,
},
}
}
}
/// Returned from EchState::check_acceptance when the server has accepted the ECH offer.
///
/// Holds the state required to continue the handshake with the inner hello from the ECH offer.
pub(crate) struct EchAccepted {
pub(crate) transcript: HandshakeHash,
pub(crate) random: Random,
pub(crate) sent_extensions: Vec<ExtensionType>,
}
pub(crate) fn fatal_alert_required(
retry_configs: Option<Vec<EchConfigMsg>>,
common: &mut CommonState,
) -> Error {
common.send_fatal_alert(
AlertDescription::EncryptedClientHelloRequired,
PeerIncompatible::ServerRejectedEncryptedClientHello(retry_configs),
)
}

View File

@ -16,7 +16,7 @@ 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::client::{tls13, ClientConfig, EchStatus};
use crate::common_state::{CommonState, HandshakeKind, State};
use crate::conn::ConnectionRandoms;
use crate::crypto::{ActiveKeyExchange, KeyExchangeAlgorithm};
@ -175,6 +175,7 @@ pub(super) fn start_handshake(
hello: ClientHelloDetails::new(extension_order_seed),
session_id,
server_name,
prev_ech_ext: None,
},
cx,
ech_state,
@ -205,6 +206,7 @@ struct ClientHelloInput {
hello: ClientHelloDetails,
session_id: SessionId,
server_name: ServerName<'static>,
prev_ech_ext: Option<ClientExtension>,
}
fn emit_client_hello_for_retry(
@ -316,16 +318,30 @@ fn emit_client_hello_for_retry(
// Extra extensions must be placed before the PSK extension
exts.extend(extra_exts.iter().cloned());
// If this is a second client hello we're constructing in response to an HRR, and
// we've rejected ECH, then we need to carry forward the exact same ECH
// extension we used in the first hello.
if matches!(cx.data.ech_status, EchStatus::Rejected) & retryreq.is_some() {
if let Some(prev_ech_ext) = input.prev_ech_ext.take() {
exts.push(prev_ech_ext);
}
}
// Do we have a SessionID or ticket cached for this host?
let tls13_session = prepare_resumption(&input.resuming, &mut exts, suite, cx, config);
// Extensions MAY be randomized
// but they also need to keep the same order as the previous ClientHello
exts.sort_by_cached_key(|new_ext| {
// PSK extension is always last
if let ClientExtension::PresharedKey(..) = new_ext {
return u32::MAX;
}
match (&cx.data.ech_status, new_ext) {
// When not offering ECH/GREASE, the PSK extension is always last.
(EchStatus::NotOffered, ClientExtension::PresharedKey(..)) => return u32::MAX,
// When ECH or GREASE are in-play, the ECH extension is always last.
(_, ClientExtension::EncryptedClientHello(_)) => return u32::MAX,
// ... and the PSK extension should be second-to-last.
(_, ClientExtension::PresharedKey(..)) => return u32::MAX - 1,
_ => {}
};
let seed = (input.hello.extension_order_seed as u32) << 16
| (u16::from(new_ext.ext_type()) as u32);
@ -356,9 +372,18 @@ 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)?;
#[allow(clippy::single_match)] // TODO(@cpu): using a match to reduce churn.
match (cx.data.ech_status, &mut ech_state) {
// If we haven't offered ECH, or have offered ECH but got a non-rejecting HRR, then
// we need to replace the client hello payload with an ECH client hello payload.
(EchStatus::NotOffered | EchStatus::Offered, Some(ech_state)) => {
// Replace the client hello payload with an ECH client hello payload.
chp_payload = ech_state.ech_hello(chp_payload, retryreq, &tls13_session)?;
cx.data.ech_status = EchStatus::Offered;
// Store the ECH extension in case we need to carry it forward in a subsequent hello.
input.prev_ech_ext = chp_payload.extensions.last().cloned();
}
_ => {}
}
// Note what extensions we sent.

View File

@ -10,8 +10,8 @@ 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::client::ech::{EchState, EchStatus};
use crate::client::{ech, hs, ClientConfig, ClientSessionStore};
use crate::common_state::{CommonState, HandshakeKind, Protocol, Side, State};
use crate::conn::ConnectionRandoms;
use crate::crypto::ActiveKeyExchange;
@ -26,7 +26,7 @@ use crate::msgs::base::{Payload, PayloadU8};
use crate::msgs::ccs::ChangeCipherSpecPayload;
use crate::msgs::enums::{ExtensionType, KeyUpdateRequest};
use crate::msgs::handshake::{
CertificateEntry, CertificatePayloadTls13, ClientExtension, HandshakeMessagePayload,
CertificateEntry, CertificatePayloadTls13, ClientExtension, EchConfig, HandshakeMessagePayload,
HandshakePayload, HasServerExtensions, NewSessionTicketPayloadTls13, PresharedKeyIdentity,
PresharedKeyOffer, ServerExtension, ServerHelloPayload,
};
@ -65,11 +65,11 @@ pub(super) fn handle_server_hello(
server_hello: &ServerHelloPayload,
mut resuming_session: Option<persist::Tls13ClientSessionValue>,
server_name: ServerName<'static>,
randoms: ConnectionRandoms,
mut randoms: ConnectionRandoms,
suite: &'static Tls13CipherSuite,
transcript: HandshakeHash,
mut transcript: HandshakeHash,
early_key_schedule: Option<KeyScheduleEarly>,
hello: ClientHelloDetails,
mut hello: ClientHelloDetails,
our_key_share: Box<dyn ActiveKeyExchange>,
mut sent_tls13_fake_ccs: bool,
ech_state: Option<EchState>,
@ -148,11 +148,31 @@ 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 mut key_schedule = key_schedule_pre_handshake.into_handshake(shared_secret);
let key_schedule = key_schedule_pre_handshake.into_handshake(shared_secret);
// If we have ECH state, check that the server accepted our offer.
if let Some(ech_state) = ech_state {
cx.data.ech_status = match ech_state.confirm_acceptance(
&mut key_schedule,
server_hello,
suite.common.hash_provider,
)? {
// The server accepted our ECH offer, so complete the inner transcript with the
// server hello message, and switch the relevant state to the copies for the
// inner client hello.
Some(mut accepted) => {
accepted
.transcript
.add_message(server_hello_msg);
transcript = accepted.transcript;
randoms.client = accepted.random.0;
hello.sent_extensions = accepted.sent_extensions;
EchStatus::Accepted
}
// The server rejected our ECH offer.
None => EchStatus::Rejected,
};
}
// Remember what KX group the server liked for next time.
config
@ -398,6 +418,22 @@ impl State<ClientConnectionData> for ExpectEncryptedExtensions {
validate_encrypted_extensions(cx.common, &self.hello, exts)?;
hs::process_alpn_protocol(cx.common, &self.config, exts.alpn_protocol())?;
let ech_retry_configs = match (cx.data.ech_status, exts.server_ech_extension()) {
// If we didn't offer ECH, or ECH was accepted, but the server sent an ECH encrypted
// extension with retry configs, we must error.
(EchStatus::NotOffered | EchStatus::Accepted, Some(_)) => {
return Err(cx.common.send_fatal_alert(
AlertDescription::UnsupportedExtension,
PeerMisbehaved::UnsolicitedEchExtension,
))
}
// If we offered ECH, and it was rejected, store the retry configs (if any) from
// the server's ECH extension. We will return them in an error produced at the end
// of the handshake.
(EchStatus::Rejected, ext) => ext.map(|ext| ext.retry_configs.to_vec()),
_ => None,
};
// QUIC transport parameters
if cx.common.is_quic() {
match exts.quic_params_extension() {
@ -448,6 +484,7 @@ impl State<ClientConnectionData> for ExpectEncryptedExtensions {
client_auth: None,
cert_verified,
sig_verified,
ech_retry_configs,
}))
} else {
if exts.early_data_extension_offered() {
@ -463,6 +500,7 @@ impl State<ClientConnectionData> for ExpectEncryptedExtensions {
suite: self.suite,
transcript: self.transcript,
key_schedule: self.key_schedule,
ech_retry_configs,
}))
}
}
@ -479,6 +517,7 @@ struct ExpectCertificateOrCertReq {
suite: &'static Tls13CipherSuite,
transcript: HandshakeHash,
key_schedule: KeyScheduleHandshake,
ech_retry_configs: Option<Vec<EchConfig>>,
}
impl State<ClientConnectionData> for ExpectCertificateOrCertReq {
@ -506,6 +545,7 @@ impl State<ClientConnectionData> for ExpectCertificateOrCertReq {
transcript: self.transcript,
key_schedule: self.key_schedule,
client_auth: None,
ech_retry_configs: self.ech_retry_configs,
})
.handle(cx, m),
MessagePayload::Handshake {
@ -522,6 +562,7 @@ impl State<ClientConnectionData> for ExpectCertificateOrCertReq {
suite: self.suite,
transcript: self.transcript,
key_schedule: self.key_schedule,
ech_retry_configs: self.ech_retry_configs,
})
.handle(cx, m),
payload => Err(inappropriate_handshake_message(
@ -550,6 +591,7 @@ struct ExpectCertificateRequest {
suite: &'static Tls13CipherSuite,
transcript: HandshakeHash,
key_schedule: KeyScheduleHandshake,
ech_retry_configs: Option<Vec<EchConfig>>,
}
impl State<ClientConnectionData> for ExpectCertificateRequest {
@ -614,6 +656,7 @@ impl State<ClientConnectionData> for ExpectCertificateRequest {
transcript: self.transcript,
key_schedule: self.key_schedule,
client_auth: Some(client_auth),
ech_retry_configs: self.ech_retry_configs,
}))
}
@ -630,6 +673,7 @@ struct ExpectCertificate {
transcript: HandshakeHash,
key_schedule: KeyScheduleHandshake,
client_auth: Option<ClientAuthDetails>,
ech_retry_configs: Option<Vec<EchConfig>>,
}
impl State<ClientConnectionData> for ExpectCertificate {
@ -676,6 +720,7 @@ impl State<ClientConnectionData> for ExpectCertificate {
key_schedule: self.key_schedule,
server_cert,
client_auth: self.client_auth,
ech_retry_configs: self.ech_retry_configs,
}))
}
@ -694,6 +739,7 @@ struct ExpectCertificateVerify<'a> {
key_schedule: KeyScheduleHandshake,
server_cert: ServerCertDetails<'a>,
client_auth: Option<ClientAuthDetails>,
ech_retry_configs: Option<Vec<EchConfig>>,
}
impl State<ClientConnectionData> for ExpectCertificateVerify<'_> {
@ -765,6 +811,7 @@ impl State<ClientConnectionData> for ExpectCertificateVerify<'_> {
client_auth: self.client_auth,
cert_verified,
sig_verified,
ech_retry_configs: self.ech_retry_configs,
}))
}
@ -778,6 +825,7 @@ impl State<ClientConnectionData> for ExpectCertificateVerify<'_> {
key_schedule: self.key_schedule,
server_cert: self.server_cert.into_owned(),
client_auth: self.client_auth,
ech_retry_configs: self.ech_retry_configs,
})
}
}
@ -884,6 +932,7 @@ struct ExpectFinished {
client_auth: Option<ClientAuthDetails>,
cert_verified: verify::ServerCertVerified,
sig_verified: verify::HandshakeSignatureValid,
ech_retry_configs: Option<Vec<EchConfig>>,
}
impl State<ClientConnectionData> for ExpectFinished {
@ -941,13 +990,19 @@ impl State<ClientConnectionData> for ExpectFinished {
signer,
auth_context_tls13: auth_context,
} => {
emit_certificate_tls13(
&mut st.transcript,
Some(&certkey),
auth_context,
cx.common,
);
emit_certverify_tls13(&mut st.transcript, signer.as_ref(), cx.common)?;
// If ECH was offered, and rejected, we MUST respond with
// an empty certificate message.
if cx.data.ech_status == EchStatus::Rejected {
emit_certificate_tls13(&mut st.transcript, None, auth_context, cx.common);
} else {
emit_certificate_tls13(
&mut st.transcript,
Some(&certkey),
auth_context,
cx.common,
);
emit_certverify_tls13(&mut st.transcript, signer.as_ref(), cx.common)?;
}
}
}
}
@ -976,6 +1031,13 @@ impl State<ClientConnectionData> for ExpectFinished {
cx.common
.start_traffic(&mut cx.sendable_plaintext);
// Now that we've reached the end of the normal handshake we must enforce ECH acceptance by
// sending an alert and returning an error (potentially with retry configs) if the server
// did not accept our ECH offer.
if cx.data.ech_status == EchStatus::Rejected {
return Err(ech::fatal_alert_required(st.ech_retry_configs, cx.common));
}
let st = ExpectTraffic {
config: Arc::clone(&st.config),
session_storage: Arc::clone(&st.config.resumption.store),

View File

@ -240,6 +240,7 @@ pub enum PeerMisbehaved {
UnsolicitedSctList,
UnsolicitedServerHelloExtension,
WrongGroupForKeyShare,
UnsolicitedEchExtension,
}
impl From<PeerMisbehaved> for Error {

View File

@ -550,7 +550,7 @@ pub mod client {
};
#[cfg(feature = "std")]
pub use client_conn::{ClientConnection, WriteEarlyData};
pub use ech::EchConfig;
pub use ech::{EchConfig, EchStatus};
#[cfg(any(feature = "std", feature = "hashbrown"))]
pub use handy::ClientSessionMemoryCache;

View File

@ -705,6 +705,7 @@ pub enum ServerExtension {
TransportParameters(Vec<u8>),
TransportParametersDraft(Vec<u8>),
EarlyData,
EncryptedClientHello(ServerEncryptedClientHello),
Unknown(UnknownExtension),
}
@ -724,6 +725,7 @@ impl ServerExtension {
Self::TransportParameters(_) => ExtensionType::TransportParameters,
Self::TransportParametersDraft(_) => ExtensionType::TransportParametersDraft,
Self::EarlyData => ExtensionType::EarlyData,
Self::EncryptedClientHello(_) => ExtensionType::EncryptedClientHello,
Self::Unknown(ref r) => r.typ,
}
}
@ -749,6 +751,7 @@ impl Codec<'_> for ServerExtension {
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),
}
}
@ -776,6 +779,9 @@ impl Codec<'_> for ServerExtension {
Self::TransportParametersDraft(sub.rest().to_vec())
}
ExtensionType::EarlyData => Self::EarlyData,
ExtensionType::EncryptedClientHello => {
Self::EncryptedClientHello(ServerEncryptedClientHello::read(&mut sub)?)
}
_ => Self::Unknown(UnknownExtension::read(typ, &mut sub)),
};
@ -1175,16 +1181,7 @@ pub struct ServerHelloPayload {
impl Codec<'_> for ServerHelloPayload {
fn encode(&self, bytes: &mut Vec<u8>) {
self.legacy_version.encode(bytes);
self.random.encode(bytes);
self.session_id.encode(bytes);
self.cipher_suite.encode(bytes);
self.compression_method.encode(bytes);
if !self.extensions.is_empty() {
self.extensions.encode(bytes);
}
self.payload_encode(bytes, Encoding::Standard)
}
// minus version and random, which have already been read.
@ -1257,6 +1254,31 @@ impl ServerHelloPayload {
_ => None,
}
}
fn payload_encode(&self, bytes: &mut Vec<u8>, encoding: Encoding) {
self.legacy_version.encode(bytes);
match encoding {
// Standard encoding encodes the random value as is.
Encoding::Standard => self.random.encode(bytes),
// When encoding a ServerHello for ECH confirmation, the random value
// has the last 8 bytes zeroed out.
Encoding::EchConfirmation => {
// Indexing safety: self.random is 32 bytes long by definition.
let rand_vec = self.random.get_encoding();
bytes.extend_from_slice(&rand_vec.as_slice()[..24]);
bytes.extend_from_slice(&[0u8; 8]);
}
}
self.session_id.encode(bytes);
self.cipher_suite.encode(bytes);
self.compression_method.encode(bytes);
if !self.extensions.is_empty() {
self.extensions.encode(bytes);
}
}
}
#[derive(Clone, Default, Debug)]
@ -1839,6 +1861,14 @@ pub(crate) trait HasServerExtensions {
}
}
fn server_ech_extension(&self) -> Option<ServerEncryptedClientHello> {
let ext = self.find_extension(ExtensionType::EncryptedClientHello)?;
match ext {
ServerExtension::EncryptedClientHello(ech) => Some(ech.clone()),
_ => None,
}
}
fn early_data_extension_offered(&self) -> bool {
self.find_extension(ExtensionType::EarlyData)
.is_some()
@ -2303,15 +2333,7 @@ pub struct HandshakeMessagePayload<'a> {
impl<'a> Codec<'a> for HandshakeMessagePayload<'a> {
fn encode(&self, bytes: &mut Vec<u8>) {
// output type, length, and encoded payload
match self.typ {
HandshakeType::HelloRetryRequest => HandshakeType::ServerHello,
_ => self.typ,
}
.encode(bytes);
let nested = LengthPrefixedBuffer::new(ListLength::U24 { max: usize::MAX }, bytes);
self.payload.encode(nested.buf);
self.payload_encode(bytes, Encoding::Standard);
}
fn read(r: &mut Reader<'a>) -> Result<Self, InvalidMessage> {
@ -2444,6 +2466,27 @@ impl<'a> HandshakeMessagePayload<'a> {
ret
}
pub(crate) fn payload_encode(&self, bytes: &mut Vec<u8>, encoding: Encoding) {
// output type, length, and encoded payload
match self.typ {
HandshakeType::HelloRetryRequest => HandshakeType::ServerHello,
_ => self.typ,
}
.encode(bytes);
let nested = LengthPrefixedBuffer::new(ListLength::U24 { max: usize::MAX }, bytes);
match &self.payload {
// for Server Hello payloads we need to encode the payload differently
// based on the purpose of the encoding.
HandshakePayload::ServerHello(payload) => payload.payload_encode(nested.buf, encoding),
// TODO(@cpu): We will also need to handle HelloRetryRequests differently for HRR
// ECH confirmation.
// All other payload types are encoded the same regardless of purpose.
_ => self.payload.encode(nested.buf),
}
}
pub(crate) fn build_handshake_hash(hash: &[u8]) -> Self {
Self {
typ: HandshakeType::MessageHash,
@ -2734,6 +2777,39 @@ impl Codec<'_> for EncryptedClientHelloOuter {
}
}
/// Representation of the ECHEncryptedExtensions 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 struct ServerEncryptedClientHello {
pub(crate) retry_configs: Vec<EchConfig>,
}
impl Codec<'_> for ServerEncryptedClientHello {
fn encode(&self, bytes: &mut Vec<u8>) {
self.retry_configs.encode(bytes);
}
fn read(r: &mut Reader) -> Result<Self, InvalidMessage> {
Ok(Self {
retry_configs: Vec::<EchConfig>::read(r)?,
})
}
}
/// The method of encoding to use for a handshake message.
///
/// In some cases a handshake message may be encoded differently depending on the purpose
/// the encoded message is being used for. For example, a [ServerHelloPayload] may be encoded
/// with the last 8 bytes of the random zeroed out when being encoded for ECH confirmation.
pub(crate) enum Encoding {
/// Standard RFC 8446 encoding.
Standard,
/// Encoding for ECH confirmation.
EchConfirmation,
}
fn has_duplicates<I: IntoIterator<Item = E>, E: Into<T>, T: Eq + Ord>(iter: I) -> bool {
let mut seen = BTreeSet::new();

View File

@ -23,6 +23,7 @@ enum SecretKind {
ExporterMasterSecret,
ResumptionMasterSecret,
DerivedSecret,
ServerEchConfirmationSecret,
}
impl SecretKind {
@ -38,6 +39,8 @@ impl SecretKind {
ExporterMasterSecret => b"exp master",
ResumptionMasterSecret => b"res master",
DerivedSecret => b"derived",
// https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-18#section-7.2
ServerEchConfirmationSecret => b"ech accept confirmation",
}
}
@ -207,6 +210,30 @@ impl KeyScheduleHandshakeStart {
new
}
pub(crate) fn server_ech_confirmation_secret(
&mut self,
client_hello_inner_random: &[u8],
hs_hash: hash::Output,
) -> [u8; 8] {
/*
Per ietf-tls-esni-17 section 7.2:
<https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-17#section-7.2>
accept_confirmation = HKDF-Expand-Label(
HKDF-Extract(0, ClientHelloInner.random),
"ech accept confirmation",
transcript_ech_conf,8)
*/
hkdf_expand_label(
self.ks
.suite
.hkdf_provider
.extract_from_secret(None, client_hello_inner_random)
.as_ref(),
SecretKind::ServerEchConfirmationSecret.to_bytes(),
hs_hash.as_ref(),
)
}
fn into_handshake(
self,
hs_hash: hash::Output,