public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pve-devel] [PATCH installer] common: http: add custom ureq connector for cert-fingerprint validation
@ 2025-06-25 12:22 Christoph Heiss
  2025-06-25 13:26 ` [pve-devel] applied: " Thomas Lamprecht
  0 siblings, 1 reply; 2+ messages in thread
From: Christoph Heiss @ 2025-06-25 12:22 UTC (permalink / raw)
  To: pve-devel

Adds back custom TLS fingerprint validation for ureq 3. The API was
changed majorly, no longer providing the possibility to directly provide
a rustls `ClientConfig` to the agent.

Instead, a complete custom transport + connector must be provided to
ureq to achieve this.

Mostly based on ureq#1085 [0], which provides a example how to implement
something like this.

[0] https://github.com/algesten/ureq/pull/1085

Fixes: 2557dbf ("common: initial upgrade to ureq 3 and rustls 0.23")
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
 proxmox-installer-common/src/http.rs | 145 +++++++++++++++++++++++++--
 1 file changed, 135 insertions(+), 10 deletions(-)

diff --git a/proxmox-installer-common/src/http.rs b/proxmox-installer-common/src/http.rs
index 08b9663..7662673 100644
--- a/proxmox-installer-common/src/http.rs
+++ b/proxmox-installer-common/src/http.rs
@@ -1,10 +1,17 @@
 use anyhow::Result;
-use rustls::ClientConfig;
 use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
+use rustls::{ClientConfig, ClientConnection, StreamOwned};
 use sha2::{Digest, Sha256};
-use std::io::Read;
+use std::fmt;
+use std::io::{Read, Write};
 use std::sync::Arc;
+use std::time::Duration;
 use ureq::Agent;
+use ureq::unversioned::resolver::DefaultResolver;
+use ureq::unversioned::transport::{
+    Buffers, ConnectionDetails, Connector, Either, LazyBuffers, NextTimeout, TcpConnector,
+    Transport, TransportAdapter,
+};
 
 /// Builds an [`Agent`] with TLS suitable set up, depending whether a custom fingerprint was
 /// supplied or not. If a fingerprint was supplied, only matching certificates will be accepted.
@@ -18,19 +25,34 @@ use ureq::Agent;
 /// # Arguments
 /// * `fingerprint` - SHA256 cert fingerprint if certificate pinning should be used. Optional.
 fn build_agent(fingerprint: Option<&str>) -> Result<Agent> {
+    const GLOBAL_TIMEOUT: Duration = Duration::from_secs(60);
+
     if let Some(fingerprint) = fingerprint {
-        let tls_config = ClientConfig::builder()
+        // If the user specified a custom TLS fingerprint, we must use a custom
+        // `rustls::ClientConfig`, which in turns means to use a custom
+        // `Connector`.
+        let crypto_provider = rustls::crypto::CryptoProvider::get_default()
+            .cloned()
+            .unwrap_or_else(|| Arc::new(rustls::crypto::ring::default_provider()));
+
+        let tls_config = ClientConfig::builder_with_provider(crypto_provider)
+            .with_protocol_versions(rustls::ALL_VERSIONS)?
             .dangerous()
             .with_custom_certificate_verifier(VerifyCertFingerprint::new(fingerprint)?)
             .with_no_client_auth();
 
-        Ok(Agent::config_builder()
-            //.tls_config(tls_config) // FIXME: add custom ureq connector to manage rustls
-            .build()
-            .into())
+        let connector = UreqRustlsConnector::new(Arc::new(tls_config));
+
+        Ok(Agent::with_parts(
+            ureq::config::Config::builder()
+                .timeout_global(Some(GLOBAL_TIMEOUT))
+                .build(),
+            TcpConnector::default().chain(connector),
+            DefaultResolver::default(),
+        ))
     } else {
         Ok(Agent::config_builder()
-            .timeout_global(Some(std::time::Duration::from_secs(60)))
+            .timeout_global(Some(GLOBAL_TIMEOUT))
             .tls_config(
                 ureq::tls::TlsConfig::builder()
                     .root_certs(ureq::tls::RootCerts::PlatformVerifier)
@@ -59,8 +81,7 @@ pub fn get_as_bytes(url: &str, fingerprint: Option<&str>, max_size: usize) -> Re
 
     let (_, body) = build_agent(fingerprint)?.get(url).call()?.into_parts();
 
-    body
-        .into_reader()
+    body.into_reader()
         .take(max_size as u64)
         .read_to_end(&mut result)?;
 
@@ -158,3 +179,107 @@ impl rustls::client::danger::ServerCertVerifier for VerifyCertFingerprint {
             .supported_schemes()
     }
 }
+
+/// Mostly a copy of [ureq::unversioned::transport::RustlsConnector], with the exception of using
+/// our custom [ClientConfig].
+#[derive(Debug)]
+struct UreqRustlsConnector {
+    /// [ClientConfig] to use for the TLS connection(s).
+    config: Arc<ClientConfig>,
+}
+
+impl UreqRustlsConnector {
+    fn new(config: Arc<ClientConfig>) -> Self {
+        UreqRustlsConnector { config }
+    }
+}
+
+impl<In: Transport> Connector<In> for UreqRustlsConnector {
+    type Out = Either<In, UreqRustlsTransport>;
+
+    fn connect(
+        &self,
+        details: &ConnectionDetails,
+        chained: Option<In>,
+    ) -> Result<Option<Self::Out>, ureq::Error> {
+        let Some(transport) = chained else {
+            panic!("RustlConnector requires a chained transport");
+        };
+
+        if !details.needs_tls() || transport.is_tls() {
+            return Ok(Some(Either::A(transport)));
+        }
+
+        let name: ServerName<'_> = details
+            .uri
+            .authority()
+            .ok_or(ureq::Error::Tls("no naming authority for URI"))?
+            .host()
+            .try_into()
+            .map_err(|_| ureq::Error::Tls("invalid dns name"))?;
+
+        let conn = ClientConnection::new(self.config.clone(), name.to_owned())?;
+        let stream = StreamOwned {
+            conn,
+            sock: TransportAdapter::new(transport.boxed()),
+        };
+
+        let buffers = LazyBuffers::new(
+            details.config.input_buffer_size(),
+            details.config.output_buffer_size(),
+        );
+
+        let transport = UreqRustlsTransport { buffers, stream };
+
+        Ok(Some(Either::B(transport)))
+    }
+}
+
+/// Direct copy of ureq/tls/rustls.rs:RustlsTransport, which unfortunately is not
+/// made public by the crate.
+struct UreqRustlsTransport {
+    buffers: LazyBuffers,
+    stream: StreamOwned<ClientConnection, TransportAdapter>,
+}
+
+impl Transport for UreqRustlsTransport {
+    fn buffers(&mut self) -> &mut dyn Buffers {
+        &mut self.buffers
+    }
+
+    fn transmit_output(&mut self, amount: usize, timeout: NextTimeout) -> Result<(), ureq::Error> {
+        self.stream.get_mut().set_timeout(timeout);
+
+        let output = &self.buffers.output()[..amount];
+        self.stream.write_all(output)?;
+
+        Ok(())
+    }
+
+    fn await_input(&mut self, timeout: NextTimeout) -> Result<bool, ureq::Error> {
+        self.stream.get_mut().set_timeout(timeout);
+
+        let input = self.buffers.input_append_buf();
+        let amount = self.stream.read(input)?;
+        self.buffers.input_appended(amount);
+
+        Ok(amount > 0)
+    }
+
+    fn is_open(&mut self) -> bool {
+        self.stream.get_mut().get_mut().is_open()
+    }
+
+    fn is_tls(&self) -> bool {
+        true
+    }
+}
+
+impl fmt::Debug for UreqRustlsTransport {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct("RustlsTransport")
+            .field("chained", &self.stream.sock.inner())
+            .field("buffers", &self.buffers)
+            .finish()
+    }
+}
-- 
2.49.0



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


^ permalink raw reply	[flat|nested] 2+ messages in thread

* [pve-devel] applied: [PATCH installer] common: http: add custom ureq connector for cert-fingerprint validation
  2025-06-25 12:22 [pve-devel] [PATCH installer] common: http: add custom ureq connector for cert-fingerprint validation Christoph Heiss
@ 2025-06-25 13:26 ` Thomas Lamprecht
  0 siblings, 0 replies; 2+ messages in thread
From: Thomas Lamprecht @ 2025-06-25 13:26 UTC (permalink / raw)
  To: pve-devel, Christoph Heiss

On Wed, 25 Jun 2025 14:22:01 +0200, Christoph Heiss wrote:
> Adds back custom TLS fingerprint validation for ureq 3. The API was
> changed majorly, no longer providing the possibility to directly provide
> a rustls `ClientConfig` to the agent.
> 
> Instead, a complete custom transport + connector must be provided to
> ureq to achieve this.
> 
> [...]

Applied, thanks!

[1/1] common: http: add custom ureq connector for cert-fingerprint validation
      commit: b877bc184bf43156da1daeaf0fe05bf826aea1e1


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


^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2025-06-25 13:26 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-06-25 12:22 [pve-devel] [PATCH installer] common: http: add custom ureq connector for cert-fingerprint validation Christoph Heiss
2025-06-25 13:26 ` [pve-devel] applied: " Thomas Lamprecht

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