mirror of
https://github.com/ducaale/xh.git
synced 2025-05-05 15:32:50 +00:00
Improve rustls error messages for invalid certificates
After a recent release rustls provides better error messages for invalid certificates. For example: ``` invalid peer certificate: certificate not valid for name "wrong.host.badssl.com"; certificate is only valid for DnsName("*.badssl.com") or DnsName("badssl.com") ``` The message for expired certificates still isn't too readable but the error now contains timestamps so we enhance it ourselves: ``` xh: error: error sending request for url (https://expired.badssl.com/) Caused by: 0: client error (Connect) 1: invalid peer certificate: certificate expired: verification time 1742381579 (UNIX), but certificate is not valid after 1428883199 (313498380 seconds ago) Certificate not valid after 2015-04-12 23:59:59.0 +00:00:00 (9years 11months 6days 8h 43m 24s ago). ```
This commit is contained in:
parent
7ad28aa483
commit
300203338f
13
Cargo.lock
generated
13
Cargo.lock
generated
@ -814,9 +814,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "humantime"
|
||||
version = "2.1.0"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||
checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
@ -1760,9 +1760,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.23"
|
||||
version = "0.23.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395"
|
||||
checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
@ -1805,9 +1805,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.102.8"
|
||||
version = "0.103.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
|
||||
checksum = "0aa4eeac2588ffff23e9d7a7e9b3f971c5fb5b7ebc9452745e0c232c64f83b2f"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
@ -2798,6 +2798,7 @@ dependencies = [
|
||||
"flate2",
|
||||
"form_urlencoded",
|
||||
"http-body-util",
|
||||
"humantime",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"indicatif",
|
||||
|
@ -48,6 +48,7 @@ serde_urlencoded = "0.7.0"
|
||||
supports-hyperlinks = "3.0.0"
|
||||
termcolor = "1.1.2"
|
||||
time = "0.3.16"
|
||||
humantime = "2.2.0"
|
||||
unicode-width = "0.1.9"
|
||||
url = "2.2.2"
|
||||
ruzstd = { version = "0.7", default-features = false, features = ["std"]}
|
||||
@ -56,7 +57,7 @@ log = "0.4.21"
|
||||
|
||||
# Enable logging in transitive dependencies.
|
||||
# The rustls version number should be kept in sync with hyper/reqwest.
|
||||
rustls = { version = "0.23.14", optional = true, default-features = false, features = ["logging"] }
|
||||
rustls = { version = "0.23.25", optional = true, default-features = false, features = ["logging"] }
|
||||
tracing = { version = "0.1.41", default-features = false, features = ["log"] }
|
||||
reqwest_cookie_store = { version = "0.8.0", features = ["serde"] }
|
||||
|
||||
|
@ -1,5 +1,63 @@
|
||||
use std::process::ExitCode;
|
||||
|
||||
pub(crate) fn additional_messages(err: &anyhow::Error, native_tls: bool) -> Vec<String> {
|
||||
let mut msgs = Vec::new();
|
||||
|
||||
#[cfg(feature = "rustls")]
|
||||
msgs.extend(format_rustls_error(err));
|
||||
|
||||
if native_tls && err.root_cause().to_string() == "invalid minimum TLS version for backend" {
|
||||
msgs.push("Try running without the --native-tls flag.".into());
|
||||
}
|
||||
|
||||
msgs
|
||||
}
|
||||
|
||||
/// Format certificate expired/not valid yet messages. By default these print
|
||||
/// human-unfriendly Unix timestamps.
|
||||
///
|
||||
/// Other rustls error messages (e.g. wrong host) are readable enough.
|
||||
#[cfg(feature = "rustls")]
|
||||
fn format_rustls_error(err: &anyhow::Error) -> Option<String> {
|
||||
use humantime::format_duration;
|
||||
use rustls::pki_types::UnixTime;
|
||||
use rustls::CertificateError;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
// Multiple layers of io::Error for some reason?
|
||||
// This may be fragile
|
||||
let err = err.root_cause().downcast_ref::<std::io::Error>()?;
|
||||
let err = err.get_ref()?.downcast_ref::<std::io::Error>()?;
|
||||
let err = err.get_ref()?.downcast_ref::<rustls::Error>()?;
|
||||
let rustls::Error::InvalidCertificate(err) = err else {
|
||||
return None;
|
||||
};
|
||||
|
||||
fn conv_time(unix_time: &UnixTime) -> Option<OffsetDateTime> {
|
||||
OffsetDateTime::from_unix_timestamp(unix_time.as_secs() as i64).ok()
|
||||
}
|
||||
|
||||
match err {
|
||||
CertificateError::ExpiredContext { time, not_after } => {
|
||||
let time = conv_time(time)?;
|
||||
let not_after = conv_time(not_after)?;
|
||||
let diff = format_duration((time - not_after).try_into().ok()?);
|
||||
Some(format!(
|
||||
"Certificate not valid after {not_after} ({diff} ago).",
|
||||
))
|
||||
}
|
||||
CertificateError::NotValidYetContext { time, not_before } => {
|
||||
let time = conv_time(time)?;
|
||||
let not_before = conv_time(not_before)?;
|
||||
let diff = format_duration((not_before - time).try_into().ok()?);
|
||||
Some(format!(
|
||||
"Certificate not valid before {not_before} ({diff} from now).",
|
||||
))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn exit_code(err: &anyhow::Error) -> ExitCode {
|
||||
if let Some(err) = err.downcast_ref::<reqwest::Error>() {
|
||||
if err.is_timeout() {
|
||||
|
@ -82,11 +82,12 @@ fn main() -> ExitCode {
|
||||
Err(err) => {
|
||||
log::debug!("{err:#?}");
|
||||
eprintln!("{bin_name}: error: {err:?}");
|
||||
let msg = err.root_cause().to_string();
|
||||
if native_tls && msg == "invalid minimum TLS version for backend" {
|
||||
|
||||
for message in error_reporting::additional_messages(&err, native_tls) {
|
||||
eprintln!();
|
||||
eprintln!("Try running without the --native-tls flag.");
|
||||
eprintln!("{message}");
|
||||
}
|
||||
|
||||
error_reporting::exit_code(&err)
|
||||
}
|
||||
}
|
||||
|
10
tests/cli.rs
10
tests/cli.rs
@ -1213,6 +1213,16 @@ fn cert_without_key() {
|
||||
.stderr(predicates::str::is_empty());
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "rustls", feature = "online-tests"))]
|
||||
#[test]
|
||||
fn formatted_certificate_expired_message() {
|
||||
get_command()
|
||||
.arg("https://expired.badssl.com")
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(contains("Certificate not valid after 2015-04-12"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn override_dns_resolution() {
|
||||
let server = server::http(|req| async move {
|
||||
|
Loading…
x
Reference in New Issue
Block a user