[pbs-devel] [PATCH proxmox v7 1/9] s3 client: add crate for AWS s3 compatible object store client

Christian Ebner c.ebner at proxmox.com
Fri Jul 11 12:52:14 CEST 2025


On 7/11/25 09:42, Thomas Lamprecht wrote:
> Am 10.07.25 um 19:06 schrieb Christian Ebner:
>> +    fn verify_certificate_fingerprint(
>> +        openssl_valid: bool,
>> +        context: &mut X509StoreContextRef,
>> +        expected_fingerprint: Option<String>,
>> +        trust_openssl: Arc<Mutex<bool>>,
>> +    ) -> Result<Option<String>, Error> {
> 
> This method seems a bit like it might fit better into a (micro) crate specific for
> "cert stuff". FWIW, there is a verify_fingerprint function in the proxmox-client
> crate already, this one here seems to be a bit more generic, or well also include
> things like the fp_string function for doing &[u8] -> String the client has separately.
> 
> 
> As both use openssl, i.e. X509StoreContextRef as base, it quite probably can share
> most of the implementation.
> 
> FWIW, I'd be even open for a quite specific proxmox-tls-cert-fingerprint micro
> crate, as IMO those micro crates to not produce much maintenance cost, especially
> if one assembles it after having the use case already in a few places, thus being
> pretty likely that the API will work OK that way for new future use cases too.
> Note, not promoting creation of trivial things, e.g. the famous leftpad crates,
> but TLS (fingerprint) cert verification is not really trivial and can have
> critical implications, which then can be IMO enough to justify a micro crate.
> 
> Anyhow, this can be refactored out transparently at any time, so really not
> a blocker for getting this client in.
> 
>> +        let mut trust_openssl_valid = trust_openssl.lock().unwrap();
>> +
>> +        // only rely on openssl prevalidation if was not forced earlier
>> +        if openssl_valid && *trust_openssl_valid {
>> +            return Ok(None);
>> +        }
>> +
>> +        let certificate = match context.current_cert() {
>> +            Some(certificate) => certificate,
>> +            None => bail!("context lacks current certificate."),
>> +        };
>> +
> 
> 
> This below would do well with a (short) explanatory comment IMO.
> 
>> +        if context.error_depth() > 0 {
>> +            *trust_openssl_valid = false;
>> +            return Ok(None);
>> +        }
>> +
>> +        let certificate_digest = certificate
>> +            .digest(MessageDigest::sha256())
>> +            .context("failed to calculate certificate digest")?;
>> +        let certificate_fingerprint = hex::encode(certificate_digest);
>> +        let certificate_fingerprint = certificate_fingerprint
>> +            .as_bytes()
>> +            .chunks(2)
>> +            .map(|v| std::str::from_utf8(v).unwrap())
>> +            .collect::<Vec<&str>>()
>> +            .join(":");
> 
> I really have nothing against iterator chains, but here unrolling that seems a
> bit more readable, e.g. the aforementioned fp_string fn from proxmox-client does:
> 
> let mut cert_fingerprint_str = String::new();
> for b in fp {
>      if !cert_fingerprint_str.is_empty() {
>          out.push(':');
>      }
>      let _ = write!(cert_fingerprint_str, "{b:02x}");
> }
> 
> Or at least avoid the upfront hex::encode and to that on the fly, e.g. like:
> 
> let certificate_fingerprint = certificate_digest
>      .iter()
>      .map(|byte| format!("{byte:02x}"))
>      .collect::<Vec<String>>()
>      .join(":");
> 
> 
> That variant would also be fine, IMO even nicer as the yours and the one from
> proxmox-client (and shorter as both!), but as disclaimer I only quicktested it in
> the rust playground:
> 
> https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=f575fdf9da37aa3ebefc28a66a93be2b
> 
>> +
>> +        if let Some(expected_fingerprint) = expected_fingerprint {
>> +            let expected_fingerprint = expected_fingerprint.to_lowercase();
> 
> The to_lowercase would not be required if we control the format and use a lower
> case x in the {byte:02x} fmt.

So actually this needs some more checking. After all the 
expected_fingerprint is provided to the client via the S3ClientOptions, 
which have this as optional String parameter. There is no guarantee that 
this will stem from a config, where it is checked a-priori.

So instead of doing a to_lowercase() here, during client instantiation 
there should happen some check along the lines of (untested):

```rust
@@ -105,7 +107,17 @@ impl S3Client {
      /// Creates a new S3 client instance, connecting to the provided 
endpoint using https given the
      /// provided options.
      pub fn new(options: S3ClientOptions) -> Result<Self, Error> {
-        let expected_fingerprint = options.fingerprint.clone();
+        let expected_fingerprint = if let Some(ref fingerprint) = 
options.fingerprint {
+            match CERT_FINGERPRINT_SHA256_SCHEMA {
+                Schema::String(ref schema) => schema
+                    .check_constraints(fingerprint)
+                    .context("invalid fingerprint provided")?,
+                _ => unreachable!(),
+            }
+            Some(fingerprint.to_lowercase())
+        } else {
+            None
+        };
          let verified_fingerprint = Arc::new(Mutex::new(None));
          let trust_openssl_valid = Arc::new(Mutex::new(true));
          let mut ssl_connector_builder = 
SslConnector::builder(SslMethod::tls())?;
```

That should make this more robust.

> 
>> +            if expected_fingerprint == certificate_fingerprint {
>> +                return Ok(Some(certificate_fingerprint));
>> +            }
>> +        }
>> +
>> +        Err(format_err!(
>> +            "unexpected certificate fingerprint {certificate_fingerprint}"
>> +        ))
>> +    }
> 





More information about the pbs-devel mailing list