* [pbs-devel] [PATCH proxmox{, -backup, -websocket-tunnel} 0/4] unify openssl callback logic
@ 2025-07-24 8:56 Dominik Csapak
2025-07-24 8:56 ` [pbs-devel] [PATCH proxmox 1/2] http: factor out openssl verification callback Dominik Csapak
` (3 more replies)
0 siblings, 4 replies; 8+ messages in thread
From: Dominik Csapak @ 2025-07-24 8:56 UTC (permalink / raw)
To: pbs-devel, pve-devel
There are currently 3 slightly different implementations of the openssl
verify callback in place. They differ in how an explicit fingerprint
would be checked:
* pbs-client: if verification was on, a valid certificate would trump a
wrong epxlicit fingerprint
* proxmox-websocket-tunnel: if an explicit fingerprint was given, it was
checked, regardless of the openssl result
* proxmox-client: the openssl validity had priority as in pbs-client,
but the fingerprint was not checked against the leaf certificate, but
agains all certificates in the chain (which would lead to false
negatives). Note that this is currently only used in PDM
This series aims to unify the general behavior, but design the interface
to be flexible enought to accomodate the different call sites needs.
I included the change of features for crates, but they have to be bumped
before hand of course and the version must be changed in Cargo.toml.
(if I should send that differently, please do tell how it should be done)
Since that is technically a breaking change for PBS, we should only
change that for the next major release.
Also, since it rather deep in the stack for PBS (remotes sync, etc.) and
PVE (remote migration) IMHO this is a series that should be tested very
well.
Further work could be to unify this behavior for our perl clients too,
but it seemed out of scope for this series. (notably the PVE::APIClient
and the client used in the SDN code)
I tried to implement some tests, but due to the openssl interface this
seems to be not really possible, except if we'd start a server + client
in the tests (which seems overkill). But if anyone has an idea how we
could test this code (and i mean not only it's interface, but the
openssl connection behavior), I'd be glad.
changes from v1:
* rebase on master (drops one patch)
* drop hex dependency
proxmox:
Dominik Csapak (2):
http: factor out openssl verification callback
client: use proxmox-http's openssl verification callback
Cargo.toml | 1 +
proxmox-client/Cargo.toml | 2 +-
proxmox-client/src/client.rs | 48 ++++-----------------
proxmox-http/Cargo.toml | 5 +++
proxmox-http/src/lib.rs | 5 +++
proxmox-http/src/tls.rs | 84 ++++++++++++++++++++++++++++++++++++
proxmox-openid/Cargo.toml | 2 +-
7 files changed, 105 insertions(+), 42 deletions(-)
create mode 100644 proxmox-http/src/tls.rs
proxmox-backup:
Dominik Csapak (1):
pbs-client: use proxmox-https openssl callback
Cargo.toml | 2 +-
pbs-client/src/http_client.rs | 151 ++++++++++++++--------------------
2 files changed, 62 insertions(+), 91 deletions(-)
proxmox-websocket-tunnel:
Dominik Csapak (1):
use proxmox-http's openssl callback
Cargo.toml | 3 +--
src/main.rs | 67 +++++++++++++++++++++--------------------------------
2 files changed, 28 insertions(+), 42 deletions(-)
Summary over all repositories:
11 files changed, 195 insertions(+), 175 deletions(-)
--
Generated by git-murpp 0.8.1
_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel
^ permalink raw reply [flat|nested] 8+ messages in thread
* [pbs-devel] [PATCH proxmox 1/2] http: factor out openssl verification callback
2025-07-24 8:56 [pbs-devel] [PATCH proxmox{, -backup, -websocket-tunnel} 0/4] unify openssl callback logic Dominik Csapak
@ 2025-07-24 8:56 ` Dominik Csapak
2025-07-24 8:56 ` [pbs-devel] [PATCH proxmox 2/2] client: use proxmox-http's " Dominik Csapak
` (2 subsequent siblings)
3 siblings, 0 replies; 8+ messages in thread
From: Dominik Csapak @ 2025-07-24 8:56 UTC (permalink / raw)
To: pbs-devel, pve-devel
with the 'tls' feature offers a callback method that can be used within
openssl's `set_verify_callback` with a given expected fingerprint.
The logic is inspired by our perl and proxmox-websocket-tunnel
verification logic:
Use openssl's verification if no fingerprint is pinned. If a fingerprint
is given, ignore openssl's verification and check if the leafs
certificate is a match.
This introduces a custom error type for this, since we need to handle
errors differently for different users, e.g. pbs-client wants to be able
to use a fingerprint cache and let the user accept it in interactive cli
sessions. For this we want the 'thiserror' crate, so move it to the
workspace Cargo.toml and depend from there. (also change this for
proxmox-openid)
One thing to note here is that the APPLICATION_VERIFICATION error of
openssl is used to mark the case where an untrusted root or intermediate
certificate is trusted from the callback. When that happens, openssl
might return true for the following certificates (if nothing else is
wrong aside from a missing trust anchor), so the error is checked for
this special value to determine if the openssl validation can be
trusted.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
changes from v1:
* drop hex dependency
Cargo.toml | 1 +
proxmox-http/Cargo.toml | 5 +++
proxmox-http/src/lib.rs | 5 +++
proxmox-http/src/tls.rs | 84 +++++++++++++++++++++++++++++++++++++++
proxmox-openid/Cargo.toml | 2 +-
5 files changed, 96 insertions(+), 1 deletion(-)
create mode 100644 proxmox-http/src/tls.rs
diff --git a/Cargo.toml b/Cargo.toml
index fd7eba63..64d459a4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -118,6 +118,7 @@ serde-xml-rs = "0.5"
syn = { version = "2", features = [ "full", "visit-mut" ] }
sync_wrapper = "1"
tar = "0.4"
+thiserror = "1"
tokio = "1.6"
tokio-openssl = "0.6.1"
tokio-stream = "0.1.0"
diff --git a/proxmox-http/Cargo.toml b/proxmox-http/Cargo.toml
index 4bad2fe7..5d54bce4 100644
--- a/proxmox-http/Cargo.toml
+++ b/proxmox-http/Cargo.toml
@@ -24,6 +24,7 @@ native-tls = { workspace = true, optional = true }
openssl = { version = "0.10", optional = true }
serde_json = { workspace = true, optional = true }
sync_wrapper = { workspace = true, optional = true }
+thiserror = { workspace = true, optional = true }
tokio = { workspace = true, features = [], optional = true }
tokio-openssl = { workspace = true, optional = true }
tower-service = { workspace = true, optional = true }
@@ -103,3 +104,7 @@ websocket = [
"tokio?/sync",
"body",
]
+tls = [
+ "dep:openssl",
+ "dep:thiserror",
+]
diff --git a/proxmox-http/src/lib.rs b/proxmox-http/src/lib.rs
index 8b6953b0..218a377c 100644
--- a/proxmox-http/src/lib.rs
+++ b/proxmox-http/src/lib.rs
@@ -40,3 +40,8 @@ pub use rate_limited_stream::RateLimitedStream;
mod body;
#[cfg(feature = "body")]
pub use body::Body;
+
+#[cfg(feature = "tls")]
+mod tls;
+#[cfg(feature = "tls")]
+pub use tls::*;
diff --git a/proxmox-http/src/tls.rs b/proxmox-http/src/tls.rs
new file mode 100644
index 00000000..7648edb3
--- /dev/null
+++ b/proxmox-http/src/tls.rs
@@ -0,0 +1,84 @@
+use openssl::x509::{X509StoreContextRef, X509VerifyResult};
+
+///
+/// Error type returned by failed [`openssl_verify_callback`].
+///
+#[derive(Debug, thiserror::Error)]
+pub enum SslVerifyError {
+ /// Occurs if no certificate is found in the current part of the chain. Should never happen!
+ #[error("SSL context lacks current certificate")]
+ NoCertificate,
+
+ /// Cannot calculate fingerprint from connection
+ #[error("failed to calculate fingerprint - {0}")]
+ InvalidFingerprint(openssl::error::ErrorStack),
+
+ /// Fingerprint match error
+ #[error("found fingerprint ({fingerprint}) does not match expected fingerprint ({expected})")]
+ FingerprintMismatch {
+ fingerprint: String,
+ expected: String,
+ },
+
+ /// Untrusted certificate with fingerprint information
+ #[error("certificate validation failed")]
+ UntrustedCertificate { fingerprint: String },
+}
+
+/// Intended as an openssl verification callback.
+///
+/// The following things are checked:
+///
+/// * If no fingerprint is given, return the openssl verification result
+/// * If a fingerprint is given, do:
+/// * Ignore all non-leaf certificates/
+pub fn openssl_verify_callback(
+ openssl_valid: bool,
+ ctx: &mut X509StoreContextRef,
+ expected_fp: Option<&str>,
+) -> Result<(), SslVerifyError> {
+ let trust_openssl = ctx.error() != X509VerifyResult::APPLICATION_VERIFICATION;
+ if expected_fp.is_none() && openssl_valid && trust_openssl {
+ return Ok(());
+ }
+
+ let cert = match ctx.current_cert() {
+ Some(cert) => cert,
+ None => {
+ return Err(SslVerifyError::NoCertificate);
+ }
+ };
+
+ if ctx.error_depth() > 0 {
+ // openssl was not valid, but we want to continue, so save that we don't trust openssl
+ ctx.set_error(X509VerifyResult::APPLICATION_VERIFICATION);
+ return Ok(());
+ }
+
+ let digest = cert
+ .digest(openssl::hash::MessageDigest::sha256())
+ .map_err(SslVerifyError::InvalidFingerprint)?;
+ let fingerprint = get_fingerprint_from_u8(&digest);
+
+ if let Some(expected_fp) = expected_fp {
+ if expected_fp.to_lowercase() == fingerprint.to_lowercase() {
+ ctx.set_error(X509VerifyResult::OK);
+ Ok(())
+ } else {
+ Err(SslVerifyError::FingerprintMismatch {
+ fingerprint,
+ expected: expected_fp.to_string(),
+ })
+ }
+ } else {
+ Err(SslVerifyError::UntrustedCertificate { fingerprint })
+ }
+}
+
+/// Returns the fingerprint from a byte slice ([`&[u8]`]) in the form `00:11:22:...`
+pub fn get_fingerprint_from_u8(fp: &[u8]) -> String {
+ fp.iter()
+ .map(|byte| format!("{byte:02x}"))
+ .collect::<Vec<String>>()
+ .join(":")
+}
diff --git a/proxmox-openid/Cargo.toml b/proxmox-openid/Cargo.toml
index edca9f55..5b031800 100644
--- a/proxmox-openid/Cargo.toml
+++ b/proxmox-openid/Cargo.toml
@@ -18,7 +18,7 @@ http.workspace = true
nix.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
-thiserror = "1"
+thiserror.workspace = true
native-tls.workspace = true
openidconnect = { version = "4", default-features = false, features = ["accept-rfc3339-timestamps"] }
--
2.39.5
_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel
^ permalink raw reply [flat|nested] 8+ messages in thread
* [pbs-devel] [PATCH proxmox 2/2] client: use proxmox-http's openssl verification callback
2025-07-24 8:56 [pbs-devel] [PATCH proxmox{, -backup, -websocket-tunnel} 0/4] unify openssl callback logic Dominik Csapak
2025-07-24 8:56 ` [pbs-devel] [PATCH proxmox 1/2] http: factor out openssl verification callback Dominik Csapak
@ 2025-07-24 8:56 ` Dominik Csapak
2025-07-24 8:56 ` [pbs-devel] [PATCH proxmox-backup 1/1] pbs-client: use proxmox-https openssl callback Dominik Csapak
2025-07-24 8:56 ` [pbs-devel] [PATCH proxmox-websocket-tunnel 1/1] use proxmox-http's " Dominik Csapak
3 siblings, 0 replies; 8+ messages in thread
From: Dominik Csapak @ 2025-07-24 8:56 UTC (permalink / raw)
To: pbs-devel, pve-devel
This changes the validation logic by always checking the fingerprint of
the leaf certificate, ignoring the openssl verification if a fingerprint
is configured. This now aligns with our perl implementation and the one
for proxmox-websocket-tunnel.
Before, a valid certificate chain would have precedence over an explicit
fingerprint.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
proxmox-client/Cargo.toml | 2 +-
proxmox-client/src/client.rs | 48 ++++++------------------------------
2 files changed, 9 insertions(+), 41 deletions(-)
diff --git a/proxmox-client/Cargo.toml b/proxmox-client/Cargo.toml
index ec4078a9..9831d686 100644
--- a/proxmox-client/Cargo.toml
+++ b/proxmox-client/Cargo.toml
@@ -25,7 +25,7 @@ openssl = { workspace = true, optional = true }
proxmox-login = { workspace = true, features = [ "http" ] }
-proxmox-http = { workspace = true, optional = true, features = [ "client" ] }
+proxmox-http = { workspace = true, optional = true, features = [ "client", "tls" ] }
hyper = { workspace = true, optional = true }
proxmox-serde = { workspace = true, features = [ "perl" ] }
diff --git a/proxmox-client/src/client.rs b/proxmox-client/src/client.rs
index da2c5c59..53ebb53b 100644
--- a/proxmox-client/src/client.rs
+++ b/proxmox-client/src/client.rs
@@ -9,9 +9,9 @@ use http::uri::PathAndQuery;
use http::Method;
use http::{StatusCode, Uri};
use http_body_util::BodyExt;
-use openssl::hash::MessageDigest;
use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
use openssl::x509::{self, X509};
+use proxmox_http::get_fingerprint_from_u8;
use proxmox_login::Ticket;
use serde::Serialize;
@@ -110,10 +110,14 @@ impl Client {
TlsOptions::Insecure => connector.set_verify(SslVerifyMode::NONE),
TlsOptions::Fingerprint(expected_fingerprint) => {
connector.set_verify_callback(SslVerifyMode::PEER, move |valid, chain| {
- if valid {
- return true;
+ let fp = get_fingerprint_from_u8(&expected_fingerprint);
+ match proxmox_http::openssl_verify_callback(valid, chain, Some(&fp)) {
+ Ok(()) => true,
+ Err(err) => {
+ log::error!("{err}");
+ false
+ }
}
- verify_fingerprint(chain, &expected_fingerprint)
});
}
TlsOptions::Callback(cb) => {
@@ -545,42 +549,6 @@ impl HttpApiClient for Client {
}
}
-fn verify_fingerprint(chain: &x509::X509StoreContextRef, expected_fingerprint: &[u8]) -> bool {
- let Some(cert) = chain.current_cert() else {
- log::error!("no certificate in chain?");
- return false;
- };
-
- let fp = match cert.digest(MessageDigest::sha256()) {
- Err(err) => {
- log::error!("error calculating certificate fingerprint: {err}");
- return false;
- }
- Ok(fp) => fp,
- };
-
- if expected_fingerprint != fp.as_ref() {
- log::error!("bad fingerprint: {}", fp_string(&fp));
- log::error!("expected fingerprint: {}", fp_string(expected_fingerprint));
- return false;
- }
-
- true
-}
-
-fn fp_string(fp: &[u8]) -> String {
- use std::fmt::Write as _;
-
- let mut out = String::new();
- for b in fp {
- if !out.is_empty() {
- out.push(':');
- }
- let _ = write!(out, "{b:02x}");
- }
- out
-}
-
impl Error {
pub(crate) fn internal<E>(context: &'static str, err: E) -> Self
where
--
2.39.5
_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel
^ permalink raw reply [flat|nested] 8+ messages in thread
* [pbs-devel] [PATCH proxmox-backup 1/1] pbs-client: use proxmox-https openssl callback
2025-07-24 8:56 [pbs-devel] [PATCH proxmox{, -backup, -websocket-tunnel} 0/4] unify openssl callback logic Dominik Csapak
2025-07-24 8:56 ` [pbs-devel] [PATCH proxmox 1/2] http: factor out openssl verification callback Dominik Csapak
2025-07-24 8:56 ` [pbs-devel] [PATCH proxmox 2/2] client: use proxmox-http's " Dominik Csapak
@ 2025-07-24 8:56 ` Dominik Csapak
2025-07-24 8:56 ` [pbs-devel] [PATCH proxmox-websocket-tunnel 1/1] use proxmox-http's " Dominik Csapak
3 siblings, 0 replies; 8+ messages in thread
From: Dominik Csapak @ 2025-07-24 8:56 UTC (permalink / raw)
To: pbs-devel, pve-devel
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 | 151 ++++++++++++++--------------------
2 files changed, 62 insertions(+), 91 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index ea4133283..c8e59640d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -63,7 +63,7 @@ proxmox-compression = "1"
proxmox-config-digest = "1"
proxmox-daemon = "1"
proxmox-fuse = "1"
-proxmox-http = { version = "1", features = [ "client", "http-helpers", "websocket" ] } # see below
+proxmox-http = { version = "1", features = [ "client", "http-helpers", "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 74b076244..69ef1d6bb 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::{connect::HttpConnector, Client};
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::{json, Value};
use xdg::BaseDirectories;
@@ -31,7 +28,7 @@ use proxmox_async::broadcast_future::BroadcastFuture;
use proxmox_http::client::HttpsConnector;
use proxmox_http::uri::{build_authority, json_object_to_query};
use proxmox_http::Body;
-use proxmox_http::{ProxyConfig, RateLimiter};
+use proxmox_http::{openssl_verify_callback, ProxyConfig, RateLimiter, SslVerifyError};
use proxmox_log::{error, info, warn};
use pbs_api_types::percent_encoding::DEFAULT_ENCODE_SET;
@@ -413,30 +410,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 && prefix.is_some() {
- if let Err(err) =
- store_fingerprint(prefix.as_ref().unwrap(), &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
}
},
@@ -642,79 +651,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.39.5
_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel
^ permalink raw reply [flat|nested] 8+ messages in thread
* [pbs-devel] [PATCH proxmox-websocket-tunnel 1/1] use proxmox-http's openssl callback
2025-07-24 8:56 [pbs-devel] [PATCH proxmox{, -backup, -websocket-tunnel} 0/4] unify openssl callback logic Dominik Csapak
` (2 preceding siblings ...)
2025-07-24 8:56 ` [pbs-devel] [PATCH proxmox-backup 1/1] pbs-client: use proxmox-https openssl callback Dominik Csapak
@ 2025-07-24 8:56 ` Dominik Csapak
3 siblings, 0 replies; 8+ messages in thread
From: Dominik Csapak @ 2025-07-24 8:56 UTC (permalink / raw)
To: pbs-devel, pve-devel
no functional change intended, since the callback there should implement
the same behavior.
With this, we can drop the dependency on itertools.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
Cargo.toml | 3 +--
src/main.rs | 67 +++++++++++++++++++++--------------------------------
2 files changed, 28 insertions(+), 42 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index 02ac3d1..99cb5d4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -17,7 +17,6 @@ hex = "0.4"
http = "1"
hyper = "1"
hyper-util = "0.1"
-itertools = "0.13"
openssl = "0.10"
percent-encoding = "2"
serde = { version = "1.0", features = ["derive"] }
@@ -26,5 +25,5 @@ tokio = { version = "1", features = ["io-std", "io-util", "macros", "rt-multi-th
tokio-stream = { version = "0.1", features = ["io-util"] }
tokio-util = "0.7"
-proxmox-http = { version = "1", features = ["websocket", "client"] }
+proxmox-http = { version = "1", features = ["websocket", "client", "tls"] }
proxmox-sys = "1"
diff --git a/src/main.rs b/src/main.rs
index 6d86575..4328d64 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -25,7 +25,7 @@ use tokio_stream::StreamExt;
use proxmox_http::client::HttpsConnector;
use proxmox_http::websocket::{OpCode, WebSocket, WebSocketReader, WebSocketWriter};
-use proxmox_http::Body;
+use proxmox_http::{Body, SslVerifyError};
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
@@ -142,48 +142,35 @@ impl CtrlTunnel {
}
let mut ssl_connector_builder = SslConnector::builder(SslMethod::tls())?;
- if let Some(expected) = fingerprint {
+ if fingerprint.is_some() {
ssl_connector_builder.set_verify_callback(
openssl::ssl::SslVerifyMode::PEER,
- move |_valid, ctx| {
- let cert = match ctx.current_cert() {
- Some(cert) => cert,
- None => {
- // should not happen
- eprintln!("SSL context lacks current certificate.");
- return false;
- }
- };
-
- // skip CA certificates, we only care about the peer cert
- let depth = ctx.error_depth();
- if depth != 0 {
- return true;
- }
-
- use itertools::Itertools;
- let fp = match cert.digest(openssl::hash::MessageDigest::sha256()) {
- Ok(fp) => fp,
- Err(err) => {
- // should not happen
- eprintln!("failed to calculate certificate FP - {}", err);
- return false;
+ move |valid, ctx| match proxmox_http::openssl_verify_callback(
+ valid,
+ ctx,
+ fingerprint.as_deref(),
+ ) {
+ Ok(()) => true,
+ Err(err) => {
+ match err {
+ SslVerifyError::NoCertificate => {
+ eprintln!("SSL context lacks current certificate");
+ }
+ SslVerifyError::InvalidFingerprint(err) => {
+ eprintln!("failed to calculate certificate FP - {err}")
+ }
+ SslVerifyError::FingerprintMismatch {
+ fingerprint,
+ expected,
+ } => {
+ eprintln!(
+ "certificate fingerprint does not match expected fingerprint!"
+ );
+ eprintln!("expected: {expected}");
+ eprintln!("encountered: {fingerprint}");
+ }
+ SslVerifyError::UntrustedCertificate { .. } => {}
}
- };
- let fp_string = hex::encode(fp);
- let fp_string = fp_string
- .as_bytes()
- .chunks(2)
- .map(|v| unsafe { std::str::from_utf8_unchecked(v) })
- .join(":");
-
- let expected = expected.to_lowercase();
- if expected == fp_string {
- true
- } else {
- eprintln!("certificate fingerprint does not match expected fingerprint!");
- eprintln!("expected: {}", expected);
- eprintln!("encountered: {}", fp_string);
false
}
},
--
2.39.5
_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [pbs-devel] [PATCH proxmox 1/2] http: factor out openssl verification callback
2025-07-23 19:26 ` Thomas Lamprecht
@ 2025-07-24 7:31 ` Dominik Csapak
0 siblings, 0 replies; 8+ messages in thread
From: Dominik Csapak @ 2025-07-24 7:31 UTC (permalink / raw)
To: Thomas Lamprecht, Proxmox Backup Server development discussion
On 7/23/25 21:26, Thomas Lamprecht wrote:
> Am 21.05.25 um 10:45 schrieb Dominik Csapak:
>> with the 'tls' feature offers a callback method that can be used within
>> openssl's `set_verify_callback` with a given expected fingerprint.
>>
>> The logic is inspired by our perl and proxmox-websocket-tunnel
>> verification logic:
>>
>> Use openssl's verification if no fingerprint is pinned. If a fingerprint
>> is given, ignore openssl's verification and check if the leafs
>> certificate is a match.
>>
>> This introduces a custom error type for this, since we need to handle
>> errors differently for different users, e.g. pbs-client wants to be able
>> to use a fingerprint cache and let the user accept it in interactive cli
>> sessions. For this we want the 'thiserror' crate, so move it to the
>> workspace Cargo.toml and depend from there. (also change this for
>> proxmox-openid)
>>
>> One thing to note here is that the APPLICATION_VERIFICATION error of
>> openssl is used to mark the case where an untrusted root or intermediate
>> certificate is trusted from the callback. When that happens, openssl
>> might return true for the following certificates (if nothing else is
>> wrong aside from a missing trust anchor), so the error is checked for
>> this special value to determine if the openssl validation can be
>> trusted.
>
> needs rebasing (sorry for the delay), and some comments inline
>
will send a v2
>>
>> Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
>> ---
>> Cargo.toml | 1 +
>> proxmox-http/Cargo.toml | 7 +++
>> proxmox-http/src/lib.rs | 5 +++
>> proxmox-http/src/tls.rs | 89 +++++++++++++++++++++++++++++++++++++++
>> proxmox-openid/Cargo.toml | 2 +-
>> 5 files changed, 103 insertions(+), 1 deletion(-)
>> create mode 100644 proxmox-http/src/tls.rs
>>
>> diff --git a/Cargo.toml b/Cargo.toml
>> index 95ade7d7..8aecc6ba 100644
>> --- a/Cargo.toml
>> +++ b/Cargo.toml
>> @@ -107,6 +107,7 @@ serde_json = "1.0"
>> serde_plain = "1.0"
>> syn = { version = "2", features = [ "full", "visit-mut" ] }
>> tar = "0.4"
>> +thiserror = "1"
>> tokio = "1.6"
>> tokio-openssl = "0.6.1"
>> tokio-stream = "0.1.0"
>> diff --git a/proxmox-http/Cargo.toml b/proxmox-http/Cargo.toml
>> index afa5981d..fb2eb26d 100644
>> --- a/proxmox-http/Cargo.toml
>> +++ b/proxmox-http/Cargo.toml
>> @@ -15,11 +15,13 @@ rust-version.workspace = true
>> anyhow.workspace = true
>> base64 = { workspace = true, optional = true }
>> futures = { workspace = true, optional = true }
>> +hex = { workspace = true, optional = true }
>
> is not be required, see below.
>
>> http = { workspace = true, optional = true }
>> hyper = { workspace = true, optional = true }
>> native-tls = { workspace = true, optional = true }
>> openssl = { version = "0.10", optional = true }
>> serde_json = { workspace = true, optional = true }
>> +thiserror = { workspace = true, optional = true }
>> tokio = { workspace = true, features = [], optional = true }
>> tokio-openssl = { workspace = true, optional = true }
>> tower-service.workspace = true
>> @@ -79,3 +81,8 @@ websocket = [
>> "tokio?/io-util",
>> "tokio?/sync",
>> ]
>> +tls = [
>> + "dep:hex",
>> + "dep:openssl",
>> + "dep:thiserror",
>> +]
>> diff --git a/proxmox-http/src/lib.rs b/proxmox-http/src/lib.rs
>> index 4770aaf4..a676acf9 100644
>> --- a/proxmox-http/src/lib.rs
>> +++ b/proxmox-http/src/lib.rs
>> @@ -35,3 +35,8 @@ pub use rate_limiter::{RateLimit, RateLimiter, RateLimiterVec, ShareableRateLimi
>> mod rate_limited_stream;
>> #[cfg(feature = "rate-limited-stream")]
>> pub use rate_limited_stream::RateLimitedStream;
>> +
>> +#[cfg(feature = "tls")]
>> +mod tls;
>> +#[cfg(feature = "tls")]
>> +pub use tls::*;
>> diff --git a/proxmox-http/src/tls.rs b/proxmox-http/src/tls.rs
>> new file mode 100644
>> index 00000000..6da03cd9
>> --- /dev/null
>> +++ b/proxmox-http/src/tls.rs
>> @@ -0,0 +1,89 @@
>> +use openssl::x509::{X509StoreContextRef, X509VerifyResult};
>> +
>> +///
>> +/// Error type returned by failed [`openssl_verify_callback`].
>> +///
>> +#[derive(Debug, thiserror::Error)]
>> +pub enum SslVerifyError {
>> + /// Occurs if no certificate is found in the current part of the chain. Should never happen!
>> + #[error("SSL context lacks current certificate")]
>> + NoCertificate,
>> +
>> + /// Cannot calculate fingerprint from connection
>> + #[error("failed to calculate fingerprint - {0}")]
>> + InvalidFingerprint(openssl::error::ErrorStack),
>> +
>> + /// Fingerprint match error
>> + #[error("found fingerprint ({fingerprint}) does not match expected fingerprint ({expected})")]
>> + FingerprintMismatch {
>> + fingerprint: String,
>> + expected: String,
>> + },
>> +
>> + /// Untrusted certificate with fingerprint information
>> + #[error("certificate validation failed")]
>> + UntrustedCertificate { fingerprint: String },
>> +}
>> +
>> +/// Intended as an openssl verification callback.
>> +///
>> +/// The following things are checked:
>> +///
>> +/// * If no fingerprint is given, return the openssl verification result
>> +/// * If a fingerprint is given, do:
>> +/// * Ignore all non-leaf certificates/
>> +pub fn openssl_verify_callback(
>> + openssl_valid: bool,
>> + ctx: &mut X509StoreContextRef,
>
> hmm, not sure how simple it is to construct/derive above type manually,
> but would be great to get this method some actual tests, potentially
> with some temporary certs created on demand (or the snake oil one).
> But that doesn't need to block this series, it's already an improvement
> to have just one of these around in any case.
i tried really hard to create in-rust tests by using the openssl
interfaces, but i wasn't able to massage them to actually test the
callback.
imho the only way i see how we could test is to write a short
server and start that and connect with socat/openssl/... directly
during the tests... but i'm not sure if that is really practical to do
in rust tests...
>
>> + expected_fp: Option<&str>,
>> +) -> Result<(), SslVerifyError> {
>> + let trust_openssl = ctx.error() != X509VerifyResult::APPLICATION_VERIFICATION;
>> + if expected_fp.is_none() && openssl_valid && trust_openssl {
>> + return Ok(());
>> + }
>> +
>> + let cert = match ctx.current_cert() {
>> + Some(cert) => cert,
>> + None => {
>> + return Err(SslVerifyError::NoCertificate);
>> + }
>> + };
>> +
>> + if ctx.error_depth() > 0 {
>> + // openssl was not valid, but we want to continue, so save that we don't trust openssl
>> + ctx.set_error(X509VerifyResult::APPLICATION_VERIFICATION);
>> + return Ok(());
>> + }
>> +
>> + let digest = cert
>> + .digest(openssl::hash::MessageDigest::sha256())
>> + .map_err(SslVerifyError::InvalidFingerprint)?;
>> + let fingerprint = get_fingerprint_from_u8(&digest);
>> +
>> + if let Some(expected_fp) = expected_fp {
>> + if expected_fp.to_lowercase() == fingerprint.to_lowercase() {
>> + ctx.set_error(X509VerifyResult::OK);
>> + Ok(())
>> + } else {
>> + Err(SslVerifyError::FingerprintMismatch {
>> + fingerprint,
>> + expected: expected_fp.to_string(),
>> + })
>> + }
>> + } else {
>> + Err(SslVerifyError::UntrustedCertificate { fingerprint })
>> + }
>> +}
>> +
>> +/// Returns the fingerprint from a byte slice ([`&[u8]`]) in the form `00:11:22:...`
>> +pub fn get_fingerprint_from_u8(fp: &[u8]) -> String {
>> + let fp_string = hex::encode(fp);
>> + let fp_string = fp_string
>> + .as_bytes()
>> + .chunks(2)
>> + .map(|v| unsafe { std::str::from_utf8_unchecked(v) })
>> + .collect::<Vec<_>>()
>> + .join(":");
>> +
>> + fp_string
>
> As mentioned recently in the review for the s3-client [0], this can be done
> simpler:
>
> fp
> .iter()
> .map(|byte| format!("{byte:02x}"))
> .collect::<Vec<String>>()
> .join(":")
>
>
> [0]: https://lore.proxmox.com/all/38e3b3b5-a1a6-43d6-b925-1d04d8b1e22d@proxmox.com/
true
>
>> +}
>> diff --git a/proxmox-openid/Cargo.toml b/proxmox-openid/Cargo.toml
>> index a5249214..4223d10c 100644
>> --- a/proxmox-openid/Cargo.toml
>> +++ b/proxmox-openid/Cargo.toml
>> @@ -18,7 +18,7 @@ http.workspace = true
>> nix.workspace = true
>> serde = { workspace = true, features = ["derive"] }
>> serde_json.workspace = true
>> -thiserror = "1"
>> +thiserror.workspace = true
>
> a bit unrelated to the rest, but can be OK to do in a patch like this I guess.
shall i send a seperate patch upfront that moves the thiserror crate
dependency to the workspace cargo.toml ?
>
>> native-tls.workspace = true
>>
>> openidconnect = { version = "2.4", default-features = false, features = ["accept-rfc3339-timestamps"] }
>
_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [pbs-devel] [PATCH proxmox 1/2] http: factor out openssl verification callback
2025-05-21 8:45 ` [pbs-devel] [PATCH proxmox 1/2] http: factor out openssl verification callback Dominik Csapak
@ 2025-07-23 19:26 ` Thomas Lamprecht
2025-07-24 7:31 ` Dominik Csapak
0 siblings, 1 reply; 8+ messages in thread
From: Thomas Lamprecht @ 2025-07-23 19:26 UTC (permalink / raw)
To: Proxmox Backup Server development discussion, Dominik Csapak
Am 21.05.25 um 10:45 schrieb Dominik Csapak:
> with the 'tls' feature offers a callback method that can be used within
> openssl's `set_verify_callback` with a given expected fingerprint.
>
> The logic is inspired by our perl and proxmox-websocket-tunnel
> verification logic:
>
> Use openssl's verification if no fingerprint is pinned. If a fingerprint
> is given, ignore openssl's verification and check if the leafs
> certificate is a match.
>
> This introduces a custom error type for this, since we need to handle
> errors differently for different users, e.g. pbs-client wants to be able
> to use a fingerprint cache and let the user accept it in interactive cli
> sessions. For this we want the 'thiserror' crate, so move it to the
> workspace Cargo.toml and depend from there. (also change this for
> proxmox-openid)
>
> One thing to note here is that the APPLICATION_VERIFICATION error of
> openssl is used to mark the case where an untrusted root or intermediate
> certificate is trusted from the callback. When that happens, openssl
> might return true for the following certificates (if nothing else is
> wrong aside from a missing trust anchor), so the error is checked for
> this special value to determine if the openssl validation can be
> trusted.
needs rebasing (sorry for the delay), and some comments inline
>
> Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
> ---
> Cargo.toml | 1 +
> proxmox-http/Cargo.toml | 7 +++
> proxmox-http/src/lib.rs | 5 +++
> proxmox-http/src/tls.rs | 89 +++++++++++++++++++++++++++++++++++++++
> proxmox-openid/Cargo.toml | 2 +-
> 5 files changed, 103 insertions(+), 1 deletion(-)
> create mode 100644 proxmox-http/src/tls.rs
>
> diff --git a/Cargo.toml b/Cargo.toml
> index 95ade7d7..8aecc6ba 100644
> --- a/Cargo.toml
> +++ b/Cargo.toml
> @@ -107,6 +107,7 @@ serde_json = "1.0"
> serde_plain = "1.0"
> syn = { version = "2", features = [ "full", "visit-mut" ] }
> tar = "0.4"
> +thiserror = "1"
> tokio = "1.6"
> tokio-openssl = "0.6.1"
> tokio-stream = "0.1.0"
> diff --git a/proxmox-http/Cargo.toml b/proxmox-http/Cargo.toml
> index afa5981d..fb2eb26d 100644
> --- a/proxmox-http/Cargo.toml
> +++ b/proxmox-http/Cargo.toml
> @@ -15,11 +15,13 @@ rust-version.workspace = true
> anyhow.workspace = true
> base64 = { workspace = true, optional = true }
> futures = { workspace = true, optional = true }
> +hex = { workspace = true, optional = true }
is not be required, see below.
> http = { workspace = true, optional = true }
> hyper = { workspace = true, optional = true }
> native-tls = { workspace = true, optional = true }
> openssl = { version = "0.10", optional = true }
> serde_json = { workspace = true, optional = true }
> +thiserror = { workspace = true, optional = true }
> tokio = { workspace = true, features = [], optional = true }
> tokio-openssl = { workspace = true, optional = true }
> tower-service.workspace = true
> @@ -79,3 +81,8 @@ websocket = [
> "tokio?/io-util",
> "tokio?/sync",
> ]
> +tls = [
> + "dep:hex",
> + "dep:openssl",
> + "dep:thiserror",
> +]
> diff --git a/proxmox-http/src/lib.rs b/proxmox-http/src/lib.rs
> index 4770aaf4..a676acf9 100644
> --- a/proxmox-http/src/lib.rs
> +++ b/proxmox-http/src/lib.rs
> @@ -35,3 +35,8 @@ pub use rate_limiter::{RateLimit, RateLimiter, RateLimiterVec, ShareableRateLimi
> mod rate_limited_stream;
> #[cfg(feature = "rate-limited-stream")]
> pub use rate_limited_stream::RateLimitedStream;
> +
> +#[cfg(feature = "tls")]
> +mod tls;
> +#[cfg(feature = "tls")]
> +pub use tls::*;
> diff --git a/proxmox-http/src/tls.rs b/proxmox-http/src/tls.rs
> new file mode 100644
> index 00000000..6da03cd9
> --- /dev/null
> +++ b/proxmox-http/src/tls.rs
> @@ -0,0 +1,89 @@
> +use openssl::x509::{X509StoreContextRef, X509VerifyResult};
> +
> +///
> +/// Error type returned by failed [`openssl_verify_callback`].
> +///
> +#[derive(Debug, thiserror::Error)]
> +pub enum SslVerifyError {
> + /// Occurs if no certificate is found in the current part of the chain. Should never happen!
> + #[error("SSL context lacks current certificate")]
> + NoCertificate,
> +
> + /// Cannot calculate fingerprint from connection
> + #[error("failed to calculate fingerprint - {0}")]
> + InvalidFingerprint(openssl::error::ErrorStack),
> +
> + /// Fingerprint match error
> + #[error("found fingerprint ({fingerprint}) does not match expected fingerprint ({expected})")]
> + FingerprintMismatch {
> + fingerprint: String,
> + expected: String,
> + },
> +
> + /// Untrusted certificate with fingerprint information
> + #[error("certificate validation failed")]
> + UntrustedCertificate { fingerprint: String },
> +}
> +
> +/// Intended as an openssl verification callback.
> +///
> +/// The following things are checked:
> +///
> +/// * If no fingerprint is given, return the openssl verification result
> +/// * If a fingerprint is given, do:
> +/// * Ignore all non-leaf certificates/
> +pub fn openssl_verify_callback(
> + openssl_valid: bool,
> + ctx: &mut X509StoreContextRef,
hmm, not sure how simple it is to construct/derive above type manually,
but would be great to get this method some actual tests, potentially
with some temporary certs created on demand (or the snake oil one).
But that doesn't need to block this series, it's already an improvement
to have just one of these around in any case.
> + expected_fp: Option<&str>,
> +) -> Result<(), SslVerifyError> {
> + let trust_openssl = ctx.error() != X509VerifyResult::APPLICATION_VERIFICATION;
> + if expected_fp.is_none() && openssl_valid && trust_openssl {
> + return Ok(());
> + }
> +
> + let cert = match ctx.current_cert() {
> + Some(cert) => cert,
> + None => {
> + return Err(SslVerifyError::NoCertificate);
> + }
> + };
> +
> + if ctx.error_depth() > 0 {
> + // openssl was not valid, but we want to continue, so save that we don't trust openssl
> + ctx.set_error(X509VerifyResult::APPLICATION_VERIFICATION);
> + return Ok(());
> + }
> +
> + let digest = cert
> + .digest(openssl::hash::MessageDigest::sha256())
> + .map_err(SslVerifyError::InvalidFingerprint)?;
> + let fingerprint = get_fingerprint_from_u8(&digest);
> +
> + if let Some(expected_fp) = expected_fp {
> + if expected_fp.to_lowercase() == fingerprint.to_lowercase() {
> + ctx.set_error(X509VerifyResult::OK);
> + Ok(())
> + } else {
> + Err(SslVerifyError::FingerprintMismatch {
> + fingerprint,
> + expected: expected_fp.to_string(),
> + })
> + }
> + } else {
> + Err(SslVerifyError::UntrustedCertificate { fingerprint })
> + }
> +}
> +
> +/// Returns the fingerprint from a byte slice ([`&[u8]`]) in the form `00:11:22:...`
> +pub fn get_fingerprint_from_u8(fp: &[u8]) -> String {
> + let fp_string = hex::encode(fp);
> + let fp_string = fp_string
> + .as_bytes()
> + .chunks(2)
> + .map(|v| unsafe { std::str::from_utf8_unchecked(v) })
> + .collect::<Vec<_>>()
> + .join(":");
> +
> + fp_string
As mentioned recently in the review for the s3-client [0], this can be done
simpler:
fp
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<Vec<String>>()
.join(":")
[0]: https://lore.proxmox.com/all/38e3b3b5-a1a6-43d6-b925-1d04d8b1e22d@proxmox.com/
> +}
> diff --git a/proxmox-openid/Cargo.toml b/proxmox-openid/Cargo.toml
> index a5249214..4223d10c 100644
> --- a/proxmox-openid/Cargo.toml
> +++ b/proxmox-openid/Cargo.toml
> @@ -18,7 +18,7 @@ http.workspace = true
> nix.workspace = true
> serde = { workspace = true, features = ["derive"] }
> serde_json.workspace = true
> -thiserror = "1"
> +thiserror.workspace = true
a bit unrelated to the rest, but can be OK to do in a patch like this I guess.
> native-tls.workspace = true
>
> openidconnect = { version = "2.4", default-features = false, features = ["accept-rfc3339-timestamps"] }
_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel
^ permalink raw reply [flat|nested] 8+ messages in thread
* [pbs-devel] [PATCH proxmox 1/2] http: factor out openssl verification callback
2025-05-21 8:45 [pbs-devel] [PATCH proxmox{, -websocket-tunnel, -backup} 0/5] unify openssl callback logic Dominik Csapak
@ 2025-05-21 8:45 ` Dominik Csapak
2025-07-23 19:26 ` Thomas Lamprecht
0 siblings, 1 reply; 8+ messages in thread
From: Dominik Csapak @ 2025-05-21 8:45 UTC (permalink / raw)
To: pbs-devel
with the 'tls' feature offers a callback method that can be used within
openssl's `set_verify_callback` with a given expected fingerprint.
The logic is inspired by our perl and proxmox-websocket-tunnel
verification logic:
Use openssl's verification if no fingerprint is pinned. If a fingerprint
is given, ignore openssl's verification and check if the leafs
certificate is a match.
This introduces a custom error type for this, since we need to handle
errors differently for different users, e.g. pbs-client wants to be able
to use a fingerprint cache and let the user accept it in interactive cli
sessions. For this we want the 'thiserror' crate, so move it to the
workspace Cargo.toml and depend from there. (also change this for
proxmox-openid)
One thing to note here is that the APPLICATION_VERIFICATION error of
openssl is used to mark the case where an untrusted root or intermediate
certificate is trusted from the callback. When that happens, openssl
might return true for the following certificates (if nothing else is
wrong aside from a missing trust anchor), so the error is checked for
this special value to determine if the openssl validation can be
trusted.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
Cargo.toml | 1 +
proxmox-http/Cargo.toml | 7 +++
proxmox-http/src/lib.rs | 5 +++
proxmox-http/src/tls.rs | 89 +++++++++++++++++++++++++++++++++++++++
proxmox-openid/Cargo.toml | 2 +-
5 files changed, 103 insertions(+), 1 deletion(-)
create mode 100644 proxmox-http/src/tls.rs
diff --git a/Cargo.toml b/Cargo.toml
index 95ade7d7..8aecc6ba 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -107,6 +107,7 @@ serde_json = "1.0"
serde_plain = "1.0"
syn = { version = "2", features = [ "full", "visit-mut" ] }
tar = "0.4"
+thiserror = "1"
tokio = "1.6"
tokio-openssl = "0.6.1"
tokio-stream = "0.1.0"
diff --git a/proxmox-http/Cargo.toml b/proxmox-http/Cargo.toml
index afa5981d..fb2eb26d 100644
--- a/proxmox-http/Cargo.toml
+++ b/proxmox-http/Cargo.toml
@@ -15,11 +15,13 @@ rust-version.workspace = true
anyhow.workspace = true
base64 = { workspace = true, optional = true }
futures = { workspace = true, optional = true }
+hex = { workspace = true, optional = true }
http = { workspace = true, optional = true }
hyper = { workspace = true, optional = true }
native-tls = { workspace = true, optional = true }
openssl = { version = "0.10", optional = true }
serde_json = { workspace = true, optional = true }
+thiserror = { workspace = true, optional = true }
tokio = { workspace = true, features = [], optional = true }
tokio-openssl = { workspace = true, optional = true }
tower-service.workspace = true
@@ -79,3 +81,8 @@ websocket = [
"tokio?/io-util",
"tokio?/sync",
]
+tls = [
+ "dep:hex",
+ "dep:openssl",
+ "dep:thiserror",
+]
diff --git a/proxmox-http/src/lib.rs b/proxmox-http/src/lib.rs
index 4770aaf4..a676acf9 100644
--- a/proxmox-http/src/lib.rs
+++ b/proxmox-http/src/lib.rs
@@ -35,3 +35,8 @@ pub use rate_limiter::{RateLimit, RateLimiter, RateLimiterVec, ShareableRateLimi
mod rate_limited_stream;
#[cfg(feature = "rate-limited-stream")]
pub use rate_limited_stream::RateLimitedStream;
+
+#[cfg(feature = "tls")]
+mod tls;
+#[cfg(feature = "tls")]
+pub use tls::*;
diff --git a/proxmox-http/src/tls.rs b/proxmox-http/src/tls.rs
new file mode 100644
index 00000000..6da03cd9
--- /dev/null
+++ b/proxmox-http/src/tls.rs
@@ -0,0 +1,89 @@
+use openssl::x509::{X509StoreContextRef, X509VerifyResult};
+
+///
+/// Error type returned by failed [`openssl_verify_callback`].
+///
+#[derive(Debug, thiserror::Error)]
+pub enum SslVerifyError {
+ /// Occurs if no certificate is found in the current part of the chain. Should never happen!
+ #[error("SSL context lacks current certificate")]
+ NoCertificate,
+
+ /// Cannot calculate fingerprint from connection
+ #[error("failed to calculate fingerprint - {0}")]
+ InvalidFingerprint(openssl::error::ErrorStack),
+
+ /// Fingerprint match error
+ #[error("found fingerprint ({fingerprint}) does not match expected fingerprint ({expected})")]
+ FingerprintMismatch {
+ fingerprint: String,
+ expected: String,
+ },
+
+ /// Untrusted certificate with fingerprint information
+ #[error("certificate validation failed")]
+ UntrustedCertificate { fingerprint: String },
+}
+
+/// Intended as an openssl verification callback.
+///
+/// The following things are checked:
+///
+/// * If no fingerprint is given, return the openssl verification result
+/// * If a fingerprint is given, do:
+/// * Ignore all non-leaf certificates/
+pub fn openssl_verify_callback(
+ openssl_valid: bool,
+ ctx: &mut X509StoreContextRef,
+ expected_fp: Option<&str>,
+) -> Result<(), SslVerifyError> {
+ let trust_openssl = ctx.error() != X509VerifyResult::APPLICATION_VERIFICATION;
+ if expected_fp.is_none() && openssl_valid && trust_openssl {
+ return Ok(());
+ }
+
+ let cert = match ctx.current_cert() {
+ Some(cert) => cert,
+ None => {
+ return Err(SslVerifyError::NoCertificate);
+ }
+ };
+
+ if ctx.error_depth() > 0 {
+ // openssl was not valid, but we want to continue, so save that we don't trust openssl
+ ctx.set_error(X509VerifyResult::APPLICATION_VERIFICATION);
+ return Ok(());
+ }
+
+ let digest = cert
+ .digest(openssl::hash::MessageDigest::sha256())
+ .map_err(SslVerifyError::InvalidFingerprint)?;
+ let fingerprint = get_fingerprint_from_u8(&digest);
+
+ if let Some(expected_fp) = expected_fp {
+ if expected_fp.to_lowercase() == fingerprint.to_lowercase() {
+ ctx.set_error(X509VerifyResult::OK);
+ Ok(())
+ } else {
+ Err(SslVerifyError::FingerprintMismatch {
+ fingerprint,
+ expected: expected_fp.to_string(),
+ })
+ }
+ } else {
+ Err(SslVerifyError::UntrustedCertificate { fingerprint })
+ }
+}
+
+/// Returns the fingerprint from a byte slice ([`&[u8]`]) in the form `00:11:22:...`
+pub fn get_fingerprint_from_u8(fp: &[u8]) -> String {
+ let fp_string = hex::encode(fp);
+ let fp_string = fp_string
+ .as_bytes()
+ .chunks(2)
+ .map(|v| unsafe { std::str::from_utf8_unchecked(v) })
+ .collect::<Vec<_>>()
+ .join(":");
+
+ fp_string
+}
diff --git a/proxmox-openid/Cargo.toml b/proxmox-openid/Cargo.toml
index a5249214..4223d10c 100644
--- a/proxmox-openid/Cargo.toml
+++ b/proxmox-openid/Cargo.toml
@@ -18,7 +18,7 @@ http.workspace = true
nix.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
-thiserror = "1"
+thiserror.workspace = true
native-tls.workspace = true
openidconnect = { version = "2.4", default-features = false, features = ["accept-rfc3339-timestamps"] }
--
2.39.5
_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel
^ permalink raw reply [flat|nested] 8+ messages in thread
end of thread, other threads:[~2025-07-24 8:55 UTC | newest]
Thread overview: 8+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-07-24 8:56 [pbs-devel] [PATCH proxmox{, -backup, -websocket-tunnel} 0/4] unify openssl callback logic Dominik Csapak
2025-07-24 8:56 ` [pbs-devel] [PATCH proxmox 1/2] http: factor out openssl verification callback Dominik Csapak
2025-07-24 8:56 ` [pbs-devel] [PATCH proxmox 2/2] client: use proxmox-http's " Dominik Csapak
2025-07-24 8:56 ` [pbs-devel] [PATCH proxmox-backup 1/1] pbs-client: use proxmox-https openssl callback Dominik Csapak
2025-07-24 8:56 ` [pbs-devel] [PATCH proxmox-websocket-tunnel 1/1] use proxmox-http's " Dominik Csapak
-- strict thread matches above, loose matches on Subject: below --
2025-05-21 8:45 [pbs-devel] [PATCH proxmox{, -websocket-tunnel, -backup} 0/5] unify openssl callback logic Dominik Csapak
2025-05-21 8:45 ` [pbs-devel] [PATCH proxmox 1/2] http: factor out openssl verification callback Dominik Csapak
2025-07-23 19:26 ` Thomas Lamprecht
2025-07-24 7:31 ` Dominik Csapak
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox