* [PATCH proxmox{,-backup,-websocket-tunnel} v4 0/8] unify openssl callback logic
@ 2026-07-01 10:30 Dominik Csapak
2026-07-01 10:30 ` [PATCH proxmox v4 1/8] http: factor out openssl verification callback Dominik Csapak
` (8 more replies)
0 siblings, 9 replies; 13+ messages in thread
From: Dominik Csapak @ 2026-07-01 10:30 UTC (permalink / raw)
To: pve-devel, pbs-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
* PDM client has also a different implementation (not touched here)
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.
There is a patch int the proxmox-http crate is to preserve backwards
compatibility with the current pbs client behavior, but is opt-in via
environment variable (which we might want to enable automatically for the
pbs-client? though this is difficult to do, since the client can and will
be called from scripts or manually)
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)
Tests were implemented by Shannon (thanks!) but I refined it's behavior
(see the commit)
This series partially overlaps/interferes with shannons recent series:
https://lore.proxmox.com/pdm-devel/20260611120327.257523-1-s.sterz@proxmox.com/
changes from v3:
* include a Fingerprint struct to reuse (also for other sites, eg. PDM)
* use much simpler code in callback by always getting the leaf certificate
* make backwards compat opt-in instead of opt-out
* include shannons tests
* adapt usage sites to new interface
changes from v2:
* rebase on master
* add backwards compatibility switch via ENV variable
* add patch for pbs to check already verified fingerprints
changes from v1:
* rebase on master (drops one patch)
* drop hex dependency
proxmox:
Dominik Csapak (4):
http: factor out openssl verification callback
http: tls: use legacy behavior when PROXMOX_OLD_TLS_CHECK is set to
"1"
http: tls: add warning if old check behavior is enabled and triggered
client: use proxmox-http's openssl verification callback
Shannon Sterz (1):
http: tls: add integration tests for openssl verify callbacks
Cargo.toml | 1 +
proxmox-client/Cargo.toml | 2 +-
proxmox-client/src/client.rs | 70 ++--
proxmox-http/Cargo.toml | 15 +
proxmox-http/src/lib.rs | 5 +
proxmox-http/src/tls.rs | 221 ++++++++++
proxmox-http/tests/certs/cert-chain.pem | 46 ++
.../tests/certs/intermediate-cert.pem | 23 +
proxmox-http/tests/certs/intermediate-csr.pem | 17 +
proxmox-http/tests/certs/intermediate-key.pem | 28 ++
proxmox-http/tests/certs/leaf-cert.pem | 24 ++
proxmox-http/tests/certs/leaf-csr.pem | 17 +
proxmox-http/tests/certs/leaf-key.pem | 28 ++
proxmox-http/tests/certs/root-cert.pem | 23 +
proxmox-http/tests/certs/root-key.pem | 28 ++
proxmox-http/tests/certs/self-signed-cert.pem | 23 +
proxmox-http/tests/certs/self-signed-key.pem | 28 ++
proxmox-http/tests/common/mod.rs | 395 ++++++++++++++++++
proxmox-http/tests/openssl_verify_cb_new.rs | 89 ++++
proxmox-http/tests/openssl_verify_cb_old.rs | 77 ++++
20 files changed, 1113 insertions(+), 47 deletions(-)
create mode 100644 proxmox-http/src/tls.rs
create mode 100644 proxmox-http/tests/certs/cert-chain.pem
create mode 100644 proxmox-http/tests/certs/intermediate-cert.pem
create mode 100644 proxmox-http/tests/certs/intermediate-csr.pem
create mode 100644 proxmox-http/tests/certs/intermediate-key.pem
create mode 100644 proxmox-http/tests/certs/leaf-cert.pem
create mode 100644 proxmox-http/tests/certs/leaf-csr.pem
create mode 100644 proxmox-http/tests/certs/leaf-key.pem
create mode 100644 proxmox-http/tests/certs/root-cert.pem
create mode 100644 proxmox-http/tests/certs/root-key.pem
create mode 100644 proxmox-http/tests/certs/self-signed-cert.pem
create mode 100644 proxmox-http/tests/certs/self-signed-key.pem
create mode 100644 proxmox-http/tests/common/mod.rs
create mode 100644 proxmox-http/tests/openssl_verify_cb_new.rs
create mode 100644 proxmox-http/tests/openssl_verify_cb_old.rs
proxmox-backup:
Dominik Csapak (2):
pbs-client: use proxmox-https openssl callback
pbs-client: honor already verified fingerprint
Cargo.toml | 2 +-
pbs-client/src/http_client.rs | 183 +++++++++++++++++-----------------
2 files changed, 94 insertions(+), 91 deletions(-)
proxmox-websocket-tunnel:
Dominik Csapak (1):
use proxmox-http's openssl callback
Cargo.toml | 4 +--
src/main.rs | 76 +++++++++++++++++++++++++----------------------------
2 files changed, 37 insertions(+), 43 deletions(-)
Summary over all repositories:
24 files changed, 1244 insertions(+), 181 deletions(-)
--
Generated by murpp 0.11.0
^ permalink raw reply [flat|nested] 13+ messages in thread
* [PATCH proxmox v4 1/8] http: factor out openssl verification callback
2026-07-01 10:30 [PATCH proxmox{,-backup,-websocket-tunnel} v4 0/8] unify openssl callback logic Dominik Csapak
@ 2026-07-01 10:30 ` Dominik Csapak
2026-07-01 10:30 ` [PATCH proxmox v4 2/8] http: tls: use legacy behavior when PROXMOX_OLD_TLS_CHECK is set to "1" Dominik Csapak
` (7 subsequent siblings)
8 siblings, 0 replies; 13+ messages in thread
From: Dominik Csapak @ 2026-07-01 10:30 UTC (permalink / raw)
To: pve-devel, 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.
Introduce a Fingerprint struct and FingerprintError to be able to
represent fingerprints easily (without strings) and match on the
different error behaviors. This is mostly copied from PDMs CLI client
and can be removed there and reused from here.
This introduces a custom SslVerifyError 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.
Co-developed-by: Shannon Sterz <s.sterz@proxmox.com>
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
proxmox-http/Cargo.toml | 11 +++
proxmox-http/src/lib.rs | 5 +
proxmox-http/src/tls.rs | 202 ++++++++++++++++++++++++++++++++++++++++
3 files changed, 218 insertions(+)
create mode 100644 proxmox-http/src/tls.rs
diff --git a/proxmox-http/Cargo.toml b/proxmox-http/Cargo.toml
index 66b11650..cdc2861b 100644
--- a/proxmox-http/Cargo.toml
+++ b/proxmox-http/Cargo.toml
@@ -15,6 +15,7 @@ rust-version.workspace = true
anyhow.workspace = true
bytes = { workspace = true, optional = true }
futures = { workspace = true, optional = true }
+hex = { workspace = true, optional = true }
http = { workspace = true, optional = true }
http-body = { workspace = true, optional = true }
http-body-util = { workspace = true, optional = true }
@@ -22,8 +23,11 @@ hyper = { workspace = true, optional = true }
hyper-util = { workspace = true, optional = true, features = ["http2"] }
native-tls = { workspace = true, optional = true }
openssl = { version = "0.10", optional = true }
+serde = { workspace = true, optional = true }
serde_json = { workspace = true, optional = true }
+serde_plain = { 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 }
@@ -105,3 +109,10 @@ websocket = [
"tokio?/sync",
"body",
]
+tls = [
+ "dep:openssl",
+ "dep:hex",
+ "dep:thiserror",
+ "dep:serde",
+ "dep:serde_plain",
+]
diff --git a/proxmox-http/src/lib.rs b/proxmox-http/src/lib.rs
index ae3301e8..d27df7bc 100644
--- a/proxmox-http/src/lib.rs
+++ b/proxmox-http/src/lib.rs
@@ -39,3 +39,8 @@ pub use rate_limited_stream::{
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..5c2b1743
--- /dev/null
+++ b/proxmox-http/src/tls.rs
@@ -0,0 +1,202 @@
+use anyhow::Error;
+use openssl::x509::{X509Ref, X509StoreContextRef, X509VerifyResult};
+
+///
+/// Error type returned when trying to get a fingerprint from openssl.
+///
+#[derive(Debug, thiserror::Error)]
+pub enum FingerprintError {
+ /// Cannot calculate fingerprint from connection
+ #[error("failed to calculate fingerprint - {0}")]
+ CannotCalculate(openssl::error::ErrorStack),
+
+ /// Fingerprint has an invalid length
+ #[error("Unexpected fingerprint length")]
+ InvalidLength,
+}
+
+/// A sha256 fingerprint.
+// NOTE: The difference to ConfigDigest is that this also allows colons between bytes when parsing.
+// Also the API type's description is different.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct Fingerprint([u8; 32]);
+serde_plain::derive_deserialize_from_fromstr!(Fingerprint, "valid sha256 fingerprint");
+serde_plain::derive_serialize_from_display!(Fingerprint);
+
+impl From<[u8; 32]> for Fingerprint {
+ #[inline]
+ fn from(fp: [u8; 32]) -> Self {
+ Self(fp)
+ }
+}
+
+impl From<Fingerprint> for [u8; 32] {
+ #[inline]
+ fn from(fp: Fingerprint) -> Self {
+ fp.0
+ }
+}
+
+impl TryFrom<&[u8]> for Fingerprint {
+ type Error = std::array::TryFromSliceError;
+
+ fn try_from(slice: &[u8]) -> Result<Self, Self::Error> {
+ Ok(Self(slice.try_into()?))
+ }
+}
+
+impl TryFrom<&X509Ref> for Fingerprint {
+ type Error = FingerprintError;
+
+ fn try_from(value: &X509Ref) -> Result<Self, Self::Error> {
+ let digest = value
+ .digest(openssl::hash::MessageDigest::sha256())
+ .map_err(FingerprintError::CannotCalculate)?;
+ Fingerprint::try_from(&*digest).map_err(|_| FingerprintError::InvalidLength)
+ }
+}
+
+impl AsRef<[u8]> for Fingerprint {
+ fn as_ref(&self) -> &[u8] {
+ &self.0
+ }
+}
+
+impl AsRef<[u8; 32]> for Fingerprint {
+ fn as_ref(&self) -> &[u8; 32] {
+ &self.0
+ }
+}
+
+impl std::ops::Deref for Fingerprint {
+ type Target = [u8; 32];
+
+ fn deref(&self) -> &[u8; 32] {
+ &self.0
+ }
+}
+
+impl std::ops::DerefMut for Fingerprint {
+ fn deref_mut(&mut self) -> &mut [u8; 32] {
+ &mut self.0
+ }
+}
+
+impl std::fmt::Display for Fingerprint {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{:02x}", self[0])?;
+ for b in &self[1..] {
+ write!(f, ":{b:02x}")?;
+ }
+ Ok(())
+ }
+}
+
+impl std::str::FromStr for Fingerprint {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result<Self, Error> {
+ let s = s.replace(':', "");
+ let mut fp = [0u8; 32];
+ hex::decode_to_slice(s, &mut fp)?;
+ Ok(Fingerprint(fp))
+ }
+}
+
+///
+/// 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 get the fingerprint from openssl.
+ #[error("error getting the fingerprint from openssl - {0}")]
+ InvalidFingerprint(FingerprintError),
+
+ /// Fingerprint match error
+ #[error("found fingerprint ({fingerprint}) does not match expected fingerprint ({expected})")]
+ FingerprintMismatch {
+ fingerprint: Fingerprint,
+ expected: Fingerprint,
+ },
+
+ /// Untrusted certificate with fingerprint information
+ #[error("certificate validation failed")]
+ UntrustedCertificate { fingerprint: Fingerprint },
+}
+
+// Useful for testing. Can't hide it behind `#[cfg(test)]` for integration tests, though.
+impl PartialEq for SslVerifyError {
+ fn eq(&self, other: &Self) -> bool {
+ match (self, other) {
+ (SslVerifyError::NoCertificate, SslVerifyError::NoCertificate) => true,
+ (SslVerifyError::InvalidFingerprint(_), SslVerifyError::InvalidFingerprint(_)) => true,
+ (
+ SslVerifyError::FingerprintMismatch {
+ fingerprint: a_fingerprint,
+ expected: a_expected,
+ },
+ SslVerifyError::FingerprintMismatch {
+ fingerprint: b_fingerprint,
+ expected: b_expected,
+ },
+ ) => a_fingerprint == b_fingerprint && a_expected == b_expected,
+ (
+ SslVerifyError::UntrustedCertificate {
+ fingerprint: a_fingerprint,
+ },
+ SslVerifyError::UntrustedCertificate {
+ fingerprint: b_fingerprint,
+ },
+ ) => a_fingerprint == b_fingerprint,
+ _ => false,
+ }
+ }
+}
+
+/// 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 get the leaf fp and check that against the given
+pub fn openssl_verify_callback(
+ openssl_valid: bool,
+ ctx: &mut X509StoreContextRef,
+ expected_fp: Option<Fingerprint>,
+) -> Result<(), SslVerifyError> {
+ match expected_fp {
+ Some(expected_fp) => {
+ let fingerprint = get_leaf_fp(ctx)?;
+ if expected_fp == fingerprint {
+ ctx.set_error(X509VerifyResult::OK);
+ Ok(())
+ } else {
+ Err(SslVerifyError::FingerprintMismatch {
+ fingerprint,
+ expected: expected_fp,
+ })
+ }
+ }
+ None if openssl_valid => Ok(()),
+ None => {
+ let fingerprint = get_leaf_fp(ctx)?;
+ Err(SslVerifyError::UntrustedCertificate { fingerprint })
+ }
+ }
+}
+
+fn get_leaf_cert(ctx: &X509StoreContextRef) -> Option<&X509Ref> {
+ let chain = ctx.chain()?;
+ chain.get(0)
+}
+
+fn get_leaf_fp(ctx: &X509StoreContextRef) -> Result<Fingerprint, SslVerifyError> {
+ let leaf_cert = get_leaf_cert(ctx).ok_or_else(|| SslVerifyError::NoCertificate)?;
+ leaf_cert
+ .try_into()
+ .map_err(SslVerifyError::InvalidFingerprint)
+}
--
2.47.3
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [PATCH proxmox v4 2/8] http: tls: use legacy behavior when PROXMOX_OLD_TLS_CHECK is set to "1"
2026-07-01 10:30 [PATCH proxmox{,-backup,-websocket-tunnel} v4 0/8] unify openssl callback logic Dominik Csapak
2026-07-01 10:30 ` [PATCH proxmox v4 1/8] http: factor out openssl verification callback Dominik Csapak
@ 2026-07-01 10:30 ` Dominik Csapak
2026-07-01 13:36 ` Shannon Sterz
2026-07-01 10:30 ` [PATCH proxmox v4 3/8] http: tls: add warning if old check behavior is enabled and triggered Dominik Csapak
` (6 subsequent siblings)
8 siblings, 1 reply; 13+ messages in thread
From: Dominik Csapak @ 2026-07-01 10:30 UTC (permalink / raw)
To: pve-devel, pbs-devel
If that environment variable is set to "1", give the openssl result
priority, and potentially ignore a given fingerprint that is not
matching. If that's the case, print a warning.
Co-developed-by: Shannon Sterz <s.sterz@proxmox.com>
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
proxmox-http/src/tls.rs | 15 +++++++++++++--
1 file changed, 13 insertions(+), 2 deletions(-)
diff --git a/proxmox-http/src/tls.rs b/proxmox-http/src/tls.rs
index 5c2b1743..abdf51e9 100644
--- a/proxmox-http/src/tls.rs
+++ b/proxmox-http/src/tls.rs
@@ -103,6 +103,8 @@ impl std::str::FromStr for Fingerprint {
}
}
+pub const PROXMOX_OLD_TLS_CHECK_VAR: &str = "PROXMOX_OLD_TLS_CHECK";
+
///
/// Error type returned by failed [`openssl_verify_callback`].
///
@@ -159,15 +161,24 @@ impl PartialEq for SslVerifyError {
/// Intended as an openssl verification callback.
///
-/// The following things are checked:
+/// If the 'PROXMOX_OLD_TLS_CHECK' environment variable is not set to "1",
+/// the following things are checked:
///
/// * If no fingerprint is given, return the openssl verification result
-/// * If a fingerprint is given get the leaf fp and check that against the given
+/// * If a fingerprint is given, check it against the leaf fp
+///
+/// Otherwise, we trust the openssl result if the whole chain was trusted
pub fn openssl_verify_callback(
openssl_valid: bool,
ctx: &mut X509StoreContextRef,
expected_fp: Option<Fingerprint>,
) -> Result<(), SslVerifyError> {
+ let old_check = matches!(std::env::var(PROXMOX_OLD_TLS_CHECK_VAR).as_deref(), Ok("1"));
+
+ if old_check && openssl_valid {
+ return Ok(());
+ }
+
match expected_fp {
Some(expected_fp) => {
let fingerprint = get_leaf_fp(ctx)?;
--
2.47.3
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [PATCH proxmox v4 3/8] http: tls: add warning if old check behavior is enabled and triggered
2026-07-01 10:30 [PATCH proxmox{,-backup,-websocket-tunnel} v4 0/8] unify openssl callback logic Dominik Csapak
2026-07-01 10:30 ` [PATCH proxmox v4 1/8] http: factor out openssl verification callback Dominik Csapak
2026-07-01 10:30 ` [PATCH proxmox v4 2/8] http: tls: use legacy behavior when PROXMOX_OLD_TLS_CHECK is set to "1" Dominik Csapak
@ 2026-07-01 10:30 ` Dominik Csapak
2026-07-01 10:30 ` [PATCH proxmox v4 4/8] http: tls: add integration tests for openssl verify callbacks Dominik Csapak
` (5 subsequent siblings)
8 siblings, 0 replies; 13+ messages in thread
From: Dominik Csapak @ 2026-07-01 10:30 UTC (permalink / raw)
To: pve-devel, pbs-devel
only warn if we encounter the situation that a given fingerprint does
not match but we accept it anyway since openssl has priority due to
PROXMOX_OLD_TLS_CHECK=1.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
proxmox-http/Cargo.toml | 2 ++
proxmox-http/src/tls.rs | 8 ++++++++
2 files changed, 10 insertions(+)
diff --git a/proxmox-http/Cargo.toml b/proxmox-http/Cargo.toml
index cdc2861b..8a6af43c 100644
--- a/proxmox-http/Cargo.toml
+++ b/proxmox-http/Cargo.toml
@@ -21,6 +21,7 @@ http-body = { workspace = true, optional = true }
http-body-util = { workspace = true, optional = true }
hyper = { workspace = true, optional = true }
hyper-util = { workspace = true, optional = true, features = ["http2"] }
+log = { workspace = true, optional = true }
native-tls = { workspace = true, optional = true }
openssl = { version = "0.10", optional = true }
serde = { workspace = true, optional = true }
@@ -110,6 +111,7 @@ websocket = [
"body",
]
tls = [
+ "dep:log",
"dep:openssl",
"dep:hex",
"dep:thiserror",
diff --git a/proxmox-http/src/tls.rs b/proxmox-http/src/tls.rs
index abdf51e9..3c68be9d 100644
--- a/proxmox-http/src/tls.rs
+++ b/proxmox-http/src/tls.rs
@@ -176,6 +176,14 @@ pub fn openssl_verify_callback(
let old_check = matches!(std::env::var(PROXMOX_OLD_TLS_CHECK_VAR).as_deref(), Ok("1"));
if old_check && openssl_valid {
+ if ctx.error_depth() == 0 && expected_fp.is_some() && expected_fp != get_leaf_fp(ctx).ok() {
+ log::warn!(
+ "Mismatched fingerprint given, but openssl result was valid, ignoring fingerprint!"
+ );
+ log::warn!(
+ "To switch to new behavior remove PROXMOX_OLD_TLS_CHECK=1 from your environment."
+ );
+ }
return Ok(());
}
--
2.47.3
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [PATCH proxmox v4 4/8] http: tls: add integration tests for openssl verify callbacks
2026-07-01 10:30 [PATCH proxmox{,-backup,-websocket-tunnel} v4 0/8] unify openssl callback logic Dominik Csapak
` (2 preceding siblings ...)
2026-07-01 10:30 ` [PATCH proxmox v4 3/8] http: tls: add warning if old check behavior is enabled and triggered Dominik Csapak
@ 2026-07-01 10:30 ` Dominik Csapak
2026-07-01 10:30 ` [PATCH proxmox v4 5/8] client: use proxmox-http's openssl verification callback Dominik Csapak
` (4 subsequent siblings)
8 siblings, 0 replies; 13+ messages in thread
From: Dominik Csapak @ 2026-07-01 10:30 UTC (permalink / raw)
To: pve-devel, pbs-devel
From: Shannon Sterz <s.sterz@proxmox.com>
these integration tests intend to clearly encode how TLS verification
is handled. they work by spawning a TLS server via `openssl s_server`
and connecting to it. to avoid having the tests fail due to there not
being an open port, a unix socket is used for the connection.
encoded in these tests is the following behaviour for the callback:
1. if a single certificate is used by the server (for example, a
self-signed certificate):
a. if the fingerprint matches the certificate -> valid connection
b. if no fingerprint was provided -> fall back to OpenSSL's checks
2. if a certificate chain was provided by the server:
a. if the fingerprint matches the leaf certificate of the chain ->
valid connection
b. if no fingerprint was provided -> fall back to OpenSSL's checks
tests for the new and old behavior differ in one key way: the old
behavior accepted connections that did not provide a certificate
(chain) matching the fingerprint but were valid according to OpenSSL.
this broke the implicit assumption that providing a fingerprint acted
like "pinning" a certificate. the new behavior does not accept such
connections.
note that 2.a.) means that only leaf certificates can be pinned, not
root or intermediate ones. This makes sense as it's the usual behavior
for such parameters and checking too against the root or intermediates
would be rather surprising.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
[DC: Adapted to reversed env variable, restrict fp check to leaf, adapt
commit message]
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
Cargo.toml | 1 +
proxmox-http/Cargo.toml | 2 +
proxmox-http/tests/certs/cert-chain.pem | 46 ++
.../tests/certs/intermediate-cert.pem | 23 +
proxmox-http/tests/certs/intermediate-csr.pem | 17 +
proxmox-http/tests/certs/intermediate-key.pem | 28 ++
proxmox-http/tests/certs/leaf-cert.pem | 24 ++
proxmox-http/tests/certs/leaf-csr.pem | 17 +
proxmox-http/tests/certs/leaf-key.pem | 28 ++
proxmox-http/tests/certs/root-cert.pem | 23 +
proxmox-http/tests/certs/root-key.pem | 28 ++
proxmox-http/tests/certs/self-signed-cert.pem | 23 +
proxmox-http/tests/certs/self-signed-key.pem | 28 ++
proxmox-http/tests/common/mod.rs | 395 ++++++++++++++++++
proxmox-http/tests/openssl_verify_cb_new.rs | 89 ++++
proxmox-http/tests/openssl_verify_cb_old.rs | 77 ++++
16 files changed, 849 insertions(+)
create mode 100644 proxmox-http/tests/certs/cert-chain.pem
create mode 100644 proxmox-http/tests/certs/intermediate-cert.pem
create mode 100644 proxmox-http/tests/certs/intermediate-csr.pem
create mode 100644 proxmox-http/tests/certs/intermediate-key.pem
create mode 100644 proxmox-http/tests/certs/leaf-cert.pem
create mode 100644 proxmox-http/tests/certs/leaf-csr.pem
create mode 100644 proxmox-http/tests/certs/leaf-key.pem
create mode 100644 proxmox-http/tests/certs/root-cert.pem
create mode 100644 proxmox-http/tests/certs/root-key.pem
create mode 100644 proxmox-http/tests/certs/self-signed-cert.pem
create mode 100644 proxmox-http/tests/certs/self-signed-key.pem
create mode 100644 proxmox-http/tests/common/mod.rs
create mode 100644 proxmox-http/tests/openssl_verify_cb_new.rs
create mode 100644 proxmox-http/tests/openssl_verify_cb_old.rs
diff --git a/Cargo.toml b/Cargo.toml
index fd219e7e..c8896e76 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -139,6 +139,7 @@ serde_plain = "1.0"
syn = { version = "2", features = [ "full", "visit-mut" ] }
sync_wrapper = "1"
tar = "0.4"
+tempfile = "3.15"
termcolor = "1.1.2"
thiserror = "2"
tokio = "1.6"
diff --git a/proxmox-http/Cargo.toml b/proxmox-http/Cargo.toml
index 8a6af43c..bb0d18e1 100644
--- a/proxmox-http/Cargo.toml
+++ b/proxmox-http/Cargo.toml
@@ -47,6 +47,8 @@ proxmox-compression = { workspace = true, optional = true }
[dev-dependencies]
tokio = { workspace = true, features = [ "macros" ] }
flate2 = { workspace = true }
+proxmox-sys = { workspace = true }
+tempfile = { workspace = true }
[features]
default = []
diff --git a/proxmox-http/tests/certs/cert-chain.pem b/proxmox-http/tests/certs/cert-chain.pem
new file mode 100644
index 00000000..a88ae376
--- /dev/null
+++ b/proxmox-http/tests/certs/cert-chain.pem
@@ -0,0 +1,46 @@
+-----BEGIN CERTIFICATE-----
+MIID0zCCArugAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCQVQx
+DzANBgNVBAgMBlZpZW5uYTEPMA0GA1UEBwwGVmllbm5hMRAwDgYDVQQKDAdQcm94
+bW94MRAwDgYDVQQLDAdQcm94bW94MScwJQYDVQQDDB50bHMtY2VydC12YWxpZGF0
+aW9uLXJvb3QudGVzdHMwIBcNMjYwNjI0MTM0MjAxWhgPMzAyNTEwMjUxMzQyMDFa
+MHMxCzAJBgNVBAYTAkFUMQ8wDQYDVQQIDAZWaWVubmExEDAOBgNVBAoMB1Byb3ht
+b3gxEDAOBgNVBAsMB1Byb3htb3gxLzAtBgNVBAMMJnRscy1jZXJ0LXZhbGlkYXRp
+b24taW50ZXJtZWRpYXRlLnRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEA2/UMNpaQ7WsTmUFvXO/FyGcKSaAjcLvfj15ezjcJxlDr1S580Z3T7kph
+boR5K8iH8nJa0Ujv7ty2c0HgpKZIduYzZ5+hv1K7czbpQRqoY5956O06VCUuIYC5
+Oa2byEV10PBojckd/opFW0rhCR1+VTtRLXinQ64RVdcfnHVbWpV8QBks96ZeNHAX
+mv5FlYICHfgCQm8FAljN1X3dQXkroHErmXDw6BXGrfYBnXlI4p+e5cj9pBJHqRJ0
+XGsnoAv1xmEmgsSm6S+wRZ/+dtY6rs3N//qz6J9ZuyAccjKpY5yS1Xa6/f+7IJFd
+W7MGh+DZ6kWJ1MxAbTKcIUo4P4GOUQIDAQABo2YwZDAdBgNVHQ4EFgQUa+cbP/Az
+gyufv3AtFJdQ7tY6iGswHwYDVR0jBBgwFoAUsI4iO4yr1STBmK9TWO0+bgf9fCsw
+EgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQEL
+BQADggEBAFq9zeICWKkM7P5D2e0f1tUt4krV45D7avZqkNe7tlglm9g66UlDHXH9
+bvVPYkPG3mHSn0QtEykTgzCB5l1EgL3+OL32SoOBOQtrS0ePlmVhe05ydgqf7oFf
+EECvghk+Z96cSRtgPwrWp8U93C3Zggaf56QI7KqZg9QpYCjyAG+pGrhoPTmyMv8H
+PLxgGw7fJyviwHLrHCUxZridQ6w9fOT02aqsIoock9df9YBJuDKJILDI3oXaeKds
+y09qJ/2kwJPlHrtyvqOdWrrJz/x3trpOO1cpH96VttYW0usHOsWdgqvc9tR7Axwf
+TYthAFu4PV97914MfSvvPPEyP+C6NIA=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIID6zCCAtOgAwIBAgIUcHTmdgi8DylmBhrTr4NBKpnLBk4wDQYJKoZIhvcNAQEL
+BQAwfDELMAkGA1UEBhMCQVQxDzANBgNVBAgMBlZpZW5uYTEPMA0GA1UEBwwGVmll
+bm5hMRAwDgYDVQQKDAdQcm94bW94MRAwDgYDVQQLDAdQcm94bW94MScwJQYDVQQD
+DB50bHMtY2VydC12YWxpZGF0aW9uLXJvb3QudGVzdHMwIBcNMjYwNjI0MTMzODIw
+WhgPMzAyNTEwMjUxMzM4MjBaMHwxCzAJBgNVBAYTAkFUMQ8wDQYDVQQIDAZWaWVu
+bmExDzANBgNVBAcMBlZpZW5uYTEQMA4GA1UECgwHUHJveG1veDEQMA4GA1UECwwH
+UHJveG1veDEnMCUGA1UEAwwedGxzLWNlcnQtdmFsaWRhdGlvbi1yb290LnRlc3Rz
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoIyemjaxlCB1jDMgBooZ
+/Bhhprhbz6OxoAGFpI8T7ppzZFKoFOWIHEJcYrvqAQn0RqZe8EIUXbquIYjBZPnA
+bwxrZHMgcC5J/f0pRD+N68uCUU2EPaoHqk+v/YMM+zTViftWIFpU17HfSmEkPR4l
+9OljcYendp1nMRsRB3dxTPmoK/D1E87LVrz7GlsuoFDJE3CfBQ6eNUMSdNbicDF0
+xwzyzQsCGuCboHa1Q+fFU1Se3Lts1S+X7TdX2XySvFziq3wkAKaMZ46C4wM25+Z7
+Is+kBDxIxaPq13XdpneG5Iis2hT2ruWSbopuD57Zuh83/HQSSNlaaPFivib2O9kA
+1QIDAQABo2MwYTAdBgNVHQ4EFgQUsI4iO4yr1STBmK9TWO0+bgf9fCswHwYDVR0j
+BBgwFoAUsI4iO4yr1STBmK9TWO0+bgf9fCswDwYDVR0TAQH/BAUwAwEB/zAOBgNV
+HQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggEBAEM83xjtDyctuwYTCuwM7KRt
+StuAmtnw/snfBJZgCKNPi0fOM1rzwM1g/h4LrH7go9VpxQ2VtT+9/20MBhlqWdAN
+sI1IpUMArsmzlKaBZUZDrS3An9iRztsmnftLfkXyku6nUcb8TPDmE5r1arnDngsX
++EcINC1DgOTy4Sv9vWv6apJQtNg+/Xqs+Ax+4iIXDJde28SX7p8vTdkBKLhHnGLJ
+zrEI2DzGqy8+sPTKSYGw3oNH3QUwf3FJnZKJGifmiehzdHkVKF3XesBddQjWOM/y
+E4yYJqlwpDhykDEz5d6sD6F/5mw3LOqk2J2jfDbPN5IEagYEDMzAqx10Wi7U+94=
+-----END CERTIFICATE-----
diff --git a/proxmox-http/tests/certs/intermediate-cert.pem b/proxmox-http/tests/certs/intermediate-cert.pem
new file mode 100644
index 00000000..4a56c76c
--- /dev/null
+++ b/proxmox-http/tests/certs/intermediate-cert.pem
@@ -0,0 +1,23 @@
+-----BEGIN CERTIFICATE-----
+MIID0zCCArugAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCQVQx
+DzANBgNVBAgMBlZpZW5uYTEPMA0GA1UEBwwGVmllbm5hMRAwDgYDVQQKDAdQcm94
+bW94MRAwDgYDVQQLDAdQcm94bW94MScwJQYDVQQDDB50bHMtY2VydC12YWxpZGF0
+aW9uLXJvb3QudGVzdHMwIBcNMjYwNjI0MTM0MjAxWhgPMzAyNTEwMjUxMzQyMDFa
+MHMxCzAJBgNVBAYTAkFUMQ8wDQYDVQQIDAZWaWVubmExEDAOBgNVBAoMB1Byb3ht
+b3gxEDAOBgNVBAsMB1Byb3htb3gxLzAtBgNVBAMMJnRscy1jZXJ0LXZhbGlkYXRp
+b24taW50ZXJtZWRpYXRlLnRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEA2/UMNpaQ7WsTmUFvXO/FyGcKSaAjcLvfj15ezjcJxlDr1S580Z3T7kph
+boR5K8iH8nJa0Ujv7ty2c0HgpKZIduYzZ5+hv1K7czbpQRqoY5956O06VCUuIYC5
+Oa2byEV10PBojckd/opFW0rhCR1+VTtRLXinQ64RVdcfnHVbWpV8QBks96ZeNHAX
+mv5FlYICHfgCQm8FAljN1X3dQXkroHErmXDw6BXGrfYBnXlI4p+e5cj9pBJHqRJ0
+XGsnoAv1xmEmgsSm6S+wRZ/+dtY6rs3N//qz6J9ZuyAccjKpY5yS1Xa6/f+7IJFd
+W7MGh+DZ6kWJ1MxAbTKcIUo4P4GOUQIDAQABo2YwZDAdBgNVHQ4EFgQUa+cbP/Az
+gyufv3AtFJdQ7tY6iGswHwYDVR0jBBgwFoAUsI4iO4yr1STBmK9TWO0+bgf9fCsw
+EgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQEL
+BQADggEBAFq9zeICWKkM7P5D2e0f1tUt4krV45D7avZqkNe7tlglm9g66UlDHXH9
+bvVPYkPG3mHSn0QtEykTgzCB5l1EgL3+OL32SoOBOQtrS0ePlmVhe05ydgqf7oFf
+EECvghk+Z96cSRtgPwrWp8U93C3Zggaf56QI7KqZg9QpYCjyAG+pGrhoPTmyMv8H
+PLxgGw7fJyviwHLrHCUxZridQ6w9fOT02aqsIoock9df9YBJuDKJILDI3oXaeKds
+y09qJ/2kwJPlHrtyvqOdWrrJz/x3trpOO1cpH96VttYW0usHOsWdgqvc9tR7Axwf
+TYthAFu4PV97914MfSvvPPEyP+C6NIA=
+-----END CERTIFICATE-----
diff --git a/proxmox-http/tests/certs/intermediate-csr.pem b/proxmox-http/tests/certs/intermediate-csr.pem
new file mode 100644
index 00000000..824248d8
--- /dev/null
+++ b/proxmox-http/tests/certs/intermediate-csr.pem
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIICyjCCAbICAQAwgYQxCzAJBgNVBAYTAkFUMQ8wDQYDVQQIDAZWaWVubmExDzAN
+BgNVBAcMBlZpZW5uYTEQMA4GA1UECgwHUHJveG1veDEQMA4GA1UECwwHUHJveG1v
+eDEvMC0GA1UEAwwmdGxzLWNlcnQtdmFsaWRhdGlvbi1pbnRlcm1lZGlhdGUudGVz
+dHMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDb9Qw2lpDtaxOZQW9c
+78XIZwpJoCNwu9+PXl7ONwnGUOvVLnzRndPuSmFuhHkryIfyclrRSO/u3LZzQeCk
+pkh25jNnn6G/UrtzNulBGqhjn3no7TpUJS4hgLk5rZvIRXXQ8GiNyR3+ikVbSuEJ
+HX5VO1EteKdDrhFV1x+cdVtalXxAGSz3pl40cBea/kWVggId+AJCbwUCWM3Vfd1B
+eSugcSuZcPDoFcat9gGdeUjin57lyP2kEkepEnRcayegC/XGYSaCxKbpL7BFn/52
+1jquzc3/+rPon1m7IBxyMqljnJLVdrr9/7sgkV1bswaH4NnqRYnUzEBtMpwhSjg/
+gY5RAgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAQEAsI2k85A0oMQRAqeZEG7mV48y
+65FQ/WzdnIe1/SsmGYnrJ4ZWZF69+hRCTaVrEJEIlbD1lT+teKYlE1x4aMR8zaWo
+CymBFfDAjRUyR4s38BliXslekivC7o8IUcyi7prjOvMHtK3p+1f+wPCyz6jDSD9V
+9LuHi7kdZBfCUIxBtPtGrGdouf+s6LkTv64DGyldsturDl3CnRvUaRDt95qc6gUW
+YhMLv/bzxet75htvk4H2VhpZn/ZKhJRNebetDFWH0LWB5IouLOBdDsDkWRMaV0x9
+HYbG3jZ8NgSHmqAxqxSq/dt6XSJ9mpVl9vTaAPxq05v337j54FC34oz0q0pK9Q==
+-----END CERTIFICATE REQUEST-----
diff --git a/proxmox-http/tests/certs/intermediate-key.pem b/proxmox-http/tests/certs/intermediate-key.pem
new file mode 100644
index 00000000..beb4bad6
--- /dev/null
+++ b/proxmox-http/tests/certs/intermediate-key.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDb9Qw2lpDtaxOZ
+QW9c78XIZwpJoCNwu9+PXl7ONwnGUOvVLnzRndPuSmFuhHkryIfyclrRSO/u3LZz
+QeCkpkh25jNnn6G/UrtzNulBGqhjn3no7TpUJS4hgLk5rZvIRXXQ8GiNyR3+ikVb
+SuEJHX5VO1EteKdDrhFV1x+cdVtalXxAGSz3pl40cBea/kWVggId+AJCbwUCWM3V
+fd1BeSugcSuZcPDoFcat9gGdeUjin57lyP2kEkepEnRcayegC/XGYSaCxKbpL7BF
+n/521jquzc3/+rPon1m7IBxyMqljnJLVdrr9/7sgkV1bswaH4NnqRYnUzEBtMpwh
+Sjg/gY5RAgMBAAECggEABQOjEaOBDkiAm9/IACBfK+Bddaw24p0FzajjFGRgzqqN
+lcCHi+fDKw17Bx/x+zOJFdfRhi/ZeGKDrkD0NAyuXjeFOHmFIG1sZIX970QCTrMV
+/l9aEwz97jmW/1+by4b51peEaqcJDgJs7lXYp3KKrLq7cQPtHDfdoU1UJSbvvDLd
+Xl7gZ4WYla4EAncFsA1K5jBuANG66mO8Gmumz+eLmslUB2RVLxPrVx0PybSXxQII
+rnzd6V24DV6+VkN0lBVm1k98Is4hbCEmMyZGweWX9wjdCzMPgqqPHbto7gY8UkPM
+Iz+BwbTI59pM9/m0BLtPxUd7Sccy076L0PtC3XYJeQKBgQDwLISbPRjJaGnJMpU9
+8M5+6n0D8E+BlRCS6Qkav6X/gy6rdNAl5hQjDaSZk72D7a+DGJUBsa9F9GgIw8pW
+Btkd+4/ukqLP3aoSKTKTRtt7tFZMPi+5CJJSGUDIDbmS0XrxxNMudu7kO8ODmPE3
+w5uHSYztw+I6yGMNWY5GmCdjOwKBgQDqc37YmadECvq1IwHZiXWprDowe/qL+VYy
+VFZj/zGABRxSGOoIc/AW8MjbDh+WEyErIkVkxj3AZ8N85sziIJH0+K823DWtiILR
+zuUJ1GrVfTzkGjFTrrCJd6Ev0fBKO7/jtpGOYD9XpCtEoxz30RDNjq+aIv57B53w
+3+Gq7tKj4wKBgQCjKtt8S+nHC3SzB/Z0emEPwGbmgiDBvG/iHwfccE9qY8kVGus+
+lC0iE2a8H68lLhmLSuwQlpKpR/5V1g5km4pt4DZMsrqB1epxJCQEAqOiS0ZFzgnF
+/5jIxfdI8moc4MxR7JI8gviRfji58vIOHIpRQxrHfcj4fqMssqcCNuSreQKBgQCt
+EC5tQxcGkjg4p6vA4cg6REj76ziqRJaNNlZDIGhwwNUEASIYtURgGsOZd9Z3GI3e
+YkDpP7Drq2zRcSmCLlqvgzcLfwgcne07ZMcLN4LZLsZY9sC8rfHgt68DNqxyj6J5
+PBY8C+4WCrhpxSIoCGqn4hDb7cL+HERJP2o8nGhe0wKBgDddzovqa7xEJAJAKFU9
+YiZGhSq7sB/1MXEAzmbQLFUmuMpQFEVv4hjXry1d3n51Y2M8AbOLRttZD5myN/QT
+BFoKmfQzOY5IydfLy4XGmsB/dVovd2Sq3JUuqeA9rtPvkGZcLppiipGc8mkSxpI2
+RGcpj1GoYLQKcFQraBdFWxhG
+-----END PRIVATE KEY-----
diff --git a/proxmox-http/tests/certs/leaf-cert.pem b/proxmox-http/tests/certs/leaf-cert.pem
new file mode 100644
index 00000000..a6260999
--- /dev/null
+++ b/proxmox-http/tests/certs/leaf-cert.pem
@@ -0,0 +1,24 @@
+-----BEGIN CERTIFICATE-----
+MIID9DCCAtygAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwczELMAkGA1UEBhMCQVQx
+DzANBgNVBAgMBlZpZW5uYTEQMA4GA1UECgwHUHJveG1veDEQMA4GA1UECwwHUHJv
+eG1veDEvMC0GA1UEAwwmdGxzLWNlcnQtdmFsaWRhdGlvbi1pbnRlcm1lZGlhdGUu
+dGVzdHMwIBcNMjYwNjI0MTM1MTA1WhgPMzAyNTEwMjUxMzUxMDVaMHwxCzAJBgNV
+BAYTAkFUMQ8wDQYDVQQIDAZWaWVubmExDzANBgNVBAcMBlZpZW5uYTEQMA4GA1UE
+CgwHUHJveG1veDEQMA4GA1UECwwHUHJveG1veDEnMCUGA1UEAwwedGxzLWNlcnQt
+dmFsaWRhdGlvbi1sZWFmLnRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEAkHL4lBzjkvGm0iNRwLijTKqLIYL5kjr2rfepjtFV0f/s+WDjSUcbgryv
+VrjoAazt4G4huxFsWA1Yc/wq90ut5FQCn3zID/XUTjloTOE7S7jyc+a+agbZNMEj
+R2udNqNPIMAUDnCss5PDq/S+15lnPdkK8YWk4vl2hT4GiU3P8xv6WZQPWlJA0gRe
+mA2/sCdzxux0YP5yOUPJ9ZnYTgJyXdZ1WigsjEwzKgjWBzmRMDp81T02Pyan8oUq
+frCzC/jIoeyC7JemZCzVaEhkbRRmh9eS3O9trIjjeXGVdi04JUDdeecFWXVRO9G2
+xRc71L4OxHuHUoJNyYbIkn6j5IiaewIDAQABo4GGMIGDMAkGA1UdEwQCMAAwEQYJ
+YIZIAYb4QgEBBAQDAgZAMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEF
+BQcDATAfBgNVHSMEGDAWgBRr5xs/8DODK5+/cC0Ul1Du1jqIazAdBgNVHQ4EFgQU
+xakweDrqyFkO8jE7UamgXUN/fOcwDQYJKoZIhvcNAQELBQADggEBALZaE4lP+Krm
+gEnFfG+CftOzC5zmjAI56aZJ661n87Lh/ECQdgRbvvtAqFGdmJi5GFRkesJSyfIx
+YGFj1mc2TC1BCv7bKJnoM2YFebXHiTJ+/ODuboiGEdbm5xWaowjgWutaXHr7O/aa
+KV9mzFasJegO3i4rCVFfnNVDW+aq23OTYkgNiJjp0H2sSckzsyQFmYLqMPzGXk1F
+ed2YpoNv4GT+HOfdP6ExWH1cL6AZI+jJ7/fEbBiEFyAsfaW7c8knphe/MGztWgCp
+ayCoe48IWwGS2nzdUvjw0w+QF6PpLo6CiFap8qiALlvKK6hijsrKFpVvD1PxggRA
+3mYsWKaRuHo=
+-----END CERTIFICATE-----
diff --git a/proxmox-http/tests/certs/leaf-csr.pem b/proxmox-http/tests/certs/leaf-csr.pem
new file mode 100644
index 00000000..318d2e37
--- /dev/null
+++ b/proxmox-http/tests/certs/leaf-csr.pem
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIICwTCCAakCAQAwfDELMAkGA1UEBhMCQVQxDzANBgNVBAgMBlZpZW5uYTEPMA0G
+A1UEBwwGVmllbm5hMRAwDgYDVQQKDAdQcm94bW94MRAwDgYDVQQLDAdQcm94bW94
+MScwJQYDVQQDDB50bHMtY2VydC12YWxpZGF0aW9uLWxlYWYudGVzdHMwggEiMA0G
+CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCQcviUHOOS8abSI1HAuKNMqoshgvmS
+Ovat96mO0VXR/+z5YONJRxuCvK9WuOgBrO3gbiG7EWxYDVhz/Cr3S63kVAKffMgP
+9dROOWhM4TtLuPJz5r5qBtk0wSNHa502o08gwBQOcKyzk8Or9L7XmWc92QrxhaTi
++XaFPgaJTc/zG/pZlA9aUkDSBF6YDb+wJ3PG7HRg/nI5Q8n1mdhOAnJd1nVaKCyM
+TDMqCNYHOZEwOnzVPTY/JqfyhSp+sLML+Mih7ILsl6ZkLNVoSGRtFGaH15Lc722s
+iON5cZV2LTglQN155wVZdVE70bbFFzvUvg7Ee4dSgk3JhsiSfqPkiJp7AgMBAAGg
+ADANBgkqhkiG9w0BAQsFAAOCAQEAGtog/UF1Kfs4YP+T0SBoyvKKhPiKQo9muW1b
+YvdcBbWdRSa9YGNVXRCmvDDLzfzH0DoDvQzRcf8OC2wtytB5s0NF5SA+BdRQFcoi
+RIUPx1AmJ6fVXpE0lLB54hjdw+ngq2WBMJN/cWzC0lb+6vckTTH7g9LKp2yn0pUh
+Ycf0O8sD4LCQNJn7yN3TwmpiALStW04yg6CY3KKn2ZeoHbDxfs3Tyyf4mVX2SppX
+50Fnj16Ykypd5iQSGWETZc7/cYdO2d+BX/6G7x91HuQRLV/1/jYVd00G3horuaiw
+P+V84LrJh6/ZOJc660IK+CEn9vjVQ5y/zrSQMfe4cFStc79dFA==
+-----END CERTIFICATE REQUEST-----
diff --git a/proxmox-http/tests/certs/leaf-key.pem b/proxmox-http/tests/certs/leaf-key.pem
new file mode 100644
index 00000000..f64392e2
--- /dev/null
+++ b/proxmox-http/tests/certs/leaf-key.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCQcviUHOOS8abS
+I1HAuKNMqoshgvmSOvat96mO0VXR/+z5YONJRxuCvK9WuOgBrO3gbiG7EWxYDVhz
+/Cr3S63kVAKffMgP9dROOWhM4TtLuPJz5r5qBtk0wSNHa502o08gwBQOcKyzk8Or
+9L7XmWc92QrxhaTi+XaFPgaJTc/zG/pZlA9aUkDSBF6YDb+wJ3PG7HRg/nI5Q8n1
+mdhOAnJd1nVaKCyMTDMqCNYHOZEwOnzVPTY/JqfyhSp+sLML+Mih7ILsl6ZkLNVo
+SGRtFGaH15Lc722siON5cZV2LTglQN155wVZdVE70bbFFzvUvg7Ee4dSgk3JhsiS
+fqPkiJp7AgMBAAECggEAQ9brLcx/iNSbD1ftHkDY2LnDzAJSKb4teji1VlC0KIM0
+jU5WkGSn4/evtV/z/k10DpJKnyuooZXq89X7a9cMHQ7jiHm3D9/ZTL+jX2/sRDzh
+CVPWG7+JpUALzJAa7r01/WCYSsvaICCGpiy0sFboaOCVRicI8FxOsHcX5MY5oqfN
+M15GIA+jCwfiLQI2JuBE1Q39ieMpRsA0zG+itSm+EOYacHDduUjtLEgSh0fXhSrF
+q1rwK7kdNMGZNg9FdDtROX4FCrQvEjWOj1hl2Z/GzOMkpJu4SCrDIzXPppfkjGbF
+KUBcZ67EEChd8EjJ8LYOuKJf8XrIr4HJRhsJP51syQKBgQDMB27OrWeZHnCisXtr
+Ve3CSCp/BaZ/6fgPe5fufqcymIDb61bgzk1MqwT3pfP8/YiRZfokZT87ehox3gML
+HfAq8STWb4mVp7s/YuAFwlXFhTlBB4Fwge7wy7CjmCKgtknxMiiO8voCCsN0GQLw
+MwGjqTHF4q98iGTNn0XoJWE49QKBgQC1PmEokad/9sHLNr1Jjke8R7K9tNUfFTQP
+m5S/LUfXwcXsJx6QEWnVNgDXvWIq1WnTfwbSnfu+XTrjiaTAi4ZWXusicpTd30em
+3VXDIdXwYJSCU3OFWPQhbCnWDx4aVoJEb44KrGvCC/LJkweXzEminoTDMWdsGscI
+Ik25NFkfrwKBgQCJRqsAdl3RAVEpth7jXkKFyMaHBoc7Y3HbAP59okve2AtDbPnc
+chJCdoL2GXurie6cXa/LUzATVZlQWh9UGIWibvOpMAyzW9K52E4AsfvB1Vxra6Bk
+0Zex/mrP96m81kmz9lqhq8wZGaLed4GpmbgNpOZvTZFjSeYBD5wakSP0DQKBgEC+
+VtC6Lz6L9DBWjomfFMsSRax004j19xH4PsuILljJdJ1mYAmQ3uB2GRj4IwAwGkyd
+3N8R5mLbRPURL1REwylJYO9+ROV5JExcVo2NIbJrncFsdCDXZOYnkE5SOiuoaYJu
+4yu26gt4XzNYnWbBaDB6NezQUiSQ8DZcoq0dIRUrAoGBAMoYm7nV/q11i+08I9J1
+vbCoatf9BVp4kDlbAE3YxQw/QNxDkUfh9VDLIDrxYxDgrK8YS0tZJgHGEmWLzpo9
+8/L5NoD1ayaRHboSBuoVsYCy/DI98cuTPWdCDa29jKXToLfgJCdc9lvk7u6vr2zX
+zAt212NdiNc+OWkwy3m7EYiL
+-----END PRIVATE KEY-----
diff --git a/proxmox-http/tests/certs/root-cert.pem b/proxmox-http/tests/certs/root-cert.pem
new file mode 100644
index 00000000..d6231c81
--- /dev/null
+++ b/proxmox-http/tests/certs/root-cert.pem
@@ -0,0 +1,23 @@
+-----BEGIN CERTIFICATE-----
+MIID6zCCAtOgAwIBAgIUcHTmdgi8DylmBhrTr4NBKpnLBk4wDQYJKoZIhvcNAQEL
+BQAwfDELMAkGA1UEBhMCQVQxDzANBgNVBAgMBlZpZW5uYTEPMA0GA1UEBwwGVmll
+bm5hMRAwDgYDVQQKDAdQcm94bW94MRAwDgYDVQQLDAdQcm94bW94MScwJQYDVQQD
+DB50bHMtY2VydC12YWxpZGF0aW9uLXJvb3QudGVzdHMwIBcNMjYwNjI0MTMzODIw
+WhgPMzAyNTEwMjUxMzM4MjBaMHwxCzAJBgNVBAYTAkFUMQ8wDQYDVQQIDAZWaWVu
+bmExDzANBgNVBAcMBlZpZW5uYTEQMA4GA1UECgwHUHJveG1veDEQMA4GA1UECwwH
+UHJveG1veDEnMCUGA1UEAwwedGxzLWNlcnQtdmFsaWRhdGlvbi1yb290LnRlc3Rz
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoIyemjaxlCB1jDMgBooZ
+/Bhhprhbz6OxoAGFpI8T7ppzZFKoFOWIHEJcYrvqAQn0RqZe8EIUXbquIYjBZPnA
+bwxrZHMgcC5J/f0pRD+N68uCUU2EPaoHqk+v/YMM+zTViftWIFpU17HfSmEkPR4l
+9OljcYendp1nMRsRB3dxTPmoK/D1E87LVrz7GlsuoFDJE3CfBQ6eNUMSdNbicDF0
+xwzyzQsCGuCboHa1Q+fFU1Se3Lts1S+X7TdX2XySvFziq3wkAKaMZ46C4wM25+Z7
+Is+kBDxIxaPq13XdpneG5Iis2hT2ruWSbopuD57Zuh83/HQSSNlaaPFivib2O9kA
+1QIDAQABo2MwYTAdBgNVHQ4EFgQUsI4iO4yr1STBmK9TWO0+bgf9fCswHwYDVR0j
+BBgwFoAUsI4iO4yr1STBmK9TWO0+bgf9fCswDwYDVR0TAQH/BAUwAwEB/zAOBgNV
+HQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggEBAEM83xjtDyctuwYTCuwM7KRt
+StuAmtnw/snfBJZgCKNPi0fOM1rzwM1g/h4LrH7go9VpxQ2VtT+9/20MBhlqWdAN
+sI1IpUMArsmzlKaBZUZDrS3An9iRztsmnftLfkXyku6nUcb8TPDmE5r1arnDngsX
++EcINC1DgOTy4Sv9vWv6apJQtNg+/Xqs+Ax+4iIXDJde28SX7p8vTdkBKLhHnGLJ
+zrEI2DzGqy8+sPTKSYGw3oNH3QUwf3FJnZKJGifmiehzdHkVKF3XesBddQjWOM/y
+E4yYJqlwpDhykDEz5d6sD6F/5mw3LOqk2J2jfDbPN5IEagYEDMzAqx10Wi7U+94=
+-----END CERTIFICATE-----
diff --git a/proxmox-http/tests/certs/root-key.pem b/proxmox-http/tests/certs/root-key.pem
new file mode 100644
index 00000000..e123e674
--- /dev/null
+++ b/proxmox-http/tests/certs/root-key.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCgjJ6aNrGUIHWM
+MyAGihn8GGGmuFvPo7GgAYWkjxPumnNkUqgU5YgcQlxiu+oBCfRGpl7wQhRduq4h
+iMFk+cBvDGtkcyBwLkn9/SlEP43ry4JRTYQ9qgeqT6/9gwz7NNWJ+1YgWlTXsd9K
+YSQ9HiX06WNxh6d2nWcxGxEHd3FM+agr8PUTzstWvPsaWy6gUMkTcJ8FDp41QxJ0
+1uJwMXTHDPLNCwIa4JugdrVD58VTVJ7cu2zVL5ftN1fZfJK8XOKrfCQApoxnjoLj
+Azbn5nsiz6QEPEjFo+rXdd2md4bkiKzaFPau5ZJuim4Pntm6Hzf8dBJI2Vpo8WK+
+JvY72QDVAgMBAAECggEAKhhkt3uSuQt/tqhrA7vvDz3XUM7y57D8cD8l6t1G9R9T
+FSFlB8GdHAe8UHkD5IzXGzUhHG6/B0pcwNcqGg8wCQ3hFJ/pB/DjHrDjwozFaedc
+vnOMMlzkEKA/PUHAxBb4zGp1jRsSNtHhAZAR3+KJQjt1gv12B7BCr8nwf5wuPWen
+7fswPrd0JJO1p1RjQyWeiM91LfJYOuo7rt6Froi1KVSVNUt3H9U664nZTbZM36q5
++e9d940yijX0ckggV35zwSOMyhhK3sISEhsaPulsPvp0nlGtDzhhZjDX6ocklbi/
+hE4fytuxZsEHjrJGA7XMsQ7GpJgxC96DB1Eczf3zOwKBgQDTCsLMlBHEhr+mj7GJ
+6pf03udCzIovC/0QaUVHkItFhxVnrHBhn9VzbqGITYQu6MF8kXsC7rIyt7Ad59DM
+n6L5r6imKt2R+UaaezkCC1c0MWBQQ9rjfO8Yw9f5wgw4nvKE5xEf1sq/374xJKfz
+6rqexTsHWBluYK6BnhJS8oqJRwKBgQDCwDjnW9xrN5exS8zFg8c101O/+MpsJeCU
+sx+oIcxIh/7Gn9ho/vmgL8uNyoro2t31BDhgrgBmUBs6cnDdg+P8X0IlySKsGKXj
+FY7F6IzRDJVgD4Opsn2VnpMJEJGMGcBrDJPwVb1sSKL7ELQhDWTQ5EdTsmGyijkj
+OvgsYj3zAwKBgQC1LLnK8xrFwoBpN1bM9Y56c5nJaNsARKR+IEGPjHFjwPIJTKo1
+xQdzz3fxEcr2km74x9P40n48uCEDq20/HZTGEZ0Q+h+5H20TVdG9BYtZjUIH5hjV
+zv1cH1UcXxAq05mTquKymKz6R9R5T+S3q72Ga/+e8Gz0qx9kuxU0DHAOJQKBgQDC
+U2/0W5MbYQN6I/qV86IpsU7WNXg2Za0sc3fZGrBuh1TP+NvGGPYYwthICZyGMS5c
+t/NRdQ5tCO3CakL4pgwt3RdyALsaIhYU+4PVMvCgAABlM9Xa1IG/c9Wfq+qvc1qu
+9oP/wm4ayHfoMYirmmPIlKAfgdU+g/Hzl3nfP8A05QKBgFHUfa79T7h7nK/YZqgf
+/gVrJRv4vQn80fFQtcebmKmiRcjg9lziOjc1Dozv23WNQZ2y5Cw3u1K8Se86HTmk
+sEK9JDc6q0icWA3bvVG8PGtdrv43CR1Gsg786T5/JcHyupWeqgLHUhvxDXKUU0Bf
+HCdFi6WsyhVZ6k2KOP13k+QY
+-----END PRIVATE KEY-----
diff --git a/proxmox-http/tests/certs/self-signed-cert.pem b/proxmox-http/tests/certs/self-signed-cert.pem
new file mode 100644
index 00000000..14e76081
--- /dev/null
+++ b/proxmox-http/tests/certs/self-signed-cert.pem
@@ -0,0 +1,23 @@
+-----BEGIN CERTIFICATE-----
+MIID6zCCAtOgAwIBAgIUSkItlHQ0qbnha5abhSSWT5BGJbQwDQYJKoZIhvcNAQEL
+BQAwgYMxCzAJBgNVBAYTAkFUMQ8wDQYDVQQIDAZWaWVubmExDzANBgNVBAcMBlZp
+ZW5uYTEQMA4GA1UECgwHUHJveG1veDEQMA4GA1UECwwHUHJveG1veDEuMCwGA1UE
+AwwldGxzLWNlcnQtdmFsaWRhdGlvbi1zZWxmLXNpZ25lZC50ZXN0czAgFw0yNjA2
+MjQxNDI0MzZaGA8zMDI1MTAyNTE0MjQzNlowgYMxCzAJBgNVBAYTAkFUMQ8wDQYD
+VQQIDAZWaWVubmExDzANBgNVBAcMBlZpZW5uYTEQMA4GA1UECgwHUHJveG1veDEQ
+MA4GA1UECwwHUHJveG1veDEuMCwGA1UEAwwldGxzLWNlcnQtdmFsaWRhdGlvbi1z
+ZWxmLXNpZ25lZC50ZXN0czCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+APlUtV9F35NimI62w+nZVU/eUZPF2YD8j4sjLwle+SmYCXC9VHlYme9OthrQSYAS
+p/ioNyjk8DVGhA6FqLpnKW7iCBRU5mai17uzULMkVcX2lcdHkjxOyNSiEwS8mb7s
+pbiSXUqmuFsWzfNuGR4LUtW4bgqljUkhvEOD2DSI9kshJAOp1VvepvtXTVcLwXJf
+sVyF9lWh5UBjRKhyAWNZQxxdPu3FadFwAcDedbPARVPb9jO/5CJWTG3GRC/4BMiM
+d8HE1uVvW/O++xu26dzY1Vj3brwJUJIZJtVcMOdC2YkOGbe3+5B1tSGqoTuJlRjH
+Js5rNgasviH0Y4PXzag+6z0CAwEAAaNTMFEwHQYDVR0OBBYEFDKcDkUpWP/PKzzT
+PXsLpS2krdBFMB8GA1UdIwQYMBaAFDKcDkUpWP/PKzzTPXsLpS2krdBFMA8GA1Ud
+EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAB0uNKO+MA7MJ51CO282DULb
+jH9r2srM4XRoUWwFu4bYgduUa50lOMv6FMIRq1S023HKXcBxSAoCLexOSXdvb5x4
+yEbyqYrzvc5RPZvI1sDqkBbvZ1ZB3SbboHSKzOV9ddYxp4XXA60fL17syPkIGpqG
+UNeX0I/vZfKgS/EPvYi65WO/bVpnEEAIz4hUs5IJk5o74s7Mz7wz5C4Dv08kJKjH
+E4yv5yDjIlID6ksorOTB5WFdEeTibB+mTSbBXZp6c6KUZnNKvVDAHRRCbsp/eNtr
+Nr06RcBqzgwjXhJGD0JNJfe8r/loRlhDCpvXZlOV7dYKDEOODCeeKaFCQwQXYPo=
+-----END CERTIFICATE-----
diff --git a/proxmox-http/tests/certs/self-signed-key.pem b/proxmox-http/tests/certs/self-signed-key.pem
new file mode 100644
index 00000000..97c8060a
--- /dev/null
+++ b/proxmox-http/tests/certs/self-signed-key.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQD5VLVfRd+TYpiO
+tsPp2VVP3lGTxdmA/I+LIy8JXvkpmAlwvVR5WJnvTrYa0EmAEqf4qDco5PA1RoQO
+hai6Zylu4ggUVOZmote7s1CzJFXF9pXHR5I8TsjUohMEvJm+7KW4kl1KprhbFs3z
+bhkeC1LVuG4KpY1JIbxDg9g0iPZLISQDqdVb3qb7V01XC8FyX7FchfZVoeVAY0So
+cgFjWUMcXT7txWnRcAHA3nWzwEVT2/Yzv+QiVkxtxkQv+ATIjHfBxNblb1vzvvsb
+tunc2NVY9268CVCSGSbVXDDnQtmJDhm3t/uQdbUhqqE7iZUYxybOazYGrL4h9GOD
+182oPus9AgMBAAECggEAVhS3W9HRa16ughM6l4mX6S+95XF48w3/dw+qJSebiY86
+ryhGunBrERKRT7easLOAN5rIFH/aKOKUJDlkNBr61JQIKxDWzRequNyjX34LeQH1
+2yvsIpMmxjbArzF4OVEVtCAgQm5GFvjMGR/pXxSUwEHhCB75JQcXKw4hfp3Mvsnf
+YxBPC6EV69B3mDwmJjcP6aAb0t4is0BFrkeg8os1DKKQPpc+NJaWZ8imhUmudEi2
+ZUwVQvJFqXbhFmWLw4nXK93/192uWNw7lxodPpJTJrfZU5uEWLV/1FjKysYWgtTd
+Z3Z73eiq6RdWdPxXnx9g8k/hVycH66XGGNw/VPykpwKBgQD+WsCKk0GVw3J3QU7Y
+VRMG3idkIV2VcM2SIalFG0t9sb7XRN354P0J2A3EBMuqWNw7SKo3GIsFscaIdh1t
+m3fLJgmccZJaWAeQsSt7tU291p6AkeNu0UQw8tK/v+GbljlUxumBZZ009BbRCv0Y
+qb05am2JWvBtmyfkntorg34bNwKBgQD68aL01KOW3MbJcGVFumTwaEuUsv9OTrA8
+SvlTxrVfXU5uncJ+P3g2a/IeS2IELKBsWNIuPAGFVrECPQjS9Y4oYgnaj5ahN30w
+GWQ3lYnzxdvohPUOW1cCwDg6Mz5+Mmx+ygL0Sb0YG36assElipVHB9PiRukuGHSH
+6oCkwpjvKwKBgAZXeOl7lm0HfHkgtbiLFnhbXZwPgOfS8i0sja3dalpt7hYr72Tl
+iSmPq3gxrmpG4ObRfvz0rbKspgiM+VrcP3ZfMmomIsIB495lrHHfKVsMWNNXz9XZ
+fdvCkiKZxCQ+8Jr+gp/pSqwhUdhQb9MHmGIwFx8Pl2MENVBr7YCcPK6tAoGAZzol
+LY+XJ8Tz5QNeNXvCb/6HMMkdKspFxteUjrjL/Um1rN0ql6JmQgTPmVSrIkp1R3yW
+ITy/52jM8b3HtnganVQO96BfdzwLPFEFn7PdBrFaj+C5qck7Fr+ZoZ9Y0rLNXK6e
+3nzC03rj7qEfwOCsHYcDyy4eV77pmMuHVb9TB/cCgYBgh9o+io9RP5N6J16Tx7bt
+6wQeBkiuixcKrUKGJKkgbacSRgiNNNFn/wbaOUhcVu7aktTI/yLDg8TFBBS2cbqn
+9i9iMoxx14+jCXYRnOFRcHafBTi+1S2uqxn97CxNM8RzE2wh4Q44ZeKahiEDAtZP
+fPO+8PfqWIBYr3NKod7Yeg==
+-----END PRIVATE KEY-----
diff --git a/proxmox-http/tests/common/mod.rs b/proxmox-http/tests/common/mod.rs
new file mode 100644
index 00000000..2067d7b6
--- /dev/null
+++ b/proxmox-http/tests/common/mod.rs
@@ -0,0 +1,395 @@
+use std::os::unix::net::UnixStream;
+use std::path::PathBuf;
+use std::process::{Child, Command, Stdio};
+use std::sync::atomic::AtomicU32;
+use std::sync::{Arc, Mutex};
+
+use openssl::ssl::{Ssl, SslContextBuilder, SslMethod, SslVerifyMode};
+use openssl::x509::X509;
+use openssl::x509::store::X509StoreBuilder;
+use tempfile::TempPath;
+
+use proxmox_http::{
+ Fingerprint, PROXMOX_OLD_TLS_CHECK_VAR, SslVerifyError, openssl_verify_callback,
+};
+
+/// Helper type to easily get proper certificates for a given test and their fingerprints.
+#[derive(Clone, Copy)]
+pub enum CertificateType {
+ SelfSigned,
+ CertificateChain,
+}
+
+impl CertificateType {
+ pub fn certificate_path(&self) -> &'static str {
+ match self {
+ Self::SelfSigned => "tests/certs/self-signed-cert.pem",
+ Self::CertificateChain => "tests/certs/leaf-cert.pem",
+ }
+ }
+
+ pub fn key_path(&self) -> &'static str {
+ match self {
+ Self::SelfSigned => "tests/certs/self-signed-key.pem",
+ Self::CertificateChain => "tests/certs/leaf-key.pem",
+ }
+ }
+
+ pub fn certificate_chain_path(&self) -> Option<&'static str> {
+ match self {
+ Self::SelfSigned => None,
+ Self::CertificateChain => Some("tests/certs/cert-chain.pem"),
+ }
+ }
+
+ pub fn leaf_certificate(&self) -> X509 {
+ let bytes = match self {
+ Self::SelfSigned => include_str!("../certs/self-signed-cert.pem").as_bytes(),
+ Self::CertificateChain => include_str!("../certs/leaf-cert.pem").as_bytes(),
+ };
+
+ X509::from_pem(bytes).expect("could not get certificate")
+ }
+
+ pub fn intermediate_certificate(&self) -> X509 {
+ let bytes = match self {
+ Self::SelfSigned => return self.leaf_certificate(),
+ Self::CertificateChain => include_str!("../certs/intermediate-cert.pem").as_bytes(),
+ };
+
+ X509::from_pem(bytes).expect("could not get certificate")
+ }
+
+ pub fn root_certificate(&self) -> X509 {
+ let bytes = match self {
+ Self::SelfSigned => return self.leaf_certificate(),
+ Self::CertificateChain => include_str!("../certs/root-cert.pem").as_bytes(),
+ };
+
+ X509::from_pem(bytes).expect("could not get certificate")
+ }
+
+ pub fn leaf_fingerprint(&self) -> Fingerprint {
+ Self::fingerprint_for_cert(&self.leaf_certificate())
+ }
+
+ pub fn intermediate_fingerprint(&self) -> Fingerprint {
+ Self::fingerprint_for_cert(&self.intermediate_certificate())
+ }
+
+ pub fn root_fingerprint(&self) -> Fingerprint {
+ Self::fingerprint_for_cert(&self.root_certificate())
+ }
+
+ fn fingerprint_for_cert(cert: &X509) -> Fingerprint {
+ let digest = cert
+ .digest(openssl::hash::MessageDigest::sha256())
+ .expect("could not create certificate digest");
+ Fingerprint::try_from(&*digest).expect("could not get fingerprint from cert")
+ }
+}
+
+/// Used to spawn a test server that listens on a Unix socket that will be cleaned up once dropped.
+pub struct Server {
+ child: Child,
+ socket: TempPath,
+}
+
+impl Drop for Server {
+ fn drop(&mut self) {
+ let _ = self.child.kill();
+ }
+}
+
+impl Server {
+ fn new(cert_type: CertificateType) -> Server {
+ static TEST_NUMBER: AtomicU32 = AtomicU32::new(0);
+
+ let socket = TempPath::from_path(PathBuf::from(format!(
+ "/tmp/.op-cb-test-{}.sock",
+ TEST_NUMBER.fetch_add(1, std::sync::atomic::Ordering::SeqCst)
+ )));
+
+ let mut command = Command::new("openssl");
+
+ command
+ .arg("s_server")
+ .arg("-unix")
+ .arg(&socket)
+ .arg("-cert")
+ .arg(cert_type.certificate_path())
+ .arg("-key")
+ .arg(cert_type.key_path());
+
+ if let Some(cert_chain) = cert_type.certificate_chain_path() {
+ command
+ .arg("-cert_chain")
+ .arg(cert_chain)
+ .arg("-build_chain");
+ }
+
+ let child = command
+ .stdout(Stdio::piped())
+ .stderr(Stdio::piped())
+ .stdin(Stdio::piped())
+ .spawn()
+ .unwrap();
+
+ Server { child, socket }
+ }
+
+ pub fn connect(cert_type: CertificateType) -> (Server, UnixStream) {
+ let server = Server::new(cert_type);
+
+ // wait up to one second for the server to come up
+ for _i in 1..10 {
+ match UnixStream::connect(&server.socket) {
+ Ok(stream) => return (server, stream),
+ Err(_e) => std::thread::sleep(std::time::Duration::from_millis(100)),
+ }
+ }
+
+ panic!("server did not come up");
+ }
+}
+
+/// Carry out a TLS handshake with the default `SslContext` and the provided certificate type and
+/// fingerprint.
+///
+/// Returns the last result of the `openssl_verify_callback`.
+pub fn handshake(
+ case: CertificateType,
+ fingerprint: Option<Fingerprint>,
+) -> Option<Result<(), SslVerifyError>> {
+ handshake_with_builder(
+ case,
+ fingerprint,
+ SslContextBuilder::new(SslMethod::tls()).expect("could not get ssl context builder"),
+ )
+}
+
+/// Carry out a TLS handshake with the provided certificate added to the client's `SslContext` for
+/// the provided certificate type and fingerprint.
+///
+/// Returns the last result of the `openssl_verify_callback`.
+pub fn handshake_with_cert(
+ cert_type: CertificateType,
+ fingerprint: Option<Fingerprint>,
+ cert: X509,
+) -> Option<Result<(), SslVerifyError>> {
+ let mut store = X509StoreBuilder::new().expect("could not create x509 store builder");
+
+ store
+ .add_cert(cert)
+ .expect("could not add custom certificate to context");
+
+ let store = store.build();
+ let mut builder =
+ SslContextBuilder::new(SslMethod::tls()).expect("could not get ssl context builder");
+
+ builder.set_cert_store(store);
+ handshake_with_builder(cert_type, fingerprint, builder)
+}
+
+/// Carry out a TLS handshake with the provided `SslContextBuilder` used to build the
+/// `SslContext` for the provided certificate type and fingerprint.
+///
+/// Returns the last result of the `openssl_verify_callback`.
+fn handshake_with_builder(
+ case: CertificateType,
+ fingerprint: Option<Fingerprint>,
+ mut builder: SslContextBuilder,
+) -> Option<Result<(), SslVerifyError>> {
+ let last_result = Arc::new(Mutex::new(None));
+ let mv_result = last_result.clone();
+
+ builder.set_verify_callback(SslVerifyMode::PEER, move |p, x| {
+ let res = openssl_verify_callback(p, x, fingerprint.clone());
+ let ret = res.is_ok();
+ *mv_result.lock().unwrap() = Some(res);
+
+ ret
+ });
+
+ if super::SSL_OLD_VERIFY {
+ unsafe { std::env::set_var(PROXMOX_OLD_TLS_CHECK_VAR, "1") };
+ }
+
+ let (_server, stream) = Server::connect(case);
+ let ssl = Ssl::new(builder.build().as_ref()).unwrap();
+ // ignore the error from the connect below, we only care about the last result of the
+ // verify callback as that's what we are testing
+ let _res = ssl.connect(stream);
+
+ last_result.lock().unwrap().take()
+}
+
+/// Tests that should behave identical between the two versions of the callback.
+#[cfg(feature = "tls")]
+pub mod tests {
+ use std::str::FromStr;
+
+ use proxmox_http::SslVerifyError;
+
+ use super::*;
+
+ #[test]
+ fn self_signed_certificate_with_fingerprint_valid() {
+ let cert = CertificateType::SelfSigned;
+
+ assert_eq!(handshake(cert, Some(cert.leaf_fingerprint())), Some(Ok(())));
+ }
+
+ #[test]
+ fn self_signed_certificate_without_fingerprint_invalid() {
+ let cert = CertificateType::SelfSigned;
+
+ assert_eq!(
+ handshake(cert, None),
+ Some(Err(SslVerifyError::UntrustedCertificate {
+ fingerprint: cert.leaf_fingerprint(),
+ }))
+ );
+ }
+
+ #[test]
+ fn self_signed_certificate_with_incorrect_fingerprint_invalid() {
+ let cert = CertificateType::SelfSigned;
+ let expected: Fingerprint = Fingerprint::from_str("ba:dc:af:fe:e7:60:38:4e:f4:aa:bd:e2:a3:93:2b:99:af:46:34:96:ed:cf:e2:af:59:15:18:fc:ea:3b:fd:c5").expect("could not convert to fingerprint");
+
+ assert_eq!(
+ handshake(cert, Some(expected.clone())),
+ Some(Err(SslVerifyError::FingerprintMismatch {
+ fingerprint: cert.leaf_fingerprint(),
+ expected,
+ }))
+ );
+ }
+
+ #[test]
+ fn self_signed_certificate_with_cert_in_context_valid() {
+ let cert = CertificateType::SelfSigned;
+
+ assert_eq!(
+ handshake_with_cert(cert, None, cert.leaf_certificate()),
+ Some(Ok(()))
+ );
+ }
+
+ #[test]
+ fn self_signed_certificate_with_cert_in_context_and_correct_fingerprint_valid() {
+ let cert = CertificateType::SelfSigned;
+
+ assert_eq!(
+ handshake_with_cert(cert, Some(cert.leaf_fingerprint()), cert.leaf_certificate()),
+ Some(Ok(()))
+ );
+ }
+
+ #[test]
+ fn certificate_chain_with_correct_leaf_fingerprint_valid() {
+ let cert = CertificateType::CertificateChain;
+
+ assert_eq!(
+ handshake(cert, Some(cert.leaf_fingerprint()),),
+ Some(Ok(()))
+ );
+ }
+
+ #[test]
+ fn certificate_chain_with_correct_intermediate_fingerprint_invalid() {
+ let cert = CertificateType::CertificateChain;
+
+ assert_eq!(
+ handshake(cert, Some(cert.intermediate_fingerprint())),
+ Some(Err(SslVerifyError::FingerprintMismatch {
+ fingerprint: cert.leaf_fingerprint(),
+ expected: cert.intermediate_fingerprint()
+ }))
+ )
+ }
+
+ #[test]
+ fn certificate_chain_with_correct_root_fingerprint_invalid() {
+ let cert = CertificateType::CertificateChain;
+
+ assert_eq!(
+ handshake(cert, Some(cert.root_fingerprint())),
+ Some(Err(SslVerifyError::FingerprintMismatch {
+ fingerprint: cert.leaf_fingerprint(),
+ expected: cert.root_fingerprint()
+ }))
+ )
+ }
+
+ #[test]
+ fn certificate_chain_with_incorrect_fingerprint_invalid() {
+ let cert = CertificateType::CertificateChain;
+ let expected = Fingerprint::from_str("ba:dc:af:fe:e7:60:38:4e:f4:aa:bd:e2:a3:93:2b:99:af:46:34:96:ed:cf:e2:af:59:15:18:fc:ea:3b:fd:c5").expect("could not convert to fingerprint");
+
+ assert_eq!(
+ handshake(cert, Some(expected.clone())),
+ Some(Err(SslVerifyError::FingerprintMismatch {
+ fingerprint: cert.leaf_fingerprint(),
+ expected,
+ }))
+ );
+ }
+
+ #[test]
+ fn certificate_chain_with_root_cert_in_context_valid() {
+ let cert = CertificateType::CertificateChain;
+
+ assert_eq!(
+ handshake_with_cert(cert, None, cert.root_certificate()),
+ Some(Ok(()))
+ );
+ }
+
+ #[test]
+ /// This is considered invalid, because that is how OpenSSL would handle such a chain. If the
+ /// root is not trusted, it would abort the handshake right away, not caring whether a
+ /// certificate further down the chain would be valid.
+ ///
+ /// > The certificate chain is checked starting with the deepest nesting level (the root CA
+ /// > certificate) and worked upward to the peer's certificate.
+ /// > [..]
+ /// > If verify_callback returns 0, the verification process is immediately stopped with
+ /// > "verification failed" state. If SSL_VERIFY_PEER is set, a verification failure alert is
+ /// > sent to the peer and the TLS/SSL handshake is terminated.
+ /// >
+ /// > - https://docs.openssl.org/master/man3/SSL_CTX_set_verify/#notes
+ fn certificate_chain_with_intermediate_cert_in_context_invalid() {
+ let cert = CertificateType::CertificateChain;
+
+ assert_eq!(
+ handshake_with_cert(cert, None, cert.intermediate_certificate()),
+ Some(Err(SslVerifyError::UntrustedCertificate {
+ fingerprint: cert.leaf_fingerprint()
+ }))
+ );
+ }
+
+ #[test]
+ /// See description of [`certificate_chain_with_intermediate_cert_in_context_invalid`].
+ fn certificate_chain_with_leaf_cert_in_context_invalid() {
+ let cert = CertificateType::CertificateChain;
+
+ assert_eq!(
+ handshake_with_cert(cert, None, cert.leaf_certificate()),
+ Some(Err(SslVerifyError::UntrustedCertificate {
+ fingerprint: cert.leaf_fingerprint()
+ }))
+ );
+ }
+
+ #[test]
+ fn certificate_chain_with_root_cert_in_context_and_correct_leaf_fingerprint_valid() {
+ let cert = CertificateType::CertificateChain;
+
+ assert_eq!(
+ handshake_with_cert(cert, Some(cert.leaf_fingerprint()), cert.root_certificate()),
+ Some(Ok(()))
+ );
+ }
+}
diff --git a/proxmox-http/tests/openssl_verify_cb_new.rs b/proxmox-http/tests/openssl_verify_cb_new.rs
new file mode 100644
index 00000000..f286c9a6
--- /dev/null
+++ b/proxmox-http/tests/openssl_verify_cb_new.rs
@@ -0,0 +1,89 @@
+//! Integration tests for the `openssl_verify_callback` that should be used for all client TLS
+//! verification going forward. This differs to the legacy behavior in one way. If a fingerprint is
+//! provided and no certificate matches it, the connection is aborted, regardless of OpenSSL's
+//! validity checks.
+//!
+//! Other than that the tests currently encode the following behavior for both flows:
+//!
+//! * A self-signed certificate is trusted either when:
+//! 1. A matching fingerprint was provided.
+//! 2. No fingerprint was provided, but the OpenSSL context trusts it.
+//! * A certificate chain is trusted when:
+//! 1. A fingerprint is provided that matches the leaf certificate of the chain.
+//! 2. No fingerprint is provided, but the OpenSSL trust context declares the whole chain
+//! valid.
+//!
+#[cfg(feature = "tls")]
+mod common;
+
+// Make sure tests in the common module use the new verify callback flow.
+#[cfg(feature = "tls")]
+const SSL_OLD_VERIFY: bool = false;
+
+#[cfg(feature = "tls")]
+mod openssl_verify_cb_new {
+
+ use std::str::FromStr;
+
+ use proxmox_http::{Fingerprint, SslVerifyError};
+
+ use super::common::*;
+
+ #[test]
+ fn self_signed_certificate_with_cert_in_context_and_incorrect_fingerprint_invalid() {
+ let cert = CertificateType::SelfSigned;
+ let expected = Fingerprint::from_str("ba:dc:af:fe:e7:60:38:4e:f4:aa:bd:e2:a3:93:2b:99:af:46:34:96:ed:cf:e2:af:59:15:18:fc:ea:3b:fd:c5").expect("could not convert to fingerprint");
+
+ assert_eq!(
+ handshake_with_cert(cert, Some(expected.clone()), cert.leaf_certificate()),
+ Some(Err(SslVerifyError::FingerprintMismatch {
+ fingerprint: cert.leaf_fingerprint(),
+ expected,
+ }))
+ );
+ }
+
+ #[test]
+ fn certificate_chain_with_root_cert_in_context_and_incorrect_fingerprint_invalid() {
+ let cert = CertificateType::CertificateChain;
+ let expected = Fingerprint::from_str("ba:dc:af:fe:e7:60:38:4e:f4:aa:bd:e2:a3:93:2b:99:af:46:34:96:ed:cf:e2:af:59:15:18:fc:ea:3b:fd:c5").expect("could not convert to fingerprint");
+
+ assert_eq!(
+ handshake_with_cert(cert, Some(expected.clone()), cert.root_certificate()),
+ Some(Err(SslVerifyError::FingerprintMismatch {
+ fingerprint: cert.leaf_fingerprint(),
+ expected
+ }))
+ );
+ }
+
+ #[test]
+ fn certificate_chain_with_root_cert_in_context_and_correct_root_fingerprint_invalid() {
+ let cert = CertificateType::CertificateChain;
+
+ assert_eq!(
+ handshake_with_cert(cert, Some(cert.root_fingerprint()), cert.root_certificate()),
+ Some(Err(SslVerifyError::FingerprintMismatch {
+ fingerprint: cert.leaf_fingerprint(),
+ expected: cert.root_fingerprint(),
+ }))
+ );
+ }
+
+ #[test]
+ fn certificate_chain_with_root_cert_in_context_and_correct_intermediate_fingerprint_invalid() {
+ let cert = CertificateType::CertificateChain;
+
+ assert_eq!(
+ handshake_with_cert(
+ cert,
+ Some(cert.intermediate_fingerprint()),
+ cert.root_certificate()
+ ),
+ Some(Err(SslVerifyError::FingerprintMismatch {
+ fingerprint: cert.leaf_fingerprint(),
+ expected: cert.intermediate_fingerprint(),
+ }))
+ );
+ }
+}
diff --git a/proxmox-http/tests/openssl_verify_cb_old.rs b/proxmox-http/tests/openssl_verify_cb_old.rs
new file mode 100644
index 00000000..2e695093
--- /dev/null
+++ b/proxmox-http/tests/openssl_verify_cb_old.rs
@@ -0,0 +1,77 @@
+//! Integration tests for the `openssl_verify_callback` that check that the intended legacy
+//! behavior works correctly. The legacy behavior differs from the new intended behavior in one
+//! main way. If a fingerprint was provided and it does not match the leaf certificate provided by the
+//! server, but OpenSSL declares the certificate valid, the callback accepted it as well.
+//!
+//! Other than that the tests currently encode the following behavior for both flows:
+//!
+//! * A self-signed certificate is trusted either when:
+//! 1. A matching fingerprint was provided.
+//! 2. No fingerprint was provided, but the OpenSSL context trusts it.
+//! * A certificate chain is trusted when:
+//! 1. A fingerprint is provided that matches the leaf certificate of the chain.
+//! 2. No fingerprint is provided, but the OpenSSL trust context declares the whole chain
+//! valid.
+//!
+#[cfg(feature = "tls")]
+mod common;
+
+// Make sure tests in the common module use the old verify callback flow.
+#[cfg(feature = "tls")]
+const SSL_OLD_VERIFY: bool = true;
+
+#[cfg(feature = "tls")]
+pub mod openssl_verify_cb_old {
+
+ use std::str::FromStr;
+
+ use proxmox_http::Fingerprint;
+
+ use super::common::*;
+
+ #[test]
+ fn self_signed_certificate_with_cert_in_context_and_incorrect_fingerprint_valid() {
+ let cert = CertificateType::SelfSigned;
+ let expected = Fingerprint::from_str("ba:dc:af:fe:e7:60:38:4e:f4:aa:bd:e2:a3:93:2b:99:af:46:34:96:ed:cf:e2:af:59:15:18:fc:ea:3b:fd:c5").expect("could not convert to fingerprint");
+
+ assert_eq!(
+ handshake_with_cert(cert, Some(expected), cert.leaf_certificate()),
+ Some(Ok(()))
+ );
+ }
+
+ #[test]
+ fn certificate_chain_with_root_cert_in_context_and_incorrect_fingerprint_valid() {
+ let cert = CertificateType::CertificateChain;
+ let expected = Fingerprint::from_str("ba:dc:af:fe:e7:60:38:4e:f4:aa:bd:e2:a3:93:2b:99:af:46:34:96:ed:cf:e2:af:59:15:18:fc:ea:3b:fd:c5").expect("could not convert to fingerprint");
+
+ assert_eq!(
+ handshake_with_cert(cert, Some(expected), cert.root_certificate()),
+ Some(Ok(()))
+ );
+ }
+
+ #[test]
+ fn certificate_chain_with_root_cert_in_context_and_correct_root_fingerprint_valid() {
+ let cert = CertificateType::CertificateChain;
+
+ assert_eq!(
+ handshake_with_cert(cert, Some(cert.root_fingerprint()), cert.root_certificate()),
+ Some(Ok(()))
+ );
+ }
+
+ #[test]
+ fn certificate_chain_with_root_cert_in_context_and_correct_intermediate_fingerprint_valid() {
+ let cert = CertificateType::CertificateChain;
+
+ assert_eq!(
+ handshake_with_cert(
+ cert,
+ Some(cert.intermediate_fingerprint()),
+ cert.root_certificate()
+ ),
+ Some(Ok(()))
+ );
+ }
+}
--
2.47.3
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [PATCH proxmox v4 5/8] client: use proxmox-http's openssl verification callback
2026-07-01 10:30 [PATCH proxmox{,-backup,-websocket-tunnel} v4 0/8] unify openssl callback logic Dominik Csapak
` (3 preceding siblings ...)
2026-07-01 10:30 ` [PATCH proxmox v4 4/8] http: tls: add integration tests for openssl verify callbacks Dominik Csapak
@ 2026-07-01 10:30 ` Dominik Csapak
2026-07-01 10:30 ` [PATCH proxmox-backup v4 6/8] pbs-client: use proxmox-https openssl callback Dominik Csapak
` (3 subsequent siblings)
8 siblings, 0 replies; 13+ messages in thread
From: Dominik Csapak @ 2026-07-01 10:30 UTC (permalink / raw)
To: pve-devel, pbs-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 | 70 +++++++++++++-----------------------
2 files changed, 25 insertions(+), 47 deletions(-)
diff --git a/proxmox-client/Cargo.toml b/proxmox-client/Cargo.toml
index 6ca33420..5cbcecd8 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 }
hyper-util = { workspace = true, optional = true, features = [ "client-legacy" ] }
diff --git a/proxmox-client/src/client.rs b/proxmox-client/src/client.rs
index 26913dbb..b1622094 100644
--- a/proxmox-client/src/client.rs
+++ b/proxmox-client/src/client.rs
@@ -9,9 +9,9 @@ use http::request::Request;
use http::uri::PathAndQuery;
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::SslVerifyError;
use proxmox_login::Ticket;
use serde::Serialize;
@@ -109,11 +109,31 @@ impl Client {
TlsOptions::Verify => (),
TlsOptions::Insecure => connector.set_verify(SslVerifyMode::NONE),
TlsOptions::Fingerprint(expected_fingerprint) => {
+ let fp = proxmox_http::Fingerprint::try_from(&expected_fingerprint[..])
+ .map_err(|err| Error::internal("failed to parse fingerprint", err))?;
connector.set_verify_callback(SslVerifyMode::PEER, move |valid, chain| {
- if valid {
- return true;
+ match proxmox_http::openssl_verify_callback(valid, chain, Some(fp.clone())) {
+ Ok(()) => true,
+ Err(err) => {
+ match err {
+ SslVerifyError::FingerprintMismatch {
+ fingerprint,
+ expected,
+ } => {
+ log::error!("bad fingerprint: {fingerprint}");
+ log::error!("expected fingerprint: {expected}");
+ log::error!(
+ r#"If this fingerprint has worked before, it is possible that it changed on the remote
+side. This can happen, for example, if the remote rotates its certificate regularly.
+If you are sure no machine-in-the-middle attack (MitM) occurred, it is safe to set the
+above 'bad fingerprint' as the new fingerprint for the remote."#
+ );
+ }
+ _ => log::error!("{err}"),
+ }
+ false
+ }
}
- verify_fingerprint(chain, &expected_fingerprint)
});
}
TlsOptions::Callback(cb) => {
@@ -539,48 +559,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));
- log::error!(
- r#"If this fingerprint has worked before, it is possible that it changed on the remote
-side. This can happen, for example, if the remote rotates it's certificate regularly.
-If you are sure no machine-in-the-middle attack (MitM) occured, it is safe to set the
-above 'bad fingerpint' as the new fingerprint for the remote."#
- );
- 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
-}
-
/// Classify an HTTP client error into either a connection-establishment failure
/// ([`Error::Connect`]) or a post-connection error ([`Error::Client`]).
///
--
2.47.3
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [PATCH proxmox-backup v4 6/8] pbs-client: use proxmox-https openssl callback
2026-07-01 10:30 [PATCH proxmox{,-backup,-websocket-tunnel} v4 0/8] unify openssl callback logic Dominik Csapak
` (4 preceding siblings ...)
2026-07-01 10:30 ` [PATCH proxmox v4 5/8] client: use proxmox-http's openssl verification callback Dominik Csapak
@ 2026-07-01 10:30 ` Dominik Csapak
2026-07-01 13:36 ` Shannon Sterz
2026-07-01 10:30 ` [PATCH proxmox-backup v4 7/8] pbs-client: honor already verified fingerprint Dominik Csapak
` (2 subsequent siblings)
8 siblings, 1 reply; 13+ messages in thread
From: Dominik Csapak @ 2026-07-01 10:30 UTC (permalink / raw)
To: pve-devel, pbs-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 | 165 ++++++++++++++++------------------
2 files changed, 76 insertions(+), 91 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index a625370cf..9530e1ccb 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..27f5a1782 100644
--- a/pbs-client/src/http_client.rs
+++ b/pbs-client/src/http_client.rs
@@ -1,4 +1,5 @@
use std::io::{IsTerminal, Write};
+use std::str::FromStr;
use std::sync::{Arc, Mutex, RwLock};
use std::time::Duration;
@@ -15,10 +16,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;
@@ -28,10 +26,10 @@ use proxmox_sys::fs::{CreateOptions, file_get_json, replace_file};
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::{Body, Fingerprint};
+use proxmox_http::{ProxyConfig, SslVerifyError, openssl_verify_callback};
use proxmox_log::{error, info, warn};
use proxmox_rate_limiter::RateLimiter;
@@ -417,6 +415,14 @@ impl HttpClient {
expected_fingerprint = load_fingerprint(options.prefix.as_ref().unwrap(), server);
}
+ let expected_fingerprint = match expected_fingerprint {
+ Some(fp) => Some(
+ Fingerprint::from_str(&fp)
+ .map_err(|err| format_err!("could not parse fingerprint: {err}"))?,
+ ),
+ None => None,
+ };
+
let mut ssl_connector_builder = SslConnector::builder(SslMethod::tls()).unwrap();
if options.verify_cert {
@@ -425,30 +431,47 @@ 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.clone(),
) {
- 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(()) => {
+ if let Some(fp) = &expected_fingerprint {
+ *verified_fingerprint.lock().unwrap() = Some(fp.to_string());
}
- *verified_fingerprint.lock().unwrap() = Some(fingerprint);
true
- }
+ },
Err(err) => {
- error!("certificate validation failed - {}", 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}"),
+ }
+ }
+ },
+ }
false
}
},
@@ -661,79 +684,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: Fingerprint,
+ ) -> 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.to_string())
+ {
+ error!("{}", err);
+ }
}
+ *verified_fingerprint.lock().unwrap() = Some(fingerprint.to_string());
+ 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
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [PATCH proxmox-backup v4 7/8] pbs-client: honor already verified fingerprint
2026-07-01 10:30 [PATCH proxmox{,-backup,-websocket-tunnel} v4 0/8] unify openssl callback logic Dominik Csapak
` (5 preceding siblings ...)
2026-07-01 10:30 ` [PATCH proxmox-backup v4 6/8] pbs-client: use proxmox-https openssl callback Dominik Csapak
@ 2026-07-01 10:30 ` Dominik Csapak
2026-07-01 10:30 ` [PATCH proxmox-websocket-tunnel v4 8/8] use proxmox-http's openssl callback Dominik Csapak
2026-07-01 13:35 ` [PATCH proxmox{,-backup,-websocket-tunnel} v4 0/8] unify openssl callback logic Shannon Sterz
8 siblings, 0 replies; 13+ messages in thread
From: Dominik Csapak @ 2026-07-01 10:30 UTC (permalink / raw)
To: pve-devel, pbs-devel
When there is an invalid fingerprint or a mismatch and the client is
executed in an interactive way, the user is asked if he wants to trust
the fingerprint.
OpenSSL might evaluate the verify callback multiple times per
certificate (for different errors for example), so we have to check
if the fingerprint matches the one we already verified.
Otherwise the user gets multiple prompts directly after another.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
pbs-client/src/http_client.rs | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/pbs-client/src/http_client.rs b/pbs-client/src/http_client.rs
index 27f5a1782..f953a1e58 100644
--- a/pbs-client/src/http_client.rs
+++ b/pbs-client/src/http_client.rs
@@ -431,6 +431,18 @@ impl HttpClient {
let interactive = options.interactive;
let fingerprint_cache = options.fingerprint_cache;
let prefix = options.prefix.clone();
+ let check_fp_verified = |verified_fingerprint: &Arc<Mutex<Option<String>>>,
+ fingerprint: &Fingerprint|
+ -> bool {
+ let verified = verified_fingerprint.lock().unwrap();
+ if let Some(verified) = &*verified {
+ if *verified == fingerprint.to_string() {
+ // already verified
+ return true;
+ }
+ }
+ false
+ };
ssl_connector_builder.set_verify_callback(
openssl::ssl::SslVerifyMode::PEER,
move |valid, ctx| match openssl_verify_callback(
@@ -453,6 +465,9 @@ impl HttpClient {
error!("certificate validation failed - failed to calculate FP - {error_stack}")
},
SslVerifyError::UntrustedCertificate { fingerprint } => {
+ if check_fp_verified(&verified_fingerprint, &fingerprint) {
+ return true;
+ }
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,
@@ -461,6 +476,9 @@ impl HttpClient {
}
}
SslVerifyError::FingerprintMismatch { fingerprint, expected } => {
+ if check_fp_verified(&verified_fingerprint, &fingerprint) {
+ return true;
+ }
warn!("WARNING: certificate fingerprint does not match expected fingerprint!");
warn!("expected: {expected}");
--
2.47.3
^ permalink raw reply related [flat|nested] 13+ messages in thread
* [PATCH proxmox-websocket-tunnel v4 8/8] use proxmox-http's openssl callback
2026-07-01 10:30 [PATCH proxmox{,-backup,-websocket-tunnel} v4 0/8] unify openssl callback logic Dominik Csapak
` (6 preceding siblings ...)
2026-07-01 10:30 ` [PATCH proxmox-backup v4 7/8] pbs-client: honor already verified fingerprint Dominik Csapak
@ 2026-07-01 10:30 ` Dominik Csapak
2026-07-01 13:35 ` Shannon Sterz
2026-07-01 13:35 ` [PATCH proxmox{,-backup,-websocket-tunnel} v4 0/8] unify openssl callback logic Shannon Sterz
8 siblings, 1 reply; 13+ messages in thread
From: Dominik Csapak @ 2026-07-01 10:30 UTC (permalink / raw)
To: pve-devel, pbs-devel
no functional change intended, since the callback there should implement
the same behavior.
With this change, we can now drop the hex and itertools dependency.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
Cargo.toml | 4 +--
src/main.rs | 76 +++++++++++++++++++++++++----------------------------
2 files changed, 37 insertions(+), 43 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index 02ac3d1..7b5ced6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,11 +13,9 @@ anyhow = "1.0"
proxmox-base64 = "1"
futures = "0.3"
futures-util = "0.3"
-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 +24,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..b79528e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,7 @@
use anyhow::{bail, format_err, Error};
use std::collections::VecDeque;
+use std::str::FromStr;
use std::sync::Arc;
use futures::future::FutureExt;
@@ -25,7 +26,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, Fingerprint, SslVerifyError};
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
@@ -141,49 +142,44 @@ impl CtrlTunnel {
headers.insert(name, value);
}
+ let fingerprint = match fingerprint {
+ Some(fp) => Some(
+ Fingerprint::from_str(&fp)
+ .map_err(|err| format_err!("could not parse fingerprint: {err}"))?,
+ ),
+ None => None,
+ };
+
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.clone(),
+ ) {
+ 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.47.3
^ permalink raw reply related [flat|nested] 13+ messages in thread
* Re: [PATCH proxmox{,-backup,-websocket-tunnel} v4 0/8] unify openssl callback logic
2026-07-01 10:30 [PATCH proxmox{,-backup,-websocket-tunnel} v4 0/8] unify openssl callback logic Dominik Csapak
` (7 preceding siblings ...)
2026-07-01 10:30 ` [PATCH proxmox-websocket-tunnel v4 8/8] use proxmox-http's openssl callback Dominik Csapak
@ 2026-07-01 13:35 ` Shannon Sterz
8 siblings, 0 replies; 13+ messages in thread
From: Shannon Sterz @ 2026-07-01 13:35 UTC (permalink / raw)
To: Dominik Csapak, pve-devel, pbs-devel
On Wed Jul 1, 2026 at 12:30 PM CEST, Dominik Csapak wrote:
> 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
> * PDM client has also a different implementation (not touched here)
>
> 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.
>
> There is a patch int the proxmox-http crate is to preserve backwards
> compatibility with the current pbs client behavior, but is opt-in via
> environment variable (which we might want to enable automatically for the
> pbs-client? though this is difficult to do, since the client can and will
> be called from scripts or manually)
>
> 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)
>
> Tests were implemented by Shannon (thanks!) but I refined it's behavior
> (see the commit)
>
> This series partially overlaps/interferes with shannons recent series:
> https://lore.proxmox.com/pdm-devel/20260611120327.257523-1-s.sterz@proxmox.com/
thanks again, beside the tiny nits i left, consider this:
Reviewed-by: Shannon Sterz <s.sterz@proxmox.com>
as for the pbs-client, we should at least set an environment variable
for pveproxy/pvedaemon which invoke the client from what i can tell. we
should be able to set these via the systemd units. users can then add
systemd overrides to opt out again and we can drop them from the units
on the next major version (or whenever we official drop the legacy
behaviour).
for users that use pbs-client directly, im not sure what we can do other
than highlighting this change in the changelog.
^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [PATCH proxmox-websocket-tunnel v4 8/8] use proxmox-http's openssl callback
2026-07-01 10:30 ` [PATCH proxmox-websocket-tunnel v4 8/8] use proxmox-http's openssl callback Dominik Csapak
@ 2026-07-01 13:35 ` Shannon Sterz
0 siblings, 0 replies; 13+ messages in thread
From: Shannon Sterz @ 2026-07-01 13:35 UTC (permalink / raw)
To: Dominik Csapak, pve-devel, pbs-devel
On Wed Jul 1, 2026 at 12:30 PM CEST, Dominik Csapak wrote:
> no functional change intended, since the callback there should implement
> the same behavior.
>
> With this change, we can now drop the hex and itertools dependency.
>
> Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
> ---
> Cargo.toml | 4 +--
> src/main.rs | 76 +++++++++++++++++++++++++----------------------------
> 2 files changed, 37 insertions(+), 43 deletions(-)
>
> diff --git a/Cargo.toml b/Cargo.toml
> index 02ac3d1..7b5ced6 100644
> --- a/Cargo.toml
> +++ b/Cargo.toml
> @@ -13,11 +13,9 @@ anyhow = "1.0"
> proxmox-base64 = "1"
> futures = "0.3"
> futures-util = "0.3"
> -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 +24,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..b79528e 100644
> --- a/src/main.rs
> +++ b/src/main.rs
> @@ -1,6 +1,7 @@
> use anyhow::{bail, format_err, Error};
>
> use std::collections::VecDeque;
> +use std::str::FromStr;
> use std::sync::Arc;
>
> use futures::future::FutureExt;
> @@ -25,7 +26,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, Fingerprint, SslVerifyError};
>
> #[derive(Serialize, Deserialize, Debug)]
> #[serde(rename_all = "kebab-case")]
> @@ -141,49 +142,44 @@ impl CtrlTunnel {
-->8 snip 8<--
> headers.insert(name, value);
> + move |valid, ctx| match proxmox_http::openssl_verify_callback(
> + valid,
> + ctx,
> + fingerprint.clone(),
> + ) {
> + Ok(()) => true,
> + Err(err) => {
> + match err {
> + SslVerifyError::NoCertificate => {
> + eprintln!("SSL context lacks current certificate");
> + }
similar to the nit in 6/8. though this is imo already better, but i'd
personally would like to avoid the word "context".
-->8 snip 8<--
^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [PATCH proxmox-backup v4 6/8] pbs-client: use proxmox-https openssl callback
2026-07-01 10:30 ` [PATCH proxmox-backup v4 6/8] pbs-client: use proxmox-https openssl callback Dominik Csapak
@ 2026-07-01 13:36 ` Shannon Sterz
0 siblings, 0 replies; 13+ messages in thread
From: Shannon Sterz @ 2026-07-01 13:36 UTC (permalink / raw)
To: Dominik Csapak, pve-devel, pbs-devel
On Wed Jul 1, 2026 at 12:30 PM CEST, Dominik Csapak wrote:
> 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 | 165 ++++++++++++++++------------------
> 2 files changed, 76 insertions(+), 91 deletions(-)
>
> diff --git a/Cargo.toml b/Cargo.toml
> index a625370cf..9530e1ccb 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
-->8 snip 8<--
> @@ -425,30 +431,47 @@ 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.clone(),
> ) {
> - 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(()) => {
> + if let Some(fp) = &expected_fingerprint {
> + *verified_fingerprint.lock().unwrap() = Some(fp.to_string());
> }
> - *verified_fingerprint.lock().unwrap() = Some(fingerprint);
> true
> - }
> + },
> Err(err) => {
> - error!("certificate validation failed - {}", err);
> + match err {
> + SslVerifyError::NoCertificate => error!(
> + "certificate validation failed - context lacks current certificate"
> + ),
nit: i still think this could be misinterpreted and lead users to add
certificates to the trust store. i think this could be made clearer with
something like "certificate validation failed - could not get a
certificate necessary for verifiction". though, this error case isn't
likely to happen in real ussage anyway from what i can tell.
-->8 snip 8<--
^ permalink raw reply [flat|nested] 13+ messages in thread
* Re: [PATCH proxmox v4 2/8] http: tls: use legacy behavior when PROXMOX_OLD_TLS_CHECK is set to "1"
2026-07-01 10:30 ` [PATCH proxmox v4 2/8] http: tls: use legacy behavior when PROXMOX_OLD_TLS_CHECK is set to "1" Dominik Csapak
@ 2026-07-01 13:36 ` Shannon Sterz
0 siblings, 0 replies; 13+ messages in thread
From: Shannon Sterz @ 2026-07-01 13:36 UTC (permalink / raw)
To: Dominik Csapak, pve-devel, pbs-devel
On Wed Jul 1, 2026 at 12:30 PM CEST, Dominik Csapak wrote:
> If that environment variable is set to "1", give the openssl result
> priority, and potentially ignore a given fingerprint that is not
> matching. If that's the case, print a warning.
nit: the "print a warning" bit was moved to the next commit, so im
guessing the commit message should be cleaned up here :)
> Co-developed-by: Shannon Sterz <s.sterz@proxmox.com>
> Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
> ---
> proxmox-http/src/tls.rs | 15 +++++++++++++--
> 1 file changed, 13 insertions(+), 2 deletions(-)
>
> diff --git a/proxmox-http/src/tls.rs b/proxmox-http/src/tls.rs
> index 5c2b1743..abdf51e9 100644
> --- a/proxmox-http/src/tls.rs
> +++ b/proxmox-http/src/tls.rs
> @@ -103,6 +103,8 @@ impl std::str::FromStr for Fingerprint {
> }
> }
>
> +pub const PROXMOX_OLD_TLS_CHECK_VAR: &str = "PROXMOX_OLD_TLS_CHECK";
> +
> ///
> /// Error type returned by failed [`openssl_verify_callback`].
> ///
> @@ -159,15 +161,24 @@ impl PartialEq for SslVerifyError {
>
> /// Intended as an openssl verification callback.
> ///
> -/// The following things are checked:
> +/// If the 'PROXMOX_OLD_TLS_CHECK' environment variable is not set to "1",
> +/// the following things are checked:
> ///
> /// * If no fingerprint is given, return the openssl verification result
> -/// * If a fingerprint is given get the leaf fp and check that against the given
> +/// * If a fingerprint is given, check it against the leaf fp
> +///
> +/// Otherwise, we trust the openssl result if the whole chain was trusted
> pub fn openssl_verify_callback(
> openssl_valid: bool,
> ctx: &mut X509StoreContextRef,
> expected_fp: Option<Fingerprint>,
> ) -> Result<(), SslVerifyError> {
> + let old_check = matches!(std::env::var(PROXMOX_OLD_TLS_CHECK_VAR).as_deref(), Ok("1"));
> +
> + if old_check && openssl_valid {
> + return Ok(());
> + }
> +
> match expected_fp {
> Some(expected_fp) => {
> let fingerprint = get_leaf_fp(ctx)?;
^ permalink raw reply [flat|nested] 13+ messages in thread
end of thread, other threads:[~2026-07-01 13:36 UTC | newest]
Thread overview: 13+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-01 10:30 [PATCH proxmox{,-backup,-websocket-tunnel} v4 0/8] unify openssl callback logic Dominik Csapak
2026-07-01 10:30 ` [PATCH proxmox v4 1/8] http: factor out openssl verification callback Dominik Csapak
2026-07-01 10:30 ` [PATCH proxmox v4 2/8] http: tls: use legacy behavior when PROXMOX_OLD_TLS_CHECK is set to "1" Dominik Csapak
2026-07-01 13:36 ` Shannon Sterz
2026-07-01 10:30 ` [PATCH proxmox v4 3/8] http: tls: add warning if old check behavior is enabled and triggered Dominik Csapak
2026-07-01 10:30 ` [PATCH proxmox v4 4/8] http: tls: add integration tests for openssl verify callbacks Dominik Csapak
2026-07-01 10:30 ` [PATCH proxmox v4 5/8] client: use proxmox-http's openssl verification callback Dominik Csapak
2026-07-01 10:30 ` [PATCH proxmox-backup v4 6/8] pbs-client: use proxmox-https openssl callback Dominik Csapak
2026-07-01 13:36 ` Shannon Sterz
2026-07-01 10:30 ` [PATCH proxmox-backup v4 7/8] pbs-client: honor already verified fingerprint Dominik Csapak
2026-07-01 10:30 ` [PATCH proxmox-websocket-tunnel v4 8/8] use proxmox-http's openssl callback Dominik Csapak
2026-07-01 13:35 ` Shannon Sterz
2026-07-01 13:35 ` [PATCH proxmox{,-backup,-websocket-tunnel} v4 0/8] unify openssl callback logic Shannon Sterz
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox