Scope --allow-invalid-certs to the server the user named
ci / test (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 2m33s
ci / announce (pull_request) Skipped

The flag switched certificate checks off for every connection in the run.
That included the Microsoft sign-in endpoints, so a user passing it for a
self-signed source also sent refresh tokens, device codes and EWS client
secrets over unverified TLS. It also covered the export target, and any host
a server redirected to or named for its API, uploads or downloads.

It now applies only where the user pointed it: the host of --url, for the
source of an import or the target of an export. For an Exchange import with
no --url, it covers the mailbox's own domain, where on-premises Autodiscover
looks, and then only the EWS endpoint Autodiscover finds. The Microsoft and
Google sign-in and cloud hosts are always verified, with or without the flag.

Each HTTP client keeps a verifying agent and, only when the flag applies, a
second one that accepts invalid certificates, and picks per request by host.
The sign-in modules no longer take the flag at all. Autodiscover v2, which is
Microsoft's own service, is always verified.
This commit is contained in:
2026-09-30 11:31:44 -07:00
parent eb38603dbc
commit 234b3203d7
22 changed files with 549 additions and 184 deletions
+35 -15
View File
@@ -21,7 +21,7 @@ use crate::jmap::error::JmapError;
use crate::jmap::http::{Auth, RetryPolicy, retry_after_header};
use crate::jmap::retry::{self, RateLimitState};
use crate::logging::{HttpCall, LEVEL_BODIES, LEVEL_DEFAULT, LEVEL_PROGRESS, Logger};
use crate::net::{tls, with_timeouts};
use crate::net::{CertOverride, tls, with_timeouts};
const MAX_BODY: u64 = 512 * 1024 * 1024;
const LONG_RETRY_THRESHOLD: Duration = Duration::from_secs(10);
@@ -48,6 +48,8 @@ pub struct MultiStatus {
struct Inner {
agent: Agent,
lax_agent: Option<Agent>,
certs: CertOverride,
auth: Auth,
retry: RetryPolicy,
rate_limit: RateLimitState,
@@ -57,25 +59,42 @@ struct Inner {
user_agent: String,
}
impl Inner {
/// The agent for `url`: the one that accepts invalid certificates only for
/// a host `--allow-invalid-certs` covers, and the verifying one otherwise.
fn agent_for(&self, url: &str) -> &Agent {
match &self.lax_agent {
Some(lax) if self.certs.allows(url) => lax,
_ => &self.agent,
}
}
}
#[derive(Clone)]
pub struct DavClient {
inner: Arc<Inner>,
}
impl DavClient {
pub fn new(auth: Auth, retry: RetryPolicy, allow_invalid_certs: bool) -> Self {
let config: Config = with_timeouts!(
Config::builder()
.http_status_as_error(false)
.allow_non_standard_methods(true)
.max_redirects(0)
.redirect_auth_headers(RedirectAuthHeaders::SameHost)
.tls_config(tls(allow_invalid_certs))
)
.build();
pub fn new(auth: Auth, retry: RetryPolicy, certs: CertOverride) -> Self {
let build = |accept_invalid: bool| -> Agent {
let config: Config = with_timeouts!(
Config::builder()
.http_status_as_error(false)
.allow_non_standard_methods(true)
.max_redirects(0)
.redirect_auth_headers(RedirectAuthHeaders::SameHost)
.tls_config(tls(accept_invalid))
)
.build();
config.new_agent()
};
let lax_agent = certs.is_active().then(|| build(true));
DavClient {
inner: Arc::new(Inner {
agent: config.new_agent(),
agent: build(false),
lax_agent,
certs,
auth,
retry,
rate_limit: RateLimitState::new(),
@@ -816,7 +835,7 @@ impl DavClient {
let request = builder
.body(payload)
.map_err(|e| ureq::Error::Other(Box::new(std::io::Error::other(e))))?;
self.inner.agent.run(request)
self.inner.agent_for(req.url).run(request)
}
}
@@ -935,6 +954,7 @@ fn truncate(body: &[u8]) -> String {
#[cfg(test)]
mod tests {
use super::*;
use crate::net::CertOverride;
#[test]
fn every_timeout_is_a_retryable_transport_error() {
@@ -962,7 +982,7 @@ mod tests {
password: "p".into(),
},
RetryPolicy::new(3),
false,
CertOverride::none(),
);
assert_eq!(c.retries_observed(), 0);
assert_eq!(c.retry_after_sleeps(), 0);
@@ -975,7 +995,7 @@ mod tests {
token: "abc".into(),
},
RetryPolicy::new(0),
false,
CertOverride::none(),
);
let logger = c.logger();
assert_eq!(logger.level(), LEVEL_DEFAULT);