public inbox for pbs-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Dominik Csapak <d.csapak@proxmox.com>
To: Thomas Lamprecht <t.lamprecht@proxmox.com>,
	Proxmox Backup Server development discussion
	<pbs-devel@lists.proxmox.com>
Subject: Re: [pbs-devel] [PATCH proxmox 1/2] http: factor out openssl verification callback
Date: Thu, 24 Jul 2025 09:31:44 +0200	[thread overview]
Message-ID: <3d720c4a-5fdc-406d-9923-aa990a938c61@proxmox.com> (raw)
In-Reply-To: <b71ff85f-1300-459e-85e5-6034fbf42088@proxmox.com>



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

will send a v2

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

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

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

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

true


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

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

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



_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel


  reply	other threads:[~2025-07-24  7:30 UTC|newest]

Thread overview: 10+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-05-21  8:45 [pbs-devel] [PATCH proxmox{, -websocket-tunnel, -backup} 0/5] unify openssl callback logic Dominik Csapak
2025-05-21  8:45 ` [pbs-devel] [PATCH proxmox 1/2] http: factor out openssl verification callback Dominik Csapak
2025-07-23 19:26   ` Thomas Lamprecht
2025-07-24  7:31     ` Dominik Csapak [this message]
2025-05-21  8:45 ` [pbs-devel] [PATCH proxmox 2/2] client: use proxmox-http's " Dominik Csapak
2025-05-21  8:45 ` [pbs-devel] [PATCH proxmox-websocket-tunnel 1/2] update base64 dependency Dominik Csapak
2025-05-21  8:45 ` [pbs-devel] [PATCH proxmox-websocket-tunnel 2/2] use proxmox-http's openssl callback Dominik Csapak
2025-05-21  8:45 ` [pbs-devel] [PATCH proxmox-backup 1/1] pbs-client: use proxmox-https " Dominik Csapak
2025-07-24  8:59 ` [pbs-devel] superseded: [PATCH proxmox{, -websocket-tunnel, -backup} 0/5] unify openssl callback logic Dominik Csapak
2025-07-24  8:56 [pbs-devel] [PATCH proxmox{, -backup, -websocket-tunnel} 0/4] " Dominik Csapak
2025-07-24  8:56 ` [pbs-devel] [PATCH proxmox 1/2] http: factor out openssl verification 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=3d720c4a-5fdc-406d-9923-aa990a938c61@proxmox.com \
    --to=d.csapak@proxmox.com \
    --cc=pbs-devel@lists.proxmox.com \
    --cc=t.lamprecht@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
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal