From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 2A84691D87 for ; Thu, 4 Apr 2024 16:49:50 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 65C6D4688 for ; Thu, 4 Apr 2024 16:49:18 +0200 (CEST) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS for ; Thu, 4 Apr 2024 16:49:12 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 7AB6345171 for ; Thu, 4 Apr 2024 16:49:11 +0200 (CEST) From: Aaron Lauterer To: pve-devel@lists.proxmox.com Date: Thu, 4 Apr 2024 16:48:55 +0200 Message-Id: <20240404144902.273800-24-a.lauterer@proxmox.com> X-Mailer: git-send-email 2.39.2 In-Reply-To: <20240404144902.273800-1-a.lauterer@proxmox.com> References: <20240404144902.273800-1-a.lauterer@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.057 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pve-devel] [PATCH installer v4 23/30] auto-installer: fetch: add http post utility module X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Thu, 04 Apr 2024 14:49:50 -0000 It sends a http(s) POST request with the sysinfo as payload and expects an answer file in return. In order to handle non FQDN URLs (e.g. IP addresses) and self signed certificates, it can optionally take an SHA256 fingerprint of the certificate. This can of course also be used to pin a certificate explicitly, even if it would be in the trust chain. A custom cert verifier for ureq / rustl was necessary to get cert fingerprint matching to work. If no fingerprint is proviced, we switch rustls to native-certs and native-tls. Signed-off-by: Aaron Lauterer --- proxmox-auto-installer/Cargo.toml | 6 ++ .../src/fetch_plugins/utils/mod.rs | 1 + .../src/fetch_plugins/utils/post.rs | 94 +++++++++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 proxmox-auto-installer/src/fetch_plugins/utils/post.rs diff --git a/proxmox-auto-installer/Cargo.toml b/proxmox-auto-installer/Cargo.toml index bb0b49c..ac2f3a6 100644 --- a/proxmox-auto-installer/Cargo.toml +++ b/proxmox-auto-installer/Cargo.toml @@ -18,3 +18,9 @@ toml = "0.7" enum-iterator = "0.6.0" log = "0.4.20" regex = "1.7" +ureq = { version = "2.6", features = [ "native-certs", "native-tls" ] } +rustls = { version = "0.20", features = [ "dangerous_configuration" ] } +rustls-native-certs = "0.6" +native-tls = "0.2" +sha2 = "0.10" +hex = "0.4" diff --git a/proxmox-auto-installer/src/fetch_plugins/utils/mod.rs b/proxmox-auto-installer/src/fetch_plugins/utils/mod.rs index b3e9dad..6b4c7db 100644 --- a/proxmox-auto-installer/src/fetch_plugins/utils/mod.rs +++ b/proxmox-auto-installer/src/fetch_plugins/utils/mod.rs @@ -12,6 +12,7 @@ static ANSWER_MP: &str = "/mnt/answer"; static PARTLABEL: &str = "proxmoxinst"; static SEARCH_PATH: &str = "/dev/disk/by-label"; +pub mod post; pub mod sysinfo; /// Searches for upper and lower case existence of the partlabel in the search_path diff --git a/proxmox-auto-installer/src/fetch_plugins/utils/post.rs b/proxmox-auto-installer/src/fetch_plugins/utils/post.rs new file mode 100644 index 0000000..193e920 --- /dev/null +++ b/proxmox-auto-installer/src/fetch_plugins/utils/post.rs @@ -0,0 +1,94 @@ +use anyhow::Result; +use rustls::ClientConfig; +use sha2::{Digest, Sha256}; +use std::sync::Arc; +use ureq::{Agent, AgentBuilder}; + +/// Issues a POST request with the payload (JSON). Optionally a SHA256 fingerprint can be used to +/// check the cert against it, instead of the regular cert validation. +/// To gather the sha256 fingerprint you can use the following command: +/// ```no_compile +/// openssl s_client -connect :443 < /dev/null 2>/dev/null | openssl x509 -fingerprint -sha256 -noout -in /dev/stdin +/// ``` +/// +/// # Arguemnts +/// * `url` - URL to call +/// * `fingerprint` - SHA256 cert fingerprint if certificate pinning should be used. Optional. +/// * `payload` - The payload to send to the server. Expected to be a JSON formatted string. +pub fn call(url: String, fingerprint: Option<&str>, payload: String) -> Result { + let answer ; + + if let Some(fingerprint) = fingerprint { + let tls_config = ClientConfig::builder() + .with_safe_defaults() + .with_custom_certificate_verifier(VerifyCertFingerprint::new(fingerprint)?) + .with_no_client_auth(); + + let agent: Agent = AgentBuilder::new().tls_config(Arc::new(tls_config)).build(); + + answer = agent + .post(&url) + .set("Content-type", "application/json; charset=utf-") + .send_string(&payload)? + .into_string()?; + } else { + let mut roots = rustls::RootCertStore::empty(); + for cert in rustls_native_certs::load_native_certs()? { + roots.add(&rustls::Certificate(cert.0)).unwrap(); + } + + let tls_config = rustls::ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(roots) + .with_no_client_auth(); + + let agent = AgentBuilder::new() + .tls_connector(Arc::new(native_tls::TlsConnector::new()?)) + .tls_config(Arc::new(tls_config)) + .build(); + answer = agent + .post(&url) + .set("Content-type", "application/json; charset=utf-") + .timeout(std::time::Duration::from_secs(60)) + .send_string(&payload)? + .into_string()?; + } + Ok(answer) +} + +struct VerifyCertFingerprint { + cert_fingerprint: Vec, +} + +impl VerifyCertFingerprint { + fn new>(cert_fingerprint: S) -> Result> { + let cert_fingerprint = cert_fingerprint.as_ref(); + let sanitized = cert_fingerprint.replace(':', ""); + let decoded = hex::decode(sanitized)?; + Ok(std::sync::Arc::new(Self { + cert_fingerprint: decoded, + })) + } +} + +impl rustls::client::ServerCertVerifier for VerifyCertFingerprint { + fn verify_server_cert( + &self, + end_entity: &rustls::Certificate, + _intermediates: &[rustls::Certificate], + _server_name: &rustls::ServerName, + _scts: &mut dyn Iterator, + _ocsp_response: &[u8], + _now: std::time::SystemTime, + ) -> Result { + let mut hasher = Sha256::new(); + hasher.update(end_entity); + let result = hasher.finalize(); + + if result.as_slice() == self.cert_fingerprint { + Ok(rustls::client::ServerCertVerified::assertion()) + } else { + Err(rustls::Error::General("Fingerprint did not match!".into())) + } + } +} -- 2.39.2