Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tests: support multiple HTTPS RRs for ECH configs #504

Merged
merged 2 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,8 @@ impl rustls_client_config_builder {
///
/// The provided `ech_config_list_bytes` and `rustls_hpke` must not be NULL or an
/// error will be returned. The caller maintains ownership of the ECH config list TLS bytes
/// and `rustls_hpke` instance.
/// and `rustls_hpke` instance. This function does not retain any reference to
/// `ech_config_list_bytes`.
///
/// A `RUSTLS_RESULT_BUILDER_INCOMPATIBLE_TLS_VERSIONS` error is returned if the builder's
/// TLS versions have been customized via `rustls_client_config_builder_new_custom()`
Expand Down
3 changes: 2 additions & 1 deletion src/rustls.h
Original file line number Diff line number Diff line change
Expand Up @@ -1768,7 +1768,8 @@ rustls_result rustls_client_config_builder_set_key_log(struct rustls_client_conf
*
* The provided `ech_config_list_bytes` and `rustls_hpke` must not be NULL or an
* error will be returned. The caller maintains ownership of the ECH config list TLS bytes
* and `rustls_hpke` instance.
* and `rustls_hpke` instance. This function does not retain any reference to
* `ech_config_list_bytes`.
*
* A `RUSTLS_RESULT_BUILDER_INCOMPATIBLE_TLS_VERSIONS` error is returned if the builder's
* TLS versions have been customized via `rustls_client_config_builder_new_custom()`
Expand Down
93 changes: 69 additions & 24 deletions tests/client.c
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,9 @@ main(int argc, const char **argv)
config_builder = rustls_client_config_builder_new();
}

if(getenv("RUSTLS_ECH_GREASE")) {
const char *rustls_ech_grease = getenv("RUSTLS_ECH_GREASE");
const char *rustls_ech_config_list = getenv("RUSTLS_ECH_CONFIG_LIST");
if(rustls_ech_grease) {
const rustls_hpke *hpke = rustls_supported_hpke();
if(hpke == NULL) {
fprintf(stderr, "client: no HPKE suites for ECH available\n");
Expand All @@ -489,39 +491,82 @@ main(int argc, const char **argv)
}
fprintf(stderr, "configured for ECH GREASE\n");
}
else if(getenv("RUSTLS_ECH_CONFIG_LIST")) {
else if(rustls_ech_config_list) {
const rustls_hpke *hpke = rustls_supported_hpke();
if(hpke == NULL) {
fprintf(stderr, "client: no HPKE suites for ECH available\n");
goto cleanup;
}

char ech_config_list_buf[10000];
size_t ech_config_list_len;

unsigned int read_result = read_file(getenv("RUSTLS_ECH_CONFIG_LIST"),
ech_config_list_buf,
sizeof(ech_config_list_buf),
&ech_config_list_len);
if(read_result != DEMO_OK) {
fprintf(stderr,
"client: failed to read ECH config list file: '%s'\n",
getenv("RUSTLS_ECH_CONFIG_LIST"));
// Duplicate the ENV var value - calling STRTOK_R will modify the string
// to add null terminators between tokens.
char *ech_config_list_copy = strdup(rustls_ech_config_list);
if(!ech_config_list_copy) {
LOG_SIMPLE("failed to allocate memory for ECH config list");
goto cleanup;
}
result =
rustls_client_config_builder_enable_ech(config_builder,
(uint8_t *)ech_config_list_buf,
ech_config_list_len,
hpke);
if(result != RUSTLS_RESULT_OK) {
fprintf(stderr, "client: failed to configure ECH");
goto cleanup;

bool ech_configured = false;
// Tokenize the ech_config_list_copy by comma. The first invocation takes
// ech_config_list_copy. This is reentrant by virtue of saving state to
// saveptr. Only the _first_ invocation is given the original string.
// Subsequent calls should pass NULL and the same delim/saveptr.
const char *delim = ",";
char *saveptr = NULL;
char *ech_config_list_path =
STRTOK_R(ech_config_list_copy, delim, &saveptr);

while(ech_config_list_path) {
// Skip leading spaces
while(*ech_config_list_path == ' ') {
ech_config_list_path++;
}

// Try to read the token as a file path to an ECH config list.
char ech_config_list_buf[10000];
size_t ech_config_list_len;
const enum demo_result read_result =
read_file(ech_config_list_path,
ech_config_list_buf,
sizeof(ech_config_list_buf),
&ech_config_list_len);

// If we can't read the file, warn and continue
if(read_result != DEMO_OK) {
// Continue to the next token.
LOG("unable to read ECH config list from '%s'", ech_config_list_path);
ech_config_list_path = STRTOK_R(NULL, delim, &saveptr);
continue;
}

// Try to enable ECH with the config list. This may error if none
// of the ECH configs are valid/compatible.
result =
rustls_client_config_builder_enable_ech(config_builder,
(uint8_t *)ech_config_list_buf,
ech_config_list_len,
hpke);
cpu marked this conversation as resolved.
Show resolved Hide resolved

// If we successfully configured ECH with the config list then break.
if(result == RUSTLS_RESULT_OK) {
LOG("using ECH with config list from '%s'", ech_config_list_path);
ech_configured = true;
break;
}

// Otherwise continue to the next token.
LOG("no compatible/valid ECH configs found in '%s'",
ech_config_list_path);
ech_config_list_path = STRTOK_R(NULL, delim, &saveptr);
}

fprintf(stderr,
"client: using ECH with config list from '%s'\n",
getenv("RUSTLS_ECH_CONFIG_LIST"));
// Free the copy of the env var we made.
free(ech_config_list_copy);

if(!ech_configured) {
LOG_SIMPLE("failed to configure ECH with any provided config files");
goto cleanup;
}
}

if(getenv("RUSTLS_PLATFORM_VERIFIER")) {
Expand Down
6 changes: 6 additions & 0 deletions tests/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ const char *ws_strerror(int err);
#endif /* !STDOUT_FILENO */
#endif /* _WIN32 */

#if defined(_MSC_VER)
#define STRTOK_R strtok_s
#else
#define STRTOK_R strtok_r
#endif

enum demo_result
{
DEMO_OK,
Expand Down
69 changes: 51 additions & 18 deletions tests/ech_fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use std::io::Write;
use hickory_resolver::config::{ResolverConfig, ResolverOpts};
use hickory_resolver::proto::rr::rdata::svcb::{SvcParamKey, SvcParamValue};
use hickory_resolver::proto::rr::{RData, RecordType};
use hickory_resolver::{Resolver, TokioResolver};
use hickory_resolver::{ResolveError, Resolver, TokioResolver};

use rustls::pki_types::EchConfigListBytes;

Expand All @@ -24,27 +24,60 @@ async fn main() -> Result<(), Box<dyn Error>> {
.unwrap_or(format!("testdata/{}.ech.configs.bin", domain));

let resolver = Resolver::tokio(ResolverConfig::google_https(), ResolverOpts::default());
let tls_encoded_list = lookup_ech(&resolver, &domain).await;

let mut encoded_list_file = File::create(output_path)?;
encoded_list_file.write_all(&tls_encoded_list)?;
let all_lists = lookup_ech_configs(&resolver, &domain).await?;

// If there was only one HTTPS record with an ech config, write it to the output file.
if all_lists.len() == 1 {
let mut encoded_list_file = File::create(&output_path)?;
encoded_list_file.write_all(&all_lists.first().unwrap())?;
println!("{output_path}");
} else {
// Otherwise write each to its own file with a numeric suffix
for (i, ech_config_lists) in all_lists.iter().enumerate() {
let mut encoded_list_file = File::create(format!("{output_path}.{}", i + 1))?;
encoded_list_file.write_all(&ech_config_lists)?;
}
// And print a comma separated list of the file paths.
let paths = (1..=all_lists.len())
.map(|i| format!("{}.{}", output_path, i))
.collect::<Vec<_>>()
.join(",");
println!("{paths}")
}

Ok(())
}

async fn lookup_ech(resolver: &TokioResolver, domain: &str) -> EchConfigListBytes<'static> {
resolver
.lookup(domain, RecordType::HTTPS)
.await
.expect("failed to lookup HTTPS record type")
.record_iter()
.find_map(|r| match r.data() {
RData::HTTPS(svcb) => svcb.svc_params().iter().find_map(|sp| match sp {
(SvcParamKey::EchConfigList, SvcParamValue::EchConfigList(e)) => Some(e.clone().0),
_ => None,
}),
/// Collect up all `EchConfigListBytes` found in the HTTPS record(s) for a given domain name.
///
/// Assumes the port will be 443. For a more complete example see the Rustls' ech-client.rs
/// example's `lookup_ech_configs` function.
///
/// The domain name should be the **inner** name used for Encrypted Client Hello (ECH). The
/// lookup is done using DNS-over-HTTPS to protect that inner name from being disclosed in
/// plaintext ahead of the TLS handshake that negotiates ECH for the inner name.
///
/// Returns an empty vec if no HTTPS records with ECH configs are found.
async fn lookup_ech_configs(
resolver: &TokioResolver,
domain: &str,
) -> Result<Vec<EchConfigListBytes<'static>>, ResolveError> {
let lookup = resolver.lookup(domain, RecordType::HTTPS).await?;

let mut ech_config_lists = Vec::new();
for r in lookup.record_iter() {
let RData::HTTPS(svcb) = r.data() else {
continue;
};

ech_config_lists.extend(svcb.svc_params().iter().find_map(|sp| match sp {
(SvcParamKey::EchConfigList, SvcParamValue::EchConfigList(e)) => {
Some(EchConfigListBytes::from(e.clone().0))
}
_ => None,
})
.expect("missing expected HTTPS SvcParam EchConfig record")
.into()
}))
}

Ok(ech_config_lists)
}
Loading