feat: support underscore-prefixed domains with self-signed TLS certificates

Allow Delta Chat core to work with chatmail servers running on
underscore-prefixed domains (e.g. _alice.localchat) which use
self-signed TLS certificates. This is mirroring related work
on chatmail relays: https://github.com/chatmail/relay/pull/855
Underscore domains with self-signed TLS certs can be used by LXC test
containers where obtaining real certificates is not practical.

When the domain starts with '_', certificate verification is
automatically relaxed for IMAP/SMTP connections, dcaccount QR
code handling, and iroh relay endpoints. The Python test suite
is adapted to also work against such underscore-domain servers,
including cross-core tests with older Delta Chat versions.

Note: this PR does not support HTTPS requests with underscore
domains. They are not currently needed for working with LXC test
containers.

14 files changed, +102/-31 lines (excluding Cargo.lock).
Cargo.lock: +606/-11 lines from enabling iroh features
needed for connecting to iroh relay endpoint on underscore domains.
The added dependencies are unfortunate but best considered
when finally upgrading to iroh 1.0 (tm).
This commit is contained in:
holger krekel
2026-03-01 23:51:44 +01:00
committed by link2xt
parent bcaf1284e2
commit 1b860372cc
15 changed files with 733 additions and 69 deletions

View File

@@ -581,7 +581,13 @@ async fn get_configured_param(
smtp_password,
provider,
certificate_checks: match param.certificate_checks {
EnteredCertificateChecks::Automatic => ConfiguredCertificateChecks::Automatic,
EnteredCertificateChecks::Automatic => {
if param_domain.starts_with('_') {
ConfiguredCertificateChecks::AcceptInvalidCertificates
} else {
ConfiguredCertificateChecks::Automatic
}
}
EnteredCertificateChecks::Strict => ConfiguredCertificateChecks::Strict,
EnteredCertificateChecks::AcceptInvalidCertificates
| EnteredCertificateChecks::AcceptInvalidCertificates2 => {

View File

@@ -238,18 +238,21 @@ impl Context {
let secret_key = SecretKey::generate(rand_old::rngs::OsRng);
let public_key = secret_key.public();
let relay_mode = if let Some(relay_url) = self
let (relay_mode, skip_relay_tls) = if let Some(relay_url) = self
.metadata
.read()
.await
.as_ref()
.and_then(|conf| conf.iroh_relay.clone())
{
RelayMode::Custom(RelayUrl::from(relay_url).into())
// Underscore-prefixed domains use self-signed TLS certificates,
// so we need to skip relay certificate verification for them.
let skip = relay_url.host_str().map_or(false, |h| h.starts_with('_'));
(RelayMode::Custom(RelayUrl::from(relay_url).into()), skip)
} else {
// FIXME: this should be RelayMode::Disabled instead.
// Currently using default relays because otherwise Rust tests fail.
RelayMode::Default
(RelayMode::Default, false)
};
let endpoint = Endpoint::builder()
@@ -257,6 +260,7 @@ impl Context {
.secret_key(secret_key)
.alpns(vec![GOSSIP_ALPN.to_vec()])
.relay_mode(relay_mode)
.insecure_skip_relay_cert_verify(skip_relay_tls)
.bind()
.await?;

View File

@@ -817,6 +817,11 @@ pub(crate) async fn login_param_from_account_qr(
.context("Invalid DCACCOUNT scheme")?;
if !payload.starts_with(HTTPS_SCHEME) {
let certificate_checks = if payload.starts_with('_') {
EnteredCertificateChecks::AcceptInvalidCertificates
} else {
EnteredCertificateChecks::Strict
};
let rng = &mut rand::rngs::OsRng.unwrap_err();
let username = Alphanumeric.sample_string(rng, 9);
let addr = username + "@" + payload;
@@ -829,7 +834,7 @@ pub(crate) async fn login_param_from_account_qr(
..Default::default()
},
smtp: Default::default(),
certificate_checks: EnteredCertificateChecks::Strict,
certificate_checks,
oauth2: false,
};
return Ok(param);

View File

@@ -754,6 +754,35 @@ async fn test_decode_empty_account() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_account_underscore_domain() -> Result<()> {
let ctx = TestContext::new().await;
// Underscore domain is kept as-is.
let qr = check_qr(&ctx.ctx, "dcaccount:_example.org").await?;
assert_eq!(
qr,
Qr::Account {
domain: "_example.org".to_string()
}
);
// Verify login params use AcceptInvalidCertificates for underscore domain.
let param = login_param_from_account_qr(&ctx.ctx, "dcaccount:_example.org").await?;
assert!(param.addr.ends_with("@_example.org"));
assert_eq!(
param.certificate_checks,
EnteredCertificateChecks::AcceptInvalidCertificates
);
// Regular domain still uses Strict.
let param = login_param_from_account_qr(&ctx.ctx, "dcaccount:example.org").await?;
assert!(param.addr.ends_with("@example.org"));
assert_eq!(param.certificate_checks, EnteredCertificateChecks::Strict);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_decode_tg_socks_proxy() -> Result<()> {
let t = TestContext::new().await;