From: Dominik Csapak <d.csapak@proxmox.com>
To: pve-devel@lists.proxmox.com, pbs-devel@lists.proxmox.com
Subject: [PATCH proxmox-backup v3 4/6] pbs-client: use proxmox-https openssl callback
Date: Wed, 17 Jun 2026 10:59:16 +0200 [thread overview]
Message-ID: <20260617085949.1528300-5-d.csapak@proxmox.com> (raw)
In-Reply-To: <20260617085949.1528300-1-d.csapak@proxmox.com>
instead of implementing it here. This changes the behavior when giving a
fingerprint explicitly when the certificate chain is trusted by openssl.
Previously this would be accepted due to openssls checks, regardless if
the given fingerprint would match or not.
With this patch, a given fingerprint has higher priority than openssls
validation.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
Cargo.toml | 2 +-
pbs-client/src/http_client.rs | 149 ++++++++++++++--------------------
2 files changed, 61 insertions(+), 90 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index dc8e2730c..7962398f1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -64,7 +64,7 @@ proxmox-config-digest = "1"
proxmox-daemon = "1"
proxmox-fuse = "3"
proxmox-docgen = "1"
-proxmox-http = { version = "1.0.2", features = [ "client", "http-helpers", "api-types", "websocket" ] } # see below
+proxmox-http = { version = "1.0.2", features = [ "client", "http-helpers", "api-types", "websocket", "tls" ] } # see below
proxmox-human-byte = "1"
proxmox-io = "1.0.1" # tools and client use "tokio" feature
proxmox-lang = "1.1"
diff --git a/pbs-client/src/http_client.rs b/pbs-client/src/http_client.rs
index a6fcafcfd..ea6e5d7fa 100644
--- a/pbs-client/src/http_client.rs
+++ b/pbs-client/src/http_client.rs
@@ -15,10 +15,7 @@ use hyper::http::{Request, Response};
use hyper_util::client::legacy::connect::dns::GaiResolver;
use hyper_util::client::legacy::{Client, connect::HttpConnector};
use hyper_util::rt::{TokioExecutor, TokioIo};
-use openssl::{
- ssl::{SslConnector, SslMethod},
- x509::X509StoreContextRef,
-};
+use openssl::ssl::{SslConnector, SslMethod};
use percent_encoding::percent_encode;
use serde_json::{Value, json};
use xdg::BaseDirectories;
@@ -29,9 +26,9 @@ use proxmox_sys::linux::tty;
use proxmox_async::broadcast_future::BroadcastFuture;
use proxmox_http::Body;
-use proxmox_http::ProxyConfig;
use proxmox_http::client::HttpsConnector;
use proxmox_http::uri::{build_authority, json_object_to_query};
+use proxmox_http::{ProxyConfig, SslVerifyError, openssl_verify_callback};
use proxmox_log::{error, info, warn};
use proxmox_rate_limiter::RateLimiter;
@@ -425,30 +422,42 @@ impl HttpClient {
let interactive = options.interactive;
let fingerprint_cache = options.fingerprint_cache;
let prefix = options.prefix.clone();
- let trust_openssl_valid = Arc::new(Mutex::new(true));
ssl_connector_builder.set_verify_callback(
openssl::ssl::SslVerifyMode::PEER,
- move |valid, ctx| match Self::verify_callback(
+ move |valid, ctx| match openssl_verify_callback(
valid,
ctx,
- expected_fingerprint.as_ref(),
- interactive,
- Arc::clone(&trust_openssl_valid),
+ expected_fingerprint.as_deref(),
) {
- Ok(None) => true,
- Ok(Some(fingerprint)) => {
- if fingerprint_cache {
- if let Some(ref prefix) = prefix {
- if let Err(err) = store_fingerprint(prefix, &server, &fingerprint) {
- error!("{}", err);
+ Ok(()) => true,
+ Err(err) => {
+ match err {
+ SslVerifyError::NoCertificate => error!(
+ "certificate validation failed - context lacks current certificate"
+ ),
+ SslVerifyError::InvalidFingerprint(error_stack) => {
+ error!("certificate validation failed - failed to calculate FP - {error_stack}")
+ },
+ SslVerifyError::UntrustedCertificate { fingerprint } => {
+ if interactive && std::io::stdin().is_terminal() {
+ match Self::interactive_fp_check(prefix.as_deref(), &server, verified_fingerprint.clone(), fingerprint_cache, fingerprint) {
+ Ok(()) => return true,
+ Err(err) => error!("certificate validation failed - {err}"),
+ }
}
}
+ SslVerifyError::FingerprintMismatch { fingerprint, expected } => {
+ warn!("WARNING: certificate fingerprint does not match expected fingerprint!");
+ warn!("expected: {expected}");
+
+ if interactive && std::io::stdin().is_terminal() {
+ match Self::interactive_fp_check(prefix.as_deref(), &server, verified_fingerprint.clone(), fingerprint_cache, fingerprint) {
+ Ok(()) => return true,
+ Err(err) => error!("certificate validation failed - {err}"),
+ }
+ }
+ },
}
- *verified_fingerprint.lock().unwrap() = Some(fingerprint);
- true
- }
- Err(err) => {
- error!("certificate validation failed - {}", err);
false
}
},
@@ -661,79 +670,41 @@ impl HttpClient {
bail!("no password input mechanism available");
}
- fn verify_callback(
- openssl_valid: bool,
- ctx: &mut X509StoreContextRef,
- expected_fingerprint: Option<&String>,
- interactive: bool,
- trust_openssl: Arc<Mutex<bool>>,
- ) -> Result<Option<String>, Error> {
- let mut trust_openssl_valid = trust_openssl.lock().unwrap();
-
- // we can only rely on openssl's prevalidation if we haven't forced it earlier
- if openssl_valid && *trust_openssl_valid {
- return Ok(None);
- }
-
- let cert = match ctx.current_cert() {
- Some(cert) => cert,
- None => bail!("context lacks current certificate."),
- };
-
- // force trust in case of a chain, but set flag to no longer trust prevalidation by openssl
- if ctx.error_depth() > 0 {
- *trust_openssl_valid = false;
- return Ok(None);
- }
-
- // leaf certificate - if we end up here, we have to verify the fingerprint!
- let fp = match cert.digest(openssl::hash::MessageDigest::sha256()) {
- Ok(fp) => fp,
- Err(err) => bail!("failed to calculate certificate FP - {}", err), // should not happen
- };
- let fp_string = hex::encode(fp);
- let fp_string = fp_string
- .as_bytes()
- .chunks(2)
- .map(|v| std::str::from_utf8(v).unwrap())
- .collect::<Vec<&str>>()
- .join(":");
-
- if let Some(expected_fingerprint) = expected_fingerprint {
- let expected_fingerprint = expected_fingerprint.to_lowercase();
- if expected_fingerprint == fp_string {
- return Ok(Some(fp_string));
- } else {
- warn!("WARNING: certificate fingerprint does not match expected fingerprint!");
- warn!("expected: {}", expected_fingerprint);
- }
- }
-
- // If we're on a TTY, query the user
- if interactive && std::io::stdin().is_terminal() {
- info!("fingerprint: {}", fp_string);
- loop {
- eprint!("Are you sure you want to continue connecting? (y/n): ");
- let _ = std::io::stdout().flush();
- use std::io::{BufRead, BufReader};
- let mut line = String::new();
- match BufReader::new(std::io::stdin()).read_line(&mut line) {
- Ok(_) => {
- let trimmed = line.trim();
- if trimmed == "y" || trimmed == "Y" {
- return Ok(Some(fp_string));
- } else if trimmed == "n" || trimmed == "N" {
- bail!("Certificate fingerprint was not confirmed.");
- } else {
- continue;
+ fn interactive_fp_check(
+ prefix: Option<&str>,
+ server: &str,
+ verified_fingerprint: Arc<Mutex<Option<String>>>,
+ fingerprint_cache: bool,
+ fingerprint: String,
+ ) -> Result<(), Error> {
+ info!("fingerprint: {fingerprint}");
+ loop {
+ eprint!("Are you sure you want to continue connecting? (y/n): ");
+ let _ = std::io::stdout().flush();
+ use std::io::{BufRead, BufReader};
+ let mut line = String::new();
+ match BufReader::new(std::io::stdin()).read_line(&mut line) {
+ Ok(_) => {
+ let trimmed = line.trim();
+ if trimmed == "y" || trimmed == "Y" {
+ if fingerprint_cache && prefix.is_some() {
+ if let Err(err) =
+ store_fingerprint(prefix.unwrap(), server, &fingerprint)
+ {
+ error!("{}", err);
+ }
}
+ *verified_fingerprint.lock().unwrap() = Some(fingerprint);
+ return Ok(());
+ } else if trimmed == "n" || trimmed == "N" {
+ bail!("Certificate fingerprint was not confirmed.");
+ } else {
+ continue;
}
- Err(err) => bail!("Certificate fingerprint was not confirmed - {}.", err),
}
+ Err(err) => bail!("Certificate fingerprint was not confirmed - {}.", err),
}
}
-
- bail!("Certificate fingerprint was not confirmed.");
}
pub async fn request(&self, mut req: Request<Body>) -> Result<Value, Error> {
--
2.47.3
next prev parent reply other threads:[~2026-06-17 9:01 UTC|newest]
Thread overview: 7+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-17 8:59 [PATCH proxmox{,-backup,-websocket-tunnel} v3 0/6] unify openssl callback logic Dominik Csapak
2026-06-17 8:59 ` [PATCH proxmox v3 1/6] http: factor out openssl verification callback Dominik Csapak
2026-06-17 8:59 ` [PATCH proxmox v3 2/6] http: tls: use legacy behavior when PROXMOX_NEW_TLS_CHECK is not set Dominik Csapak
2026-06-17 8:59 ` [PATCH proxmox v3 3/6] client: use proxmox-http's openssl verification callback Dominik Csapak
2026-06-17 8:59 ` Dominik Csapak [this message]
2026-06-17 8:59 ` [PATCH proxmox-backup v3 5/6] pbs-client: honor already verified fingerprint Dominik Csapak
2026-06-17 8:59 ` [PATCH proxmox-websocket-tunnel v3 6/6] use proxmox-http's openssl callback Dominik Csapak
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260617085949.1528300-5-d.csapak@proxmox.com \
--to=d.csapak@proxmox.com \
--cc=pbs-devel@lists.proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.