From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 0D7681FF173 for ; Mon, 11 Nov 2024 14:16:37 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id E14C2B4D7; Mon, 11 Nov 2024 14:16:03 +0100 (CET) From: Christoph Heiss To: pve-devel@lists.proxmox.com Date: Mon, 11 Nov 2024 14:14:58 +0100 Message-ID: <20241111131519.867887-3-c.heiss@proxmox.com> X-Mailer: git-send-email 2.47.0 In-Reply-To: <20241111131519.867887-1-c.heiss@proxmox.com> References: <20241111131519.867887-1-c.heiss@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.030 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 02/12] fetch-answer: move http-related code to gated module in installer-common 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: , Reply-To: Proxmox VE development discussion Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pve-devel-bounces@lists.proxmox.com Sender: "pve-devel" This enable reusage of this code in other crates. Needed esp. by the upcoming post-installation notification hook functionality. No functional changes overall. Signed-off-by: Christoph Heiss --- Changes v2 -> v3: * rebased on latest master Changes v2 -> v3: * no changes Changes v1 -> v2: * no changes Cargo.toml | 4 + proxmox-fetch-answer/Cargo.toml | 15 +-- .../src/fetch_plugins/http.rs | 100 +----------------- proxmox-installer-common/Cargo.toml | 18 ++++ proxmox-installer-common/src/http.rs | 94 ++++++++++++++++ proxmox-installer-common/src/lib.rs | 3 + 6 files changed, 125 insertions(+), 109 deletions(-) create mode 100644 proxmox-installer-common/src/http.rs diff --git a/Cargo.toml b/Cargo.toml index ec1deaf..e20db33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,7 @@ members = [ [workspace.dependencies] anyhow = "1.0" +log = "0.4.20" +toml = "0.8" +proxmox-auto-installer.path = "./proxmox-auto-installer" +proxmox-installer-common.path = "./proxmox-installer-common" diff --git a/proxmox-fetch-answer/Cargo.toml b/proxmox-fetch-answer/Cargo.toml index 9149176..50f3da3 100644 --- a/proxmox-fetch-answer/Cargo.toml +++ b/proxmox-fetch-answer/Cargo.toml @@ -10,16 +10,9 @@ license = "AGPL-3" exclude = [ "build", "debian" ] homepage = "https://www.proxmox.com" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] anyhow.workspace = true -hex = "0.4" -log = "0.4.20" -native-tls = "0.2" -proxmox-auto-installer = { path = "../proxmox-auto-installer" } -rustls = { version = "0.21", features = [ "dangerous_configuration" ] } -rustls-native-certs = "0.6" -sha2 = "0.10" -toml = "0.8" -ureq = { version = "2.6", features = [ "native-certs", "native-tls" ] } +log.workspace = true +proxmox-auto-installer.workspace = true +proxmox-installer-common = { workspace = true, features = ["http"] } +toml.workspace = true diff --git a/proxmox-fetch-answer/src/fetch_plugins/http.rs b/proxmox-fetch-answer/src/fetch_plugins/http.rs index 5e10f6a..4317430 100644 --- a/proxmox-fetch-answer/src/fetch_plugins/http.rs +++ b/proxmox-fetch-answer/src/fetch_plugins/http.rs @@ -67,7 +67,8 @@ impl FetchFromHTTP { info!("Gathering system information."); let payload = SysInfo::as_json()?; info!("Sending POST request to '{answer_url}'."); - let answer = http_post::call(&answer_url, fingerprint.as_deref(), payload)?; + let answer = + proxmox_installer_common::http::post(&answer_url, fingerprint.as_deref(), payload)?; Ok(answer) } @@ -179,100 +180,3 @@ impl FetchFromHTTP { value.map(|value| String::from(&value[1..value.len() - 2])) } } - -mod http_post { - 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 - /// ``` - /// - /// # Arguments - /// * `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: &str, 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-8") - .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-8") - .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())) - } - } - } -} diff --git a/proxmox-installer-common/Cargo.toml b/proxmox-installer-common/Cargo.toml index fa99c57..007e6f4 100644 --- a/proxmox-installer-common/Cargo.toml +++ b/proxmox-installer-common/Cargo.toml @@ -13,3 +13,21 @@ regex = "1.7" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_plain = "1.0" + +# `http` feature +hex = { version = "0.4", optional = true } +native-tls = { version = "0.2", optional = true } +rustls = { version = "0.21", features = [ "dangerous_configuration" ], optional = true } +rustls-native-certs = { version = "0.6", optional = true } +sha2 = { version = "0.10", optional = true } +ureq = { version = "2.6", features = [ "native-certs", "native-tls" ], optional = true } + +[features] +http = [ + "dep:hex", + "dep:native-tls", + "dep:rustls", + "dep:rustls-native-certs", + "dep:sha2", + "dep:ureq" +] diff --git a/proxmox-installer-common/src/http.rs b/proxmox-installer-common/src/http.rs new file mode 100644 index 0000000..b754ed8 --- /dev/null +++ b/proxmox-installer-common/src/http.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 +/// ``` +/// +/// # Arguments +/// * `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 post(url: &str, 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-8") + .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-8") + .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())) + } + } +} diff --git a/proxmox-installer-common/src/lib.rs b/proxmox-installer-common/src/lib.rs index 028b43c..85fc399 100644 --- a/proxmox-installer-common/src/lib.rs +++ b/proxmox-installer-common/src/lib.rs @@ -3,6 +3,9 @@ pub mod options; pub mod setup; pub mod utils; +#[cfg(feature = "http")] +pub mod http; + pub const RUNTIME_DIR: &str = "/run/proxmox-installer"; /// Default placeholder value for the administrator email address. -- 2.47.0 _______________________________________________ pve-devel mailing list pve-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel