From: Dominik Csapak <d.csapak@proxmox.com>
To: pve-devel@lists.proxmox.com, pbs-devel@lists.proxmox.com
Subject: [PATCH proxmox v3 1/6] http: factor out openssl verification callback
Date: Wed, 17 Jun 2026 10:59:13 +0200 [thread overview]
Message-ID: <20260617085949.1528300-2-d.csapak@proxmox.com> (raw)
In-Reply-To: <20260617085949.1528300-1-d.csapak@proxmox.com>
with the 'tls' feature offers a callback method that can be used within
openssl's `set_verify_callback` with a given expected fingerprint.
The logic is inspired by our perl and proxmox-websocket-tunnel
verification logic:
Use openssl's verification if no fingerprint is pinned. If a fingerprint
is given, ignore openssl's verification and check if the leafs
certificate is a match.
This introduces a custom error type for this, since we need to handle
errors differently for different users, e.g. pbs-client wants to be able
to use a fingerprint cache and let the user accept it in interactive cli
sessions. For this we want the 'thiserror' crate, so move it to the
workspace Cargo.toml and depend from there. (also change this for
proxmox-openid)
One thing to note here is that the APPLICATION_VERIFICATION error of
openssl is used to mark the case where an untrusted root or intermediate
certificate is trusted from the callback. When that happens, openssl
might return true for the following certificates (if nothing else is
wrong aside from a missing trust anchor), so the error is checked for
this special value to determine if the openssl validation can be
trusted.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
proxmox-http/Cargo.toml | 5 +++
proxmox-http/src/lib.rs | 5 +++
proxmox-http/src/tls.rs | 90 +++++++++++++++++++++++++++++++++++++++++
3 files changed, 100 insertions(+)
create mode 100644 proxmox-http/src/tls.rs
diff --git a/proxmox-http/Cargo.toml b/proxmox-http/Cargo.toml
index 66b11650..e4bff930 100644
--- a/proxmox-http/Cargo.toml
+++ b/proxmox-http/Cargo.toml
@@ -24,6 +24,7 @@ native-tls = { workspace = true, optional = true }
openssl = { version = "0.10", optional = true }
serde_json = { workspace = true, optional = true }
sync_wrapper = { workspace = true, optional = true }
+thiserror = { workspace = true, optional = true }
tokio = { workspace = true, features = [], optional = true }
tokio-openssl = { workspace = true, optional = true }
tower-service = { workspace = true, optional = true }
@@ -105,3 +106,7 @@ websocket = [
"tokio?/sync",
"body",
]
+tls = [
+ "dep:openssl",
+ "dep:thiserror",
+]
diff --git a/proxmox-http/src/lib.rs b/proxmox-http/src/lib.rs
index 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..7365230e
--- /dev/null
+++ b/proxmox-http/src/tls.rs
@@ -0,0 +1,90 @@
+use openssl::x509::{X509StoreContextRef, X509VerifyResult};
+
+///
+/// Error type returned by failed [`openssl_verify_callback`].
+///
+#[derive(Debug, thiserror::Error)]
+pub enum SslVerifyError {
+ /// Occurs if no certificate is found in the current part of the chain. Should never happen!
+ #[error("SSL context lacks current certificate")]
+ NoCertificate,
+
+ /// Cannot calculate fingerprint from connection
+ #[error("failed to calculate fingerprint - {0}")]
+ InvalidFingerprint(openssl::error::ErrorStack),
+
+ /// Fingerprint match error
+ #[error("found fingerprint ({fingerprint}) does not match expected fingerprint ({expected})")]
+ FingerprintMismatch {
+ fingerprint: String,
+ expected: String,
+ },
+
+ /// Untrusted certificate with fingerprint information
+ #[error("certificate validation failed")]
+ UntrustedCertificate { fingerprint: String },
+}
+
+/// Intended as an openssl verification callback.
+///
+/// The following things are checked:
+///
+/// * If no fingerprint is given, return the openssl verification result
+/// * If a fingerprint is given, do:
+/// * Ignore all non-leaf certificates/
+pub fn openssl_verify_callback(
+ openssl_valid: bool,
+ ctx: &mut X509StoreContextRef,
+ expected_fp: Option<&str>,
+) -> Result<(), SslVerifyError> {
+ let trust_openssl = ctx.error() != X509VerifyResult::APPLICATION_VERIFICATION;
+ if expected_fp.is_none() && openssl_valid && trust_openssl {
+ return Ok(());
+ }
+
+ let cert = match ctx.current_cert() {
+ Some(cert) => cert,
+ None => {
+ return Err(SslVerifyError::NoCertificate);
+ }
+ };
+
+ if ctx.error_depth() > 0 {
+ // openssl was not valid, but we want to continue, so save that we don't trust openssl
+ ctx.set_error(X509VerifyResult::APPLICATION_VERIFICATION);
+ return Ok(());
+ }
+
+ let digest = cert
+ .digest(openssl::hash::MessageDigest::sha256())
+ .map_err(SslVerifyError::InvalidFingerprint)?;
+ let fingerprint = get_fingerprint_from_u8(&digest);
+
+ if let Some(expected_fp) = expected_fp {
+ if expected_fp.to_lowercase() == fingerprint.to_lowercase() {
+ ctx.set_error(X509VerifyResult::OK);
+ Ok(())
+ } else {
+ Err(SslVerifyError::FingerprintMismatch {
+ fingerprint,
+ expected: expected_fp.to_string(),
+ })
+ }
+ } else {
+ Err(SslVerifyError::UntrustedCertificate { fingerprint })
+ }
+}
+
+/// Returns the fingerprint from a byte slice ([`&[u8]`]) in the form `00:11:22:...`
+pub fn get_fingerprint_from_u8(fp: &[u8]) -> String {
+ use std::fmt::Write as _;
+
+ let mut target = String::with_capacity(fp.len() * 3); // 2 chars per byte + ~1 separator
+ for byte in fp {
+ if !target.is_empty() {
+ target.push(':');
+ }
+ let _ = write!(target, "{byte:02x}");
+ }
+ target
+}
--
2.47.3
next prev parent reply other threads:[~2026-06-17 9:00 UTC|newest]
Thread overview: 7+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-17 8:59 [PATCH proxmox{,-backup,-websocket-tunnel} v3 0/6] unify openssl callback logic Dominik Csapak
2026-06-17 8:59 ` Dominik Csapak [this message]
2026-06-17 8:59 ` [PATCH proxmox v3 2/6] http: tls: use legacy behavior when PROXMOX_NEW_TLS_CHECK is not set Dominik Csapak
2026-06-17 8:59 ` [PATCH proxmox v3 3/6] client: use proxmox-http's openssl verification callback Dominik Csapak
2026-06-17 8:59 ` [PATCH proxmox-backup v3 4/6] pbs-client: use proxmox-https openssl callback Dominik Csapak
2026-06-17 8:59 ` [PATCH proxmox-backup v3 5/6] pbs-client: honor already verified fingerprint Dominik Csapak
2026-06-17 8:59 ` [PATCH proxmox-websocket-tunnel v3 6/6] use proxmox-http's openssl callback Dominik Csapak
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260617085949.1528300-2-d.csapak@proxmox.com \
--to=d.csapak@proxmox.com \
--cc=pbs-devel@lists.proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox