[pbs-devel] [PATCH proxmox 1/2] http: factor out openssl verification callback

Dominik Csapak d.csapak at proxmox.com
Thu Jul 24 09:31:44 CEST 2025



On 7/23/25 21:26, Thomas Lamprecht wrote:
> Am 21.05.25 um 10:45 schrieb Dominik Csapak:
>> with the 'tls' feature offers a callback method that can be used within
>> openssl's `set_verify_callback` with a given expected fingerprint.
>>
>> The logic is inspired by our perl and proxmox-websocket-tunnel
>> verification logic:
>>
>> Use openssl's verification if no fingerprint is pinned. If a fingerprint
>> is given, ignore openssl's verification and check if the leafs
>> certificate is a match.
>>
>> This introduces a custom error type for this, since we need to handle
>> errors differently for different users, e.g. pbs-client wants to be able
>> to use a fingerprint cache and let the user accept it in interactive cli
>> sessions. For this we want the 'thiserror' crate, so move it to the
>> workspace Cargo.toml and depend from there. (also change this for
>> proxmox-openid)
>>
>> One thing to note here is that  the APPLICATION_VERIFICATION error of
>> openssl is used to mark the case where an untrusted root or intermediate
>> certificate is trusted from the callback. When that happens, openssl
>> might return true for the following certificates (if nothing else is
>> wrong aside from a missing trust anchor), so the error is checked for
>> this special value to determine if the openssl validation can be
>> trusted.
> 
> needs rebasing (sorry for the delay), and some comments inline
> 

will send a v2

>>
>> Signed-off-by: Dominik Csapak <d.csapak at proxmox.com>
>> ---
>>   Cargo.toml                |  1 +
>>   proxmox-http/Cargo.toml   |  7 +++
>>   proxmox-http/src/lib.rs   |  5 +++
>>   proxmox-http/src/tls.rs   | 89 +++++++++++++++++++++++++++++++++++++++
>>   proxmox-openid/Cargo.toml |  2 +-
>>   5 files changed, 103 insertions(+), 1 deletion(-)
>>   create mode 100644 proxmox-http/src/tls.rs
>>
>> diff --git a/Cargo.toml b/Cargo.toml
>> index 95ade7d7..8aecc6ba 100644
>> --- a/Cargo.toml
>> +++ b/Cargo.toml
>> @@ -107,6 +107,7 @@ serde_json = "1.0"
>>   serde_plain = "1.0"
>>   syn = { version = "2", features = [ "full", "visit-mut" ] }
>>   tar = "0.4"
>> +thiserror = "1"
>>   tokio = "1.6"
>>   tokio-openssl = "0.6.1"
>>   tokio-stream = "0.1.0"
>> diff --git a/proxmox-http/Cargo.toml b/proxmox-http/Cargo.toml
>> index afa5981d..fb2eb26d 100644
>> --- a/proxmox-http/Cargo.toml
>> +++ b/proxmox-http/Cargo.toml
>> @@ -15,11 +15,13 @@ rust-version.workspace = true
>>   anyhow.workspace = true
>>   base64 = { workspace = true, optional = true }
>>   futures = { workspace = true, optional = true }
>> +hex = { workspace = true, optional = true }
> 
> is not be required, see below.
> 
>>   http = { workspace = true, optional = true }
>>   hyper = { workspace = true, optional = true }
>>   native-tls = { workspace = true, optional = true }
>>   openssl =  { version = "0.10", optional = true }
>>   serde_json = { workspace = true, optional = true }
>> +thiserror = { workspace = true, optional = true }
>>   tokio = { workspace = true, features = [], optional = true }
>>   tokio-openssl = { workspace = true, optional = true }
>>   tower-service.workspace = true
>> @@ -79,3 +81,8 @@ websocket = [
>>       "tokio?/io-util",
>>       "tokio?/sync",
>>   ]
>> +tls = [
>> +    "dep:hex",
>> +    "dep:openssl",
>> +    "dep:thiserror",
>> +]
>> diff --git a/proxmox-http/src/lib.rs b/proxmox-http/src/lib.rs
>> index 4770aaf4..a676acf9 100644
>> --- a/proxmox-http/src/lib.rs
>> +++ b/proxmox-http/src/lib.rs
>> @@ -35,3 +35,8 @@ pub use rate_limiter::{RateLimit, RateLimiter, RateLimiterVec, ShareableRateLimi
>>   mod rate_limited_stream;
>>   #[cfg(feature = "rate-limited-stream")]
>>   pub use rate_limited_stream::RateLimitedStream;
>> +
>> +#[cfg(feature = "tls")]
>> +mod tls;
>> +#[cfg(feature = "tls")]
>> +pub use tls::*;
>> diff --git a/proxmox-http/src/tls.rs b/proxmox-http/src/tls.rs
>> new file mode 100644
>> index 00000000..6da03cd9
>> --- /dev/null
>> +++ b/proxmox-http/src/tls.rs
>> @@ -0,0 +1,89 @@
>> +use openssl::x509::{X509StoreContextRef, X509VerifyResult};
>> +
>> +///
>> +/// Error type returned by failed [`openssl_verify_callback`].
>> +///
>> +#[derive(Debug, thiserror::Error)]
>> +pub enum SslVerifyError {
>> +    /// Occurs if no certificate is found in the current part of the chain. Should never happen!
>> +    #[error("SSL context lacks current certificate")]
>> +    NoCertificate,
>> +
>> +    /// Cannot calculate fingerprint from connection
>> +    #[error("failed to calculate fingerprint - {0}")]
>> +    InvalidFingerprint(openssl::error::ErrorStack),
>> +
>> +    /// Fingerprint match error
>> +    #[error("found fingerprint ({fingerprint}) does not match expected fingerprint ({expected})")]
>> +    FingerprintMismatch {
>> +        fingerprint: String,
>> +        expected: String,
>> +    },
>> +
>> +    /// Untrusted certificate with fingerprint information
>> +    #[error("certificate validation failed")]
>> +    UntrustedCertificate { fingerprint: String },
>> +}
>> +
>> +/// Intended as an openssl verification callback.
>> +///
>> +/// The following things are checked:
>> +///
>> +/// * If no fingerprint is given, return the openssl verification result
>> +/// * If a fingerprint is given, do:
>> +///     * Ignore all non-leaf certificates/
>> +pub fn openssl_verify_callback(
>> +    openssl_valid: bool,
>> +    ctx: &mut X509StoreContextRef,
> 
> hmm, not sure how simple it is to construct/derive above type manually,
> but would be great to get this method some actual tests, potentially
> with some temporary certs created on demand (or the snake oil one).
> But that doesn't need to block this series, it's already an improvement
> to have just one of these around in any case.

i tried really hard to create in-rust tests by using the openssl
interfaces, but i wasn't able to massage them to actually test the
callback.

imho the only way i see how we could test is to write a short
server and start that and connect with socat/openssl/... directly
during the tests... but i'm not sure if that is really practical to do
in rust tests...

> 
>> +    expected_fp: Option<&str>,
>> +) -> Result<(), SslVerifyError> {
>> +    let trust_openssl = ctx.error() != X509VerifyResult::APPLICATION_VERIFICATION;
>> +    if expected_fp.is_none() && openssl_valid && trust_openssl {
>> +        return Ok(());
>> +    }
>> +
>> +    let cert = match ctx.current_cert() {
>> +        Some(cert) => cert,
>> +        None => {
>> +            return Err(SslVerifyError::NoCertificate);
>> +        }
>> +    };
>> +
>> +    if ctx.error_depth() > 0 {
>> +        // openssl was not valid, but we want to continue, so save that we don't trust openssl
>> +        ctx.set_error(X509VerifyResult::APPLICATION_VERIFICATION);
>> +        return Ok(());
>> +    }
>> +
>> +    let digest = cert
>> +        .digest(openssl::hash::MessageDigest::sha256())
>> +        .map_err(SslVerifyError::InvalidFingerprint)?;
>> +    let fingerprint = get_fingerprint_from_u8(&digest);
>> +
>> +    if let Some(expected_fp) = expected_fp {
>> +        if expected_fp.to_lowercase() == fingerprint.to_lowercase() {
>> +            ctx.set_error(X509VerifyResult::OK);
>> +            Ok(())
>> +        } else {
>> +            Err(SslVerifyError::FingerprintMismatch {
>> +                fingerprint,
>> +                expected: expected_fp.to_string(),
>> +            })
>> +        }
>> +    } else {
>> +        Err(SslVerifyError::UntrustedCertificate { fingerprint })
>> +    }
>> +}
>> +
>> +/// Returns the fingerprint from a byte slice ([`&[u8]`]) in the form `00:11:22:...`
>> +pub fn get_fingerprint_from_u8(fp: &[u8]) -> String {
>> +    let fp_string = hex::encode(fp);
>> +    let fp_string = fp_string
>> +        .as_bytes()
>> +        .chunks(2)
>> +        .map(|v| unsafe { std::str::from_utf8_unchecked(v) })
>> +        .collect::<Vec<_>>()
>> +        .join(":");
>> +
>> +    fp_string
> 
> As mentioned recently in the review for the s3-client [0], this can be done
> simpler:
> 
> fp
>      .iter()
>      .map(|byte| format!("{byte:02x}"))
>      .collect::<Vec<String>>()
>      .join(":")
> 
> 
> [0]: https://lore.proxmox.com/all/38e3b3b5-a1a6-43d6-b925-1d04d8b1e22d@proxmox.com/

true


> 
>> +}
>> diff --git a/proxmox-openid/Cargo.toml b/proxmox-openid/Cargo.toml
>> index a5249214..4223d10c 100644
>> --- a/proxmox-openid/Cargo.toml
>> +++ b/proxmox-openid/Cargo.toml
>> @@ -18,7 +18,7 @@ http.workspace = true
>>   nix.workspace = true
>>   serde = { workspace = true, features = ["derive"] }
>>   serde_json.workspace = true
>> -thiserror = "1"
>> +thiserror.workspace = true
> 
> a bit unrelated to the rest, but can be OK to do in a patch like this I guess.

shall i send a seperate patch upfront that moves the thiserror crate
dependency to the workspace cargo.toml ?

> 
>>   native-tls.workspace = true
>>   
>>   openidconnect = { version = "2.4", default-features = false, features = ["accept-rfc3339-timestamps"] }
> 





More information about the pbs-devel mailing list