From e8a199661918d8821dc6facb074ece584184564b Mon Sep 17 00:00:00 2001 From: Maciej Kozuszek Date: Thu, 4 Sep 2025 14:25:54 +0200 Subject: [PATCH 01/12] Move signature verification to separate directory --- base_layer/p2p/src/auto_update/error.rs | 10 +- base_layer/p2p/src/auto_update/mod.rs | 63 +++------- base_layer/p2p/src/config.rs | 6 + base_layer/p2p/src/dns/error.rs | 2 + base_layer/p2p/src/initialization.rs | 108 +++++++++--------- base_layer/p2p/src/lib.rs | 1 + base_layer/p2p/src/peer_seeds.rs | 9 ++ .../p2p/src/signature_verification/error.rs | 35 ++++++ .../gpg_keys/README.md | 0 .../gpg_keys/swvheerden.asc | 0 .../p2p/src/signature_verification/mod.rs | 103 +++++++++++++++++ .../verifier.rs} | 36 +++--- 12 files changed, 251 insertions(+), 122 deletions(-) create mode 100644 base_layer/p2p/src/signature_verification/error.rs rename base_layer/p2p/src/{auto_update => signature_verification}/gpg_keys/README.md (100%) rename base_layer/p2p/src/{auto_update => signature_verification}/gpg_keys/swvheerden.asc (100%) create mode 100644 base_layer/p2p/src/signature_verification/mod.rs rename base_layer/p2p/src/{auto_update/signature.rs => signature_verification/verifier.rs} (88%) diff --git a/base_layer/p2p/src/auto_update/error.rs b/base_layer/p2p/src/auto_update/error.rs index 50cdeb8511c..485e8e8286c 100644 --- a/base_layer/p2p/src/auto_update/error.rs +++ b/base_layer/p2p/src/auto_update/error.rs @@ -21,13 +21,13 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use crate::dns::DnsClientError; +use crate::signature_verification::SignatureVerificationError; +use thiserror::Error; -#[derive(Debug, thiserror::Error)] +#[derive(Debug, Error)] pub enum AutoUpdateError { #[error("DNS Client error: {0}")] DnsClientError(#[from] DnsClientError), - #[error("Failed to download file: {0}")] - DownloadError(#[from] reqwest::Error), - #[error("Failed to verify signature: {0}")] - SignatureError(#[from] pgp::errors::Error), + #[error("Signature verification error: {0}")] + SignatureVerificationError(#[from] SignatureVerificationError), } diff --git a/base_layer/p2p/src/auto_update/mod.rs b/base_layer/p2p/src/auto_update/mod.rs index 49d1468bfa0..de7f367e975 100644 --- a/base_layer/p2p/src/auto_update/mod.rs +++ b/base_layer/p2p/src/auto_update/mod.rs @@ -21,7 +21,6 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. mod dns; -mod signature; mod service; pub use service::{SoftwareUpdaterHandle, SoftwareUpdaterService}; @@ -30,15 +29,12 @@ mod error; use std::{ fmt, fmt::{Display, Formatter}, - io, str::FromStr, time::Duration, }; pub use error::AutoUpdateError; use futures::future; -use pgp::Deserializable; -use reqwest::IntoUrl; // Re-exports of foreign types used in public interface pub use semver::Version; use serde::{Deserialize, Serialize}; @@ -49,12 +45,12 @@ use tari_common::{ utils::{deserialize_string_or_struct, serialize_string}, StringList, }, - DnsNameServer, - SubConfigPath, + DnsNameServer, SubConfigPath, }; use tari_utilities::hex::Hex; -use crate::auto_update::{dns::UpdateSpec, signature::SignedMessageVerifier}; +use crate::auto_update::dns::UpdateSpec; +use crate::signature_verification::{self, SignedMessageVerifier}; const LOG_TARGET: &str = "p2p::auto_update"; @@ -122,24 +118,24 @@ pub async fn check_for_updates( ); let (hashes, sig) = future::join( - download_hashes_file(&hashes_url), - download_hashes_sig_file(&hashes_sig_url), + signature_verification::download_hashes_file(&hashes_url), + signature_verification::download_hashes_sig_file(&hashes_sig_url), ) .await; let hashes = hashes?; let sig = sig?; - let verifier = SignedMessageVerifier::new(maintainers().collect()); - verifier - .verify_signed_update(&sig, &hashes, &update_spec) - .map(|(_, filename)| { + let verifier = SignedMessageVerifier::new(signature_verification::maintainers().collect()); + match verifier.verify_signed_hashes(&sig, &hashes, &update_spec.hash) { + Ok((_, filename)) => { let download_url = format!("{download_base_url}/{filename}"); log::info!(target: LOG_TARGET, "Valid update found at {download_url}"); - Ok(SoftwareUpdate { + Ok(Some(SoftwareUpdate { spec: update_spec, download_url, - }) - }) - .transpose() + })) + }, + Err(_) => Ok(None), + } }, None => { log::info!("No new updates for {app} ({arch} {version})"); @@ -183,45 +179,12 @@ impl Display for SoftwareUpdate { } } -async fn download_hashes_file(url: T) -> Result { - let resp = http_download(url).await?; - let txt = resp.text().await?; - Ok(txt) -} - -async fn download_hashes_sig_file(url: T) -> Result { - let resp = http_download(url).await?; - let sig_bytes = resp.bytes().await?; - let cursor = io::Cursor::new(&sig_bytes); - let sig = pgp::StandaloneSignature::from_bytes(cursor).map_err(AutoUpdateError::SignatureError)?; - Ok(sig) -} - -async fn http_download(url: T) -> Result { - let resp = reqwest::get(url).await?.error_for_status()?; - Ok(resp) -} - -const MAINTAINERS: &[&str] = &[include_str!("gpg_keys/swvheerden.asc")]; - -fn maintainers() -> impl Iterator { - MAINTAINERS.iter().map(|s| { - let (pk, _) = pgp::SignedPublicKey::from_string(s).expect("Malformed maintainer PGP signature"); - pk - }) -} - #[cfg(test)] mod test { use tari_common::DefaultConfigLoader; use super::*; - #[test] - fn all_maintainers_well_formed() { - assert_eq!(maintainers().count(), MAINTAINERS.len()); - } - fn get_config(config_name: Option<&str>) -> config::Config { let s = match config_name { Some(o) => { diff --git a/base_layer/p2p/src/config.rs b/base_layer/p2p/src/config.rs index 5325692cddd..26ede608383 100644 --- a/base_layer/p2p/src/config.rs +++ b/base_layer/p2p/src/config.rs @@ -67,6 +67,8 @@ pub struct PeerSeedsConfig { /// All DNS seed records must pass DNSSEC validation #[serde(default)] pub dns_seeds_use_dnssec: bool, + #[serde(default)] + pub download_url: String, } impl Default for PeerSeedsConfig { @@ -98,6 +100,10 @@ impl Default for PeerSeedsConfig { ) .expect("string is valid"), dns_seeds_use_dnssec: false, + download_url: format!( + "https://cdn-universe.tari.com/tari-project/tari/{}/seednodes.json", + Network::get_current_or_user_setting_or_default().as_key_str() + ), } } } diff --git a/base_layer/p2p/src/dns/error.rs b/base_layer/p2p/src/dns/error.rs index fe781f912e3..2475f72da16 100644 --- a/base_layer/p2p/src/dns/error.rs +++ b/base_layer/p2p/src/dns/error.rs @@ -38,4 +38,6 @@ pub enum DnsClientError { DnsNameRequiredForDnsSec, #[error("Connection error: {0}")] Connection(String), + #[error("No download URL found")] + NoDownloadUrlFound, } diff --git a/base_layer/p2p/src/initialization.rs b/base_layer/p2p/src/initialization.rs index 173054a524a..d4a9e268fb9 100644 --- a/base_layer/p2p/src/initialization.rs +++ b/base_layer/p2p/src/initialization.rs @@ -28,7 +28,6 @@ use std::{ time::{Duration, Instant}, }; -use futures::future; use log::*; use tari_common::{ configuration::{DnsNameServerList, Network}, @@ -44,34 +43,21 @@ use tari_comms::{ multiaddr::multiaddr, peer_manager::{ database::{PeerDatabaseSql, MIGRATIONS}, - NodeIdentity, - Peer, - PeerFeatures, - PeerFlags, - PeerManagerError, + NodeIdentity, Peer, PeerFeatures, PeerFlags, PeerManagerError, }, pipeline, protocol::{ messaging::{MessagingEventSender, MessagingProtocolExtension}, rpc::RpcServer, - NodeNetworkInfo, - ProtocolId, + NodeNetworkInfo, ProtocolId, }, tor::{self, HiddenServiceControllerError, TorIdentity}, transports::{ - predicate::FalsePredicate, - HiddenServiceTransport, - MemoryTransport, - SocksConfig, - SocksTransport, + predicate::FalsePredicate, HiddenServiceTransport, MemoryTransport, SocksConfig, SocksTransport, TcpWithTorTransport, }, utils::cidr::parse_cidrs, - CommsBuilder, - CommsBuilderError, - CommsNode, - PeerManager, - UnspawnedCommsNode, + CommsBuilder, CommsBuilderError, CommsNode, PeerManager, UnspawnedCommsNode, }; use tari_comms_dht::{Dht, DhtInitializationError}; use tari_service_framework::{async_trait, ServiceInitializationError, ServiceInitializer, ServiceInitializerContext}; @@ -90,9 +76,7 @@ use crate::{ dns::DnsClientError, peer_seeds::{DnsSeedResolver, SeedPeer}, transport::{TorTransportConfig, TransportType}, - TransportConfig, - MAJOR_NETWORK_VERSION, - MINOR_NETWORK_VERSION, + TransportConfig, MAJOR_NETWORK_VERSION, MINOR_NETWORK_VERSION, }; const LOG_TARGET: &str = "p2p::initialization"; @@ -100,6 +84,23 @@ const LOG_TARGET: &str = "p2p::initialization"; /// ProtocolId for minotari messaging protocol pub static MESSAGING_PROTOCOL_ID: ProtocolId = ProtocolId::from_static(b"t/msg/0.1"); +// Example usage of signature verification module: +// +// use crate::signature_verification::{SignedMessageVerifier, maintainers, verify_signed_hash_file}; +// +// async fn verify_downloaded_file(hash_url: &str, sig_url: &str, file_hash: &[u8]) -> Result<(), Error> { +// // Method 1: Use the convenience function +// let (hash, filename) = verify_signed_hash_file(hash_url, sig_url, file_hash).await?; +// +// // Method 2: Use the verifier directly +// let verifier = SignedMessageVerifier::new(maintainers().collect()); +// let hashes = signature_verification::download_hashes_file(hash_url).await?; +// let sig = signature_verification::download_hashes_sig_file(sig_url).await?; +// let (hash, filename) = verifier.verify_signed_hashes(&sig, &hashes, file_hash)?; +// +// Ok(()) +// } + #[derive(Debug, Error)] pub enum CommsInitializationError { #[error("Comms builder error: `{0}`")] @@ -474,53 +475,54 @@ impl P2pInitializer { .collect::>() .join(",") ); - let start = Instant::now(); + let _start = Instant::now(); let resolver = P2pInitializer::get_dns_seed_resolver(config.dns_seeds_use_dnssec, &config.dns_seed_name_servers).await?; - let resolving = config.dns_seeds.iter().map(|addr| { + let _resolving = config.dns_seeds.iter().map(|addr| { let mut resolver = resolver.clone(); async move { let timer = Instant::now(); - let seeds_res = match timeout(Duration::from_secs(5), resolver.resolve(addr)).await { + let download_url_res = match timeout(Duration::from_secs(5), resolver.resolve_download_url(addr)).await + { Ok(res) => res, Err(_) => { - warn!(target: LOG_TARGET, "Timeout resolving DNS seed `{addr}`"); + warn!(target: LOG_TARGET, "Timeout resolving DNS download URL `{addr}`"); Err(DnsClientError::Timeout) }, }; - // let res = (resolver.resolve(addr).await, addr); - let res = (seeds_res, addr.clone()); - info!(target: LOG_TARGET, "Resolved DNS seed `{}` in {:.0?}", addr, timer.elapsed()); + let res = (download_url_res, addr.clone()); + info!(target: LOG_TARGET, "Resolved DNS download URL `{}` in {:.0?}", addr, timer.elapsed()); res } }); - let peers = future::join_all(resolving) - .await - .into_iter() - // Log and ignore errors - .filter_map(|(result, addr)| match result { - Ok(peers) => { - info!( - target: LOG_TARGET, - "Found {} peer(s) from `{}` in {:.0?}", - peers.len(), - addr, - start.elapsed() - ); - Some(peers) - }, - Err(err) => { - warn!(target: LOG_TARGET, "DNS seed `{addr}` failed to resolve: {err}"); - None - }, - }) - .flatten() - .map(Into::into) - .collect::>(); - - Ok(peers) + // let peers = future::join_all(resolving) + // .await + // .into_iter() + // // Log and ignore errors + // .filter_map(|(result, addr)| match result { + // Ok(peers) => { + // info!( + // target: LOG_TARGET, + // "Found {} peer(s) from `{}` in {:.0?}", + // peers.len(), + // addr, + // start.elapsed() + // ); + // Some(peers) + // }, + // Err(err) => { + // warn!(target: LOG_TARGET, "DNS seed `{addr}` failed to resolve: {err}"); + // None + // }, + // }) + // .flatten() + // .map(Into::into) + // .collect::>(); + + // Ok(peers) + Ok(vec![]) } async fn get_dns_seed_resolver( diff --git a/base_layer/p2p/src/lib.rs b/base_layer/p2p/src/lib.rs index 23a31eac338..3fb0b6d1cf1 100644 --- a/base_layer/p2p/src/lib.rs +++ b/base_layer/p2p/src/lib.rs @@ -36,6 +36,7 @@ pub mod peer; pub mod peer_seeds; pub mod proto; pub mod services; +pub mod signature_verification; mod socks_authentication; pub mod tari_message; mod tor_authentication; diff --git a/base_layer/p2p/src/peer_seeds.rs b/base_layer/p2p/src/peer_seeds.rs index 3d81c59b77c..22d4d6aa273 100644 --- a/base_layer/p2p/src/peer_seeds.rs +++ b/base_layer/p2p/src/peer_seeds.rs @@ -91,6 +91,15 @@ impl DnsSeedResolver { .collect(); Ok(peers) } + + pub async fn resolve_download_url(&mut self, addr: &str) -> Result { + let records = self.client.query_txt(addr).await?; + let download_url = records + .into_iter() + .find(|record| record.starts_with("https://") || record.starts_with("http://")) + .ok_or(DnsClientError::NoDownloadUrlFound)?; + Ok(download_url.to_string()) + } } /// Parsed information from a DNS seed record diff --git a/base_layer/p2p/src/signature_verification/error.rs b/base_layer/p2p/src/signature_verification/error.rs new file mode 100644 index 00000000000..c728a1466c4 --- /dev/null +++ b/base_layer/p2p/src/signature_verification/error.rs @@ -0,0 +1,35 @@ +// Copyright 2021, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum SignatureVerificationError { + #[error("Failed to download file: {0}")] + DownloadError(#[from] reqwest::Error), + #[error("Failed to verify signature: {0}")] + SignatureError(#[from] pgp::errors::Error), + #[error("Signature verification failed")] + VerificationFailed, + #[error("Invalid hash format")] + InvalidHashFormat, +} diff --git a/base_layer/p2p/src/auto_update/gpg_keys/README.md b/base_layer/p2p/src/signature_verification/gpg_keys/README.md similarity index 100% rename from base_layer/p2p/src/auto_update/gpg_keys/README.md rename to base_layer/p2p/src/signature_verification/gpg_keys/README.md diff --git a/base_layer/p2p/src/auto_update/gpg_keys/swvheerden.asc b/base_layer/p2p/src/signature_verification/gpg_keys/swvheerden.asc similarity index 100% rename from base_layer/p2p/src/auto_update/gpg_keys/swvheerden.asc rename to base_layer/p2p/src/signature_verification/gpg_keys/swvheerden.asc diff --git a/base_layer/p2p/src/signature_verification/mod.rs b/base_layer/p2p/src/signature_verification/mod.rs new file mode 100644 index 00000000000..73bb0519d20 --- /dev/null +++ b/base_layer/p2p/src/signature_verification/mod.rs @@ -0,0 +1,103 @@ +// Copyright 2021, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod error; +mod verifier; + +pub use error::SignatureVerificationError; +use futures; +use pgp::Deserializable; +use reqwest::IntoUrl; +use std::io; +pub use verifier::SignedMessageVerifier; + +const LOG_TARGET: &str = "p2p::signature_verification"; + +// Include GPG keys of authorized maintainers +const MAINTAINERS: &[&str] = &[include_str!("gpg_keys/swvheerden.asc")]; + +/// Returns an iterator over all configured maintainer public keys +pub fn maintainers() -> impl Iterator { + MAINTAINERS.iter().map(|s| { + let (pk, _) = pgp::SignedPublicKey::from_string(s).expect("Malformed maintainer PGP signature"); + pk + }) +} + +/// Download a text file from the given URL +pub async fn download_hashes_file(url: T) -> Result { + let resp = http_download(url).await?; + let txt = resp.text().await?; + Ok(txt) +} + +/// Download a PGP signature file from the given URL +pub async fn download_hashes_sig_file( + url: T, +) -> Result { + let resp = http_download(url).await?; + let sig_bytes = resp.bytes().await?; + let cursor = io::Cursor::new(&sig_bytes); + let sig = pgp::StandaloneSignature::from_bytes(cursor).map_err(SignatureVerificationError::SignatureError)?; + Ok(sig) +} + +/// Perform an HTTP GET request and return the response +async fn http_download(url: T) -> Result { + let resp = reqwest::get(url).await?.error_for_status()?; + Ok(resp) +} + +/// Verify a signed hash file and extract the hash and filename for a target hash +/// +/// This function: +/// 1. Verifies the signature of the hashes file using maintainer keys +/// 2. Parses the hashes file to find a matching hash +/// 3. Returns the hash and associated filename if found +pub async fn verify_signed_hash_file( + hashes_url: &str, + signature_url: &str, + target_hash: &[u8], +) -> Result<(Vec, String), SignatureVerificationError> { + // Download both files in parallel + let (hashes, sig) = futures::join!( + download_hashes_file(hashes_url), + download_hashes_sig_file(signature_url) + ); + + let hashes = hashes?; + let sig = sig?; + + // Verify the signature using maintainer keys + let verifier = SignedMessageVerifier::new(maintainers().collect()); + verifier.verify_signed_hashes(&sig, &hashes, target_hash) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn all_maintainers_well_formed() { + assert_eq!(maintainers().count(), MAINTAINERS.len()); + } +} diff --git a/base_layer/p2p/src/auto_update/signature.rs b/base_layer/p2p/src/signature_verification/verifier.rs similarity index 88% rename from base_layer/p2p/src/auto_update/signature.rs rename to base_layer/p2p/src/signature_verification/verifier.rs index fb770e99924..9d119deaa6c 100644 --- a/base_layer/p2p/src/auto_update/signature.rs +++ b/base_layer/p2p/src/signature_verification/verifier.rs @@ -22,7 +22,7 @@ use tari_utilities::hex::from_hex; -use crate::auto_update::dns::UpdateSpec; +use crate::signature_verification::error::SignatureVerificationError; pub struct SignedMessageVerifier { maintainers: Vec, @@ -33,13 +33,26 @@ impl SignedMessageVerifier { Self { maintainers } } - pub fn verify_signed_update( + /// Verify a standalone signature against a message using the configured maintainers' public keys + pub fn verify_signature( + &self, + signature: &pgp::StandaloneSignature, + message: &str, + ) -> Option<&pgp::SignedPublicKey> { + self.maintainers + .iter() + .find(|pk| signature.verify(pk, message.as_bytes()).is_ok()) + } + + /// Verify a signed hash file and return the matching hash and filename for a given target hash + pub fn verify_signed_hashes( &self, signature: &pgp::StandaloneSignature, hashes: &str, - update: &UpdateSpec, - ) -> Option<(Vec, String)> { - self.verify_signature(signature, hashes)?; + target_hash: &[u8], + ) -> Result<(Vec, String), SignatureVerificationError> { + self.verify_signature(signature, hashes) + .ok_or(SignatureVerificationError::VerificationFailed)?; hashes .lines() @@ -49,13 +62,8 @@ impl SignedMessageVerifier { let filename = parts.next()?; Some((hash, filename.trim().to_string())) }) - .find(|(hash, _)| update.hash == *hash) - } - - fn verify_signature(&self, signature: &pgp::StandaloneSignature, message: &str) -> Option<&pgp::SignedPublicKey> { - self.maintainers - .iter() - .find(|pk| signature.verify(pk, message.as_bytes()).is_ok()) + .find(|(hash, _)| *hash == target_hash) + .ok_or(SignatureVerificationError::InvalidHashFormat) } } @@ -64,7 +72,6 @@ mod test { use pgp::Deserializable; use super::*; - use crate::auto_update::maintainers; const PUBLIC_KEY: &str = r#"-----BEGIN PGP PUBLIC KEY BLOCK----- @@ -151,7 +158,8 @@ l9smp8LtJcXkw4cNgE4MB9VKdx+NhdbvWemt7ccldeL22hmyS24= #[test] fn it_does_not_validate_with_tampered_message() { let (sig, _) = pgp::StandaloneSignature::from_string(VALID_SIGNATURE.trim()).unwrap(); - let verifier = SignedMessageVerifier::new(maintainers().collect()); + let (key, _) = pgp::SignedPublicKey::from_string(PUBLIC_KEY).unwrap(); + let verifier = SignedMessageVerifier::new(vec![key]); assert!(verifier.verify_signature(&sig, "Zilip R. Phimmermann").is_none()); } } From c363cdd93633d7f7f73bfbc4a81c633c0a2ab294 Mon Sep 17 00:00:00 2001 From: Maciej Kozuszek Date: Thu, 4 Sep 2025 17:03:48 +0200 Subject: [PATCH 02/12] Download and verify signatures of seed peers file --- Cargo.lock | 1 + base_layer/p2p/Cargo.toml | 1 + base_layer/p2p/src/initialization.rs | 338 +++++++++++++++--- .../gpg_keys/seed_peers_http.asc | 10 + .../p2p/src/signature_verification/mod.rs | 82 ++++- .../src/signature_verification/verifier.rs | 89 +++++ 6 files changed, 450 insertions(+), 71 deletions(-) create mode 100644 base_layer/p2p/src/signature_verification/gpg_keys/seed_peers_http.asc diff --git a/Cargo.lock b/Cargo.lock index 2a0bf22a4e4..2f48b1f02de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7956,6 +7956,7 @@ dependencies = [ "reqwest 0.11.27", "semver", "serde", + "serde_json", "tari_common", "tari_common_sqlite", "tari_comms", diff --git a/base_layer/p2p/Cargo.toml b/base_layer/p2p/Cargo.toml index fb06b84ef82..c588f5c80da 100644 --- a/base_layer/p2p/Cargo.toml +++ b/base_layer/p2p/Cargo.toml @@ -26,6 +26,7 @@ rand = "0.8" reqwest = { version = "0.11", optional = true, default-features = false } semver = { version = "1.0.1", optional = true } serde = "1.0.90" +serde_json = "1.0.51" thiserror = "1.0.26" tokio = { version = "1.44", features = ["macros"] } tokio-stream = { version = "0.1.9", default-features = false, features = [ diff --git a/base_layer/p2p/src/initialization.rs b/base_layer/p2p/src/initialization.rs index d4a9e268fb9..335f5db92ac 100644 --- a/base_layer/p2p/src/initialization.rs +++ b/base_layer/p2p/src/initialization.rs @@ -28,7 +28,9 @@ use std::{ time::{Duration, Instant}, }; +use anyhow::anyhow; use log::*; +use serde_json; use tari_common::{ configuration::{DnsNameServerList, Network}, exit_codes::{ExitCode, ExitError}, @@ -86,20 +88,20 @@ pub static MESSAGING_PROTOCOL_ID: ProtocolId = ProtocolId::from_static(b"t/msg/0 // Example usage of signature verification module: // -// use crate::signature_verification::{SignedMessageVerifier, maintainers, verify_signed_hash_file}; +// use crate::signature_verification::{verify_signed_file, verify_signed_hash_file, SignedMessageVerifier, maintainers}; // -// async fn verify_downloaded_file(hash_url: &str, sig_url: &str, file_hash: &[u8]) -> Result<(), Error> { -// // Method 1: Use the convenience function -// let (hash, filename) = verify_signed_hash_file(hash_url, sig_url, file_hash).await?; -// -// // Method 2: Use the verifier directly -// let verifier = SignedMessageVerifier::new(maintainers().collect()); -// let hashes = signature_verification::download_hashes_file(hash_url).await?; -// let sig = signature_verification::download_hashes_sig_file(sig_url).await?; -// let (hash, filename) = verifier.verify_signed_hashes(&sig, &hashes, file_hash)?; +// // For verifying generic files like seednodes.json: +// async fn verify_seed_nodes(url: &str, sig_url: &str) -> Result { +// let content = verify_signed_file(url, sig_url).await?; +// Ok(content) +// } // +// // For verifying hash files: +// async fn verify_hash_file(hash_url: &str, sig_url: &str, file_hash: &[u8]) -> Result<(), Error> { +// let (hash, filename) = verify_signed_hash_file(hash_url, sig_url, file_hash).await?; // Ok(()) // } +// #[derive(Debug, Error)] pub enum CommsInitializationError { @@ -457,7 +459,72 @@ impl P2pInitializer { .collect::, _>>() } + async fn get_url_from_dns(resolver: &mut DnsSeedResolver, addr: &str) -> Result<(String, String), DnsClientError> { + let timer = Instant::now(); + let download_url_res = match timeout(Duration::from_secs(5), resolver.resolve_download_url(addr)).await { + Ok(res) => res, + Err(_) => { + warn!(target: LOG_TARGET, "Timeout resolving DNS download URL `{addr}`"); + Err(DnsClientError::Timeout) + }, + }?; + let res = (download_url_res, addr.to_string()); + info!(target: LOG_TARGET, "Resolved DNS download URL `{}` in {:.0?}", addr, timer.elapsed()); + Ok(res) + } + + /// downloads seed peers files - json with peers and .asc for verification + async fn download_seed_peers_files( + (url, addr): (String, String), + ) -> Result, ServiceInitializationError> { + use crate::signature_verification::verify_signed_file; + use serde::Deserialize; + + #[derive(Deserialize)] + struct SeedNodesJson { + peer_seeds: Vec, + } + + let timer = Instant::now(); + + // Download and verify the seed nodes file with its signature + let content = verify_signed_file(&url, &format!("{}.asc", url)).await.map_err(|e| { + warn!(target: LOG_TARGET, "Failed to verify seed nodes file from {}: {}", url, e); + anyhow!("Signature verification failed: {}", e) + })?; + + // Parse the JSON content + let seed_nodes: SeedNodesJson = serde_json::from_str(&content).map_err(|e: serde_json::Error| { + warn!(target: LOG_TARGET, "Failed to parse seed nodes JSON from {}: {}", url, e); + anyhow!("Invalid JSON: {}", e) + })?; + + // Convert strings to SeedPeer objects + let mut peers = Vec::new(); + for peer_str in seed_nodes.peer_seeds { + match peer_str.parse::() { + Ok(peer) => peers.push(peer), + Err(e) => { + warn!(target: LOG_TARGET, "Failed to parse peer '{}': {}", peer_str, e); + // Continue with other peers even if one fails + }, + } + } + + info!( + target: LOG_TARGET, + "Downloaded and verified {} seed peers from {} in {:.0?}", + peers.len(), + addr, + timer.elapsed() + ); + + Ok(peers) + } + async fn try_resolve_dns_seeds(config: &PeerSeedsConfig) -> Result, ServiceInitializationError> { + use futures::future; + if config.dns_seeds.is_empty() { debug!(target: LOG_TARGET, "No DNS Seeds configured"); return Ok(Vec::new()); @@ -475,54 +542,59 @@ impl P2pInitializer { .collect::>() .join(",") ); - let _start = Instant::now(); + let start = Instant::now(); let resolver = P2pInitializer::get_dns_seed_resolver(config.dns_seeds_use_dnssec, &config.dns_seed_name_servers).await?; - let _resolving = config.dns_seeds.iter().map(|addr| { + + // First, resolve all DNS records to get download URLs + let resolving = config.dns_seeds.iter().map(|addr| { let mut resolver = resolver.clone(); - async move { - let timer = Instant::now(); - let download_url_res = match timeout(Duration::from_secs(5), resolver.resolve_download_url(addr)).await - { - Ok(res) => res, - Err(_) => { - warn!(target: LOG_TARGET, "Timeout resolving DNS download URL `{addr}`"); - Err(DnsClientError::Timeout) - }, - }; - let res = (download_url_res, addr.clone()); - info!(target: LOG_TARGET, "Resolved DNS download URL `{}` in {:.0?}", addr, timer.elapsed()); - res - } + let addr = addr.clone(); + async move { P2pInitializer::get_url_from_dns(&mut resolver, &addr).await } }); - // let peers = future::join_all(resolving) - // .await - // .into_iter() - // // Log and ignore errors - // .filter_map(|(result, addr)| match result { - // Ok(peers) => { - // info!( - // target: LOG_TARGET, - // "Found {} peer(s) from `{}` in {:.0?}", - // peers.len(), - // addr, - // start.elapsed() - // ); - // Some(peers) - // }, - // Err(err) => { - // warn!(target: LOG_TARGET, "DNS seed `{addr}` failed to resolve: {err}"); - // None - // }, - // }) - // .flatten() - // .map(Into::into) - // .collect::>(); - - // Ok(peers) - Ok(vec![]) + let resolved_urls: Vec<(String, String)> = future::join_all(resolving) + .await + .into_iter() + .filter_map(|result| match result { + Ok(url_pair) => Some(url_pair), + Err(e) => { + warn!(target: LOG_TARGET, "Failed to resolve DNS seed: {}", e); + None + }, + }) + .collect(); + + // Download and verify seed peer files + let downloading = resolved_urls + .into_iter() + .map(|url_pair| async move { P2pInitializer::download_seed_peers_files(url_pair).await }); + + let all_seed_peers: Vec = future::join_all(downloading) + .await + .into_iter() + .filter_map(|result| match result { + Ok(peers) => Some(peers), + Err(e) => { + warn!(target: LOG_TARGET, "Failed to download/verify seed peers: {}", e); + None + }, + }) + .flatten() + .collect(); + + // Convert SeedPeer to Peer + let peers: Vec = all_seed_peers.into_iter().map(Peer::from).collect(); + + info!( + target: LOG_TARGET, + "Resolved {} seed peers from DNS in {:.0?}", + peers.len(), + start.elapsed() + ); + + Ok(peers) } async fn get_dns_seed_resolver( @@ -615,11 +687,173 @@ impl ServiceInitializer for P2pInitializer { #[cfg(test)] mod test { + use super::*; use tari_common::configuration::Network; use tari_comms::connection_manager::WireMode; + #[test] fn self_liveness_network_wire_byte_is_consistent() { let wire_mode = WireMode::Liveness; assert_eq!(wire_mode.as_byte(), Network::RESERVED_WIRE_BYTE); } + + #[tokio::test] + async fn test_parse_seed_peers_from_json() { + // Test JSON content that matches the expected format from cdn-universe.tari.com + let json_content = r#"{ + "peer_seeds": [ + "4cdfb70e0b38b60c6a3573b2870e32bc3d846419c606ea379f43650b80f38409::/ip4/51.83.4.85/tcp/18189", + "1e08628960f75b7e324f010b2ee609a9e28097e9101f4d769d474a38b6ee2d76::/ip4/51.83.102.25/tcp/18189", + "1e08628960f75b7e324f010b2ee609a9e28097e9101f4d769d474a38b6ee2d76::/ip6/2001:41d0:303:a619::1/tcp/18189", + "4cdfb70e0b38b60c6a3573b2870e32bc3d846419c606ea379f43650b80f38409::/ip6/2001:41d0:303:9a55::1/tcp/18189", + "1e08628960f75b7e324f010b2ee609a9e28097e9101f4d769d474a38b6ee2d76::/onion3/tadnxyokalnqjtvu6mlhxndcq4v2tlolotpvrflscdmi7lcautao3had:18141", + "4cdfb70e0b38b60c6a3573b2870e32bc3d846419c606ea379f43650b80f38409::/onion3/mhfptgpcj6htjkr5zwurom32wvt7x76ovqzn2ttnwo2bnku6baeaaiyd:18141" + ] + }"#; + + // Parse the JSON + #[derive(serde::Deserialize)] + struct SeedNodesJson { + peer_seeds: Vec, + } + + let seed_nodes: SeedNodesJson = serde_json::from_str(json_content).unwrap(); + assert_eq!(seed_nodes.peer_seeds.len(), 6); + + // Parse each peer string into SeedPeer + let mut peers = Vec::new(); + for peer_str in seed_nodes.peer_seeds { + let peer = peer_str.parse::().unwrap(); + peers.push(peer); + } + + assert_eq!(peers.len(), 6); + + // Verify the first peer (IPv4) + let first_peer = &peers[0]; + assert_eq!( + first_peer.public_key.to_hex(), + "4cdfb70e0b38b60c6a3573b2870e32bc3d846419c606ea379f43650b80f38409" + ); + assert_eq!(first_peer.addresses.len(), 1); + assert_eq!(first_peer.addresses[0].to_string(), "/ip4/51.83.4.85/tcp/18189"); + + // Verify an IPv6 peer + let ipv6_peer = &peers[2]; + assert_eq!( + ipv6_peer.public_key.to_hex(), + "1e08628960f75b7e324f010b2ee609a9e28097e9101f4d769d474a38b6ee2d76" + ); + assert_eq!( + ipv6_peer.addresses[0].to_string(), + "/ip6/2001:41d0:303:a619::1/tcp/18189" + ); + + // Verify an onion peer + let onion_peer = &peers[4]; + assert_eq!( + onion_peer.addresses[0].to_string(), + "/onion3/tadnxyokalnqjtvu6mlhxndcq4v2tlolotpvrflscdmi7lcautao3had:18141" + ); + } + + #[tokio::test] + async fn test_try_parse_seed_peers() { + let peer_seeds = vec![ + "4cdfb70e0b38b60c6a3573b2870e32bc3d846419c606ea379f43650b80f38409::/ip4/51.83.4.85/tcp/18189".to_string(), + "1e08628960f75b7e324f010b2ee609a9e28097e9101f4d769d474a38b6ee2d76::/ip4/51.83.102.25/tcp/18189".to_string(), + ]; + + let peers = P2pInitializer::try_parse_seed_peers(&peer_seeds).unwrap(); + assert_eq!(peers.len(), 2); + + // Verify conversion to Peer works + let first_peer = &peers[0]; + assert_eq!( + first_peer.public_key.to_hex(), + "4cdfb70e0b38b60c6a3573b2870e32bc3d846419c606ea379f43650b80f38409" + ); + } + + #[tokio::test] + async fn test_parse_invalid_seed_peers() { + // Test JSON with some invalid peers + let json_content = r#"{ + "peer_seeds": [ + "4cdfb70e0b38b60c6a3573b2870e32bc3d846419c606ea379f43650b80f38409::/ip4/51.83.4.85/tcp/18189", + "invalid_public_key::/ip4/1.2.3.4/tcp/12345", + "1e08628960f75b7e324f010b2ee609a9e28097e9101f4d769d474a38b6ee2d76::/invalid_address", + "1e08628960f75b7e324f010b2ee609a9e28097e9101f4d769d474a38b6ee2d76::/ip4/51.83.102.25/tcp/18189" + ] + }"#; + + #[derive(serde::Deserialize)] + struct SeedNodesJson { + peer_seeds: Vec, + } + + let seed_nodes: SeedNodesJson = serde_json::from_str(json_content).unwrap(); + assert_eq!(seed_nodes.peer_seeds.len(), 4); + + // Parse peers, skipping invalid ones + let mut peers = Vec::new(); + let mut invalid_count = 0; + for peer_str in seed_nodes.peer_seeds { + match peer_str.parse::() { + Ok(peer) => peers.push(peer), + Err(_) => invalid_count += 1, + } + } + + // Should have 2 valid peers and 2 invalid ones + assert_eq!(peers.len(), 2); + assert_eq!(invalid_count, 2); + } + + #[tokio::test] + async fn test_signature_verification_with_actual_key() { + // Test with the actual public key for seed peers HTTP download + const SEED_PEERS_PUBLIC_KEY: &str = r#"-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEaLl/nhYJKwYBBAHaRw8BAQdAocXM74pI54REY9Y0fESxir/iq8We9wp6JHFP +z8vcdm20Sk1hY2llaiAoVGVzdCBmb3Igc2VlZCBwZWVycyBIVFRQIGRvd25sb2Fk +KSA8bWFjaWVqLmtvenVzemVrQHNwYWNlaW5jaC5jb20+iJMEExYKADsWIQTaz8Pe +9KT58ia7xIFrHRtevPqxvwUCaLl/ngIbAwULCQgHAgIiAgYVCgkICwIEFgIDAQIe +BwIXgAAKCRBrHRtevPqxv5cOAQDR1jrEiLxlsEFLsI6DLd0I7SRQDw+tziT/02ed +7E8wMQD/ZzdO7ZO8oLfneJrrwoWiGk241+yq7ym5uEcBuhnKyQ8= +=rjiS +-----END PGP PUBLIC KEY BLOCK-----"#; + + use pgp::types::PublicKeyTrait; + use pgp::Deserializable; + + // Parse the public key + let (key, _) = pgp::SignedPublicKey::from_string(SEED_PEERS_PUBLIC_KEY).unwrap(); + + // The key should be valid - just verify it can be parsed + assert!(!key.primary_key.key_id().to_vec().is_empty()); + } + + #[tokio::test] + async fn test_empty_seed_peers_json() { + let json_content = r#"{ + "peer_seeds": [] + }"#; + + #[derive(serde::Deserialize)] + struct SeedNodesJson { + peer_seeds: Vec, + } + + let seed_nodes: SeedNodesJson = serde_json::from_str(json_content).unwrap(); + assert_eq!(seed_nodes.peer_seeds.len(), 0); + + let peers: Vec = seed_nodes + .peer_seeds + .into_iter() + .filter_map(|s| s.parse::().ok()) + .collect(); + + assert_eq!(peers.len(), 0); + } } diff --git a/base_layer/p2p/src/signature_verification/gpg_keys/seed_peers_http.asc b/base_layer/p2p/src/signature_verification/gpg_keys/seed_peers_http.asc new file mode 100644 index 00000000000..dfbc941c507 --- /dev/null +++ b/base_layer/p2p/src/signature_verification/gpg_keys/seed_peers_http.asc @@ -0,0 +1,10 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEaLl/nhYJKwYBBAHaRw8BAQdAocXM74pI54REY9Y0fESxir/iq8We9wp6JHFP +z8vcdm20Sk1hY2llaiAoVGVzdCBmb3Igc2VlZCBwZWVycyBIVFRQIGRvd25sb2Fk +KSA8bWFjaWVqLmtvenVzemVrQHNwYWNlaW5jaC5jb20+iJMEExYKADsWIQTaz8Pe +9KT58ia7xIFrHRtevPqxvwUCaLl/ngIbAwULCQgHAgIiAgYVCgkICwIEFgIDAQIe +BwIXgAAKCRBrHRtevPqxv5cOAQDR1jrEiLxlsEFLsI6DLd0I7SRQDw+tziT/02ed +7E8wMQD/ZzdO7ZO8oLfneJrrwoWiGk241+yq7ym5uEcBuhnKyQ8= +=rjiS +-----END PGP PUBLIC KEY BLOCK----- diff --git a/base_layer/p2p/src/signature_verification/mod.rs b/base_layer/p2p/src/signature_verification/mod.rs index 73bb0519d20..a26fd516548 100644 --- a/base_layer/p2p/src/signature_verification/mod.rs +++ b/base_layer/p2p/src/signature_verification/mod.rs @@ -25,40 +25,38 @@ mod verifier; pub use error::SignatureVerificationError; use futures; -use pgp::Deserializable; -use reqwest::IntoUrl; +use pgp::SignedPublicKey; +use reqwest::{self, IntoUrl}; use std::io; pub use verifier::SignedMessageVerifier; const LOG_TARGET: &str = "p2p::signature_verification"; // Include GPG keys of authorized maintainers -const MAINTAINERS: &[&str] = &[include_str!("gpg_keys/swvheerden.asc")]; +const MAINTAINERS: &[&str] = &[ + include_str!("gpg_keys/swvheerden.asc"), + include_str!("gpg_keys/seed_peers_http.asc"), +]; /// Returns an iterator over all configured maintainer public keys -pub fn maintainers() -> impl Iterator { +pub fn maintainers() -> impl Iterator { MAINTAINERS.iter().map(|s| { - let (pk, _) = pgp::SignedPublicKey::from_string(s).expect("Malformed maintainer PGP signature"); + let (pk, _) = SignedPublicKey::from_string(s).expect("Malformed maintainer PGP signature"); pk }) } -/// Download a text file from the given URL +// Legacy function names kept for backward compatibility with auto_update module +/// Download a text file from the given URL (legacy name for compatibility) pub async fn download_hashes_file(url: T) -> Result { - let resp = http_download(url).await?; - let txt = resp.text().await?; - Ok(txt) + download_file(url).await } -/// Download a PGP signature file from the given URL +/// Download a PGP signature file from the given URL (legacy name for compatibility) pub async fn download_hashes_sig_file( url: T, ) -> Result { - let resp = http_download(url).await?; - let sig_bytes = resp.bytes().await?; - let cursor = io::Cursor::new(&sig_bytes); - let sig = pgp::StandaloneSignature::from_bytes(cursor).map_err(SignatureVerificationError::SignatureError)?; - Ok(sig) + download_signature_file(url).await } /// Perform an HTTP GET request and return the response @@ -79,10 +77,7 @@ pub async fn verify_signed_hash_file( target_hash: &[u8], ) -> Result<(Vec, String), SignatureVerificationError> { // Download both files in parallel - let (hashes, sig) = futures::join!( - download_hashes_file(hashes_url), - download_hashes_sig_file(signature_url) - ); + let (hashes, sig) = futures::join!(download_file(hashes_url), download_signature_file(signature_url)); let hashes = hashes?; let sig = sig?; @@ -92,6 +87,55 @@ pub async fn verify_signed_hash_file( verifier.verify_signed_hashes(&sig, &hashes, target_hash) } +/// Download and verify a generic file with its PGP signature +/// +/// This function: +/// 1. Downloads the file and its signature +/// 2. Verifies the signature using maintainer keys +/// 3. Returns the file content if verification succeeds +/// +/// # Example +/// ```no_run +/// # async fn example() -> Result<(), Box> { +/// let content = verify_signed_file( +/// "https://example.com/seednodes.json", +/// "https://example.com/seednodes.json.asc" +/// ).await?; +/// # Ok(()) +/// # } +/// ``` +pub async fn verify_signed_file(file_url: &str, signature_url: &str) -> Result { + // Download both files in parallel + let (content, sig) = futures::join!(download_file(file_url), download_signature_file(signature_url)); + + let content = content?; + let sig = sig?; + + // Verify the signature using maintainer keys + let verifier = SignedMessageVerifier::new(maintainers().collect()); + verifier.verify_file_signature(&sig, &content)?; + + Ok(content) +} + +/// Download a text file from the given URL +pub async fn download_file(url: T) -> Result { + let resp = http_download(url).await?; + let txt = resp.text().await?; + Ok(txt) +} + +/// Download a PGP signature file from the given URL +pub async fn download_signature_file( + url: T, +) -> Result { + let resp = http_download(url).await?; + let sig_bytes = resp.bytes().await?; + let cursor = io::Cursor::new(&sig_bytes); + let sig = pgp::StandaloneSignature::from_bytes(cursor).map_err(SignatureVerificationError::SignatureError)?; + Ok(sig) +} + #[cfg(test)] mod test { use super::*; diff --git a/base_layer/p2p/src/signature_verification/verifier.rs b/base_layer/p2p/src/signature_verification/verifier.rs index 9d119deaa6c..c8fbe31ea7e 100644 --- a/base_layer/p2p/src/signature_verification/verifier.rs +++ b/base_layer/p2p/src/signature_verification/verifier.rs @@ -44,7 +44,19 @@ impl SignedMessageVerifier { .find(|pk| signature.verify(pk, message.as_bytes()).is_ok()) } + /// Verify a file's content against its signature + /// Returns Ok with the signing key if verification succeeds + pub fn verify_file_signature( + &self, + signature: &pgp::StandaloneSignature, + file_content: &str, + ) -> Result<&pgp::SignedPublicKey, SignatureVerificationError> { + self.verify_signature(signature, file_content) + .ok_or(SignatureVerificationError::VerificationFailed) + } + /// Verify a signed hash file and return the matching hash and filename for a given target hash + /// This function expects the file to contain lines in the format: "HASH filename" pub fn verify_signed_hashes( &self, signature: &pgp::StandaloneSignature, @@ -162,4 +174,81 @@ l9smp8LtJcXkw4cNgE4MB9VKdx+NhdbvWemt7ccldeL22hmyS24= let verifier = SignedMessageVerifier::new(vec![key]); assert!(verifier.verify_signature(&sig, "Zilip R. Phimmermann").is_none()); } + + #[test] + fn it_verifies_file_signature() { + let (sig, _) = pgp::StandaloneSignature::from_string(VALID_SIGNATURE.trim()).unwrap(); + let (key, _) = pgp::SignedPublicKey::from_string(PUBLIC_KEY).unwrap(); + let verifier = SignedMessageVerifier::new(vec![key]); + + // Test valid file signature + let result = verifier.verify_file_signature(&sig, MESSAGE); + assert!(result.is_ok()); + + // Test invalid file signature + let result = verifier.verify_file_signature(&sig, "Wrong content"); + assert!(result.is_err()); + } + + #[test] + fn test_verify_json_file() { + // Test that we can verify a JSON file content + let json_content = r#"{"peer_seeds": ["node1", "node2"]}"#; + let (key, _) = pgp::SignedPublicKey::from_string(PUBLIC_KEY).unwrap(); + let verifier = SignedMessageVerifier::new(vec![key]); + + // This would work with a real signature of the JSON content + // For now, we just test that the function accepts JSON strings + let (sig, _) = pgp::StandaloneSignature::from_string(VALID_SIGNATURE.trim()).unwrap(); + let result = verifier.verify_file_signature(&sig, json_content); + // This will fail because the signature is for a different message + assert!(result.is_err()); + } + + #[test] + fn test_seed_peers_http_key() { + // Test that the seed_peers_http key can be parsed and used for verification + const SEED_PEERS_KEY: &str = r#"-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEaLl/nhYJKwYBBAHaRw8BAQdAocXM74pI54REY9Y0fESxir/iq8We9wp6JHFP +z8vcdm20Sk1hY2llaiAoVGVzdCBmb3Igc2VlZCBwZWVycyBIVFRQIGRvd25sb2Fk +KSA8bWFjaWVqLmtvenVzemVrQHNwYWNlaW5jaS5jb20+iJMEExYKADsWIQTaz8Pe +9KT58ia7xIFrHRtevPqxvwUCaLl/ngIbAwULCQgHAgIiAgYVCgkICwIEFgIDAQIe +BwIXgAAKCRBrHRtevPqxv5cOAQDR1jrEiLxlsEFLsI6DLd0I7SRQDw+tziT/02ed +7E8wMQD/ZzdO7ZO8oLfneJrrwoWiGk241+yq7ym5uEcBuhnKyQ8= +=rjiS +-----END PGP PUBLIC KEY BLOCK-----"#; + + // Parse the key + let (key, _) = pgp::SignedPublicKey::from_string(SEED_PEERS_KEY).unwrap(); + + // Create verifier with the seed peers key + let verifier = SignedMessageVerifier::new(vec![key]); + + // The verifier should be created successfully + assert_eq!(verifier.maintainers.len(), 1); + } + + #[test] + fn test_multiple_maintainer_keys() { + // Test with both the original test key and the seed peers key + const SEED_PEERS_KEY: &str = r#"-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEaLl/nhYJKwYBBAHaRw8BAQdAocXM74pI54REY9Y0fESxir/iq8We9wp6JHFP +z8vcdm20Sk1hY2llaiAoVGVzdCBmb3Igc2VlZCBwZWVycyBIVFRQIGRvd25sb2Fk +KSA8bWFjaWVqLmtvenVzemVrQHNwYWNlaW5jaC5jb20+iJMEExYKADsWIQTaz8Pe +9KT58ia7xIFrHRtevPqxvwUCaLl/ngIbAwULCQgHAgIiAgYVCgkICwIEFgIDAQIe +BwIXgAAKCRBrHRtevPqxv5cOAQDR1jrEiLxlsEFLsI6DLd0I7SRQDw+tziT/02ed +7E8wMQD/ZzdO7ZO8oLfneJrrwoWiGk241+yq7ym5uEcBuhnKyQ8= +=rjiS +-----END PGP PUBLIC KEY BLOCK-----"#; + + let (key1, _) = pgp::SignedPublicKey::from_string(PUBLIC_KEY).unwrap(); + let (key2, _) = pgp::SignedPublicKey::from_string(SEED_PEERS_KEY).unwrap(); + + let verifier = SignedMessageVerifier::new(vec![key1, key2]); + + // Should have both keys + assert_eq!(verifier.maintainers.len(), 2); + } } From f5f3ef20b7435817983e967a50cf5fb7002685e4 Mon Sep 17 00:00:00 2001 From: Maciej Kozuszek Date: Thu, 4 Sep 2025 21:23:40 +0200 Subject: [PATCH 03/12] Remove feature gating for pgp and reqwest --- base_layer/p2p/Cargo.toml | 6 +-- base_layer/p2p/src/auto_update/error.rs | 4 +- base_layer/p2p/src/auto_update/mod.rs | 9 ++-- base_layer/p2p/src/initialization.rs | 34 +++++++++++---- .../p2p/src/signature_verification/mod.rs | 22 +++++----- .../src/signature_verification/verifier.rs | 43 +++++++++---------- 6 files changed, 66 insertions(+), 52 deletions(-) diff --git a/base_layer/p2p/Cargo.toml b/base_layer/p2p/Cargo.toml index c588f5c80da..48bb7666c43 100644 --- a/base_layer/p2p/Cargo.toml +++ b/base_layer/p2p/Cargo.toml @@ -20,10 +20,10 @@ tari_utilities = { workspace = true } anyhow = "1.0.53" futures = { version = "^0.3.1" } log = "0.4.6" -pgp = { version = "0.14.2", optional = true } +pgp = { version = "0.14.2" } prost = "0.13.3" rand = "0.8" -reqwest = { version = "0.11", optional = true, default-features = false } +reqwest = { version = "0.11" } semver = { version = "1.0.1", optional = true } serde = "1.0.90" serde_json = "1.0.51" @@ -53,7 +53,7 @@ tari_common = { workspace = true, features = ["build"] } [features] test-mocks = [] -auto-update = ["reqwest/default", "pgp", "semver"] +auto-update = ["semver"] [package.metadata.cargo-machete] ignored = [] diff --git a/base_layer/p2p/src/auto_update/error.rs b/base_layer/p2p/src/auto_update/error.rs index 485e8e8286c..62bbede2750 100644 --- a/base_layer/p2p/src/auto_update/error.rs +++ b/base_layer/p2p/src/auto_update/error.rs @@ -20,10 +20,10 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use crate::dns::DnsClientError; -use crate::signature_verification::SignatureVerificationError; use thiserror::Error; +use crate::{dns::DnsClientError, signature_verification::SignatureVerificationError}; + #[derive(Debug, Error)] pub enum AutoUpdateError { #[error("DNS Client error: {0}")] diff --git a/base_layer/p2p/src/auto_update/mod.rs b/base_layer/p2p/src/auto_update/mod.rs index de7f367e975..259bcf0104a 100644 --- a/base_layer/p2p/src/auto_update/mod.rs +++ b/base_layer/p2p/src/auto_update/mod.rs @@ -45,12 +45,15 @@ use tari_common::{ utils::{deserialize_string_or_struct, serialize_string}, StringList, }, - DnsNameServer, SubConfigPath, + DnsNameServer, + SubConfigPath, }; use tari_utilities::hex::Hex; -use crate::auto_update::dns::UpdateSpec; -use crate::signature_verification::{self, SignedMessageVerifier}; +use crate::{ + auto_update::dns::UpdateSpec, + signature_verification::{self, SignedMessageVerifier}, +}; const LOG_TARGET: &str = "p2p::auto_update"; diff --git a/base_layer/p2p/src/initialization.rs b/base_layer/p2p/src/initialization.rs index 335f5db92ac..715f603ce46 100644 --- a/base_layer/p2p/src/initialization.rs +++ b/base_layer/p2p/src/initialization.rs @@ -45,21 +45,34 @@ use tari_comms::{ multiaddr::multiaddr, peer_manager::{ database::{PeerDatabaseSql, MIGRATIONS}, - NodeIdentity, Peer, PeerFeatures, PeerFlags, PeerManagerError, + NodeIdentity, + Peer, + PeerFeatures, + PeerFlags, + PeerManagerError, }, pipeline, protocol::{ messaging::{MessagingEventSender, MessagingProtocolExtension}, rpc::RpcServer, - NodeNetworkInfo, ProtocolId, + NodeNetworkInfo, + ProtocolId, }, tor::{self, HiddenServiceControllerError, TorIdentity}, transports::{ - predicate::FalsePredicate, HiddenServiceTransport, MemoryTransport, SocksConfig, SocksTransport, + predicate::FalsePredicate, + HiddenServiceTransport, + MemoryTransport, + SocksConfig, + SocksTransport, TcpWithTorTransport, }, utils::cidr::parse_cidrs, - CommsBuilder, CommsBuilderError, CommsNode, PeerManager, UnspawnedCommsNode, + CommsBuilder, + CommsBuilderError, + CommsNode, + PeerManager, + UnspawnedCommsNode, }; use tari_comms_dht::{Dht, DhtInitializationError}; use tari_service_framework::{async_trait, ServiceInitializationError, ServiceInitializer, ServiceInitializerContext}; @@ -78,7 +91,9 @@ use crate::{ dns::DnsClientError, peer_seeds::{DnsSeedResolver, SeedPeer}, transport::{TorTransportConfig, TransportType}, - TransportConfig, MAJOR_NETWORK_VERSION, MINOR_NETWORK_VERSION, + TransportConfig, + MAJOR_NETWORK_VERSION, + MINOR_NETWORK_VERSION, }; const LOG_TARGET: &str = "p2p::initialization"; @@ -477,9 +492,10 @@ impl P2pInitializer { async fn download_seed_peers_files( (url, addr): (String, String), ) -> Result, ServiceInitializationError> { - use crate::signature_verification::verify_signed_file; use serde::Deserialize; + use crate::signature_verification::verify_signed_file; + #[derive(Deserialize)] struct SeedNodesJson { peer_seeds: Vec, @@ -687,10 +703,11 @@ impl ServiceInitializer for P2pInitializer { #[cfg(test)] mod test { - use super::*; use tari_common::configuration::Network; use tari_comms::connection_manager::WireMode; + use super::*; + #[test] fn self_liveness_network_wire_byte_is_consistent() { let wire_mode = WireMode::Liveness; @@ -824,8 +841,7 @@ BwIXgAAKCRBrHRtevPqxv5cOAQDR1jrEiLxlsEFLsI6DLd0I7SRQDw+tziT/02ed =rjiS -----END PGP PUBLIC KEY BLOCK-----"#; - use pgp::types::PublicKeyTrait; - use pgp::Deserializable; + use pgp::{types::PublicKeyTrait, Deserializable}; // Parse the public key let (key, _) = pgp::SignedPublicKey::from_string(SEED_PEERS_PUBLIC_KEY).unwrap(); diff --git a/base_layer/p2p/src/signature_verification/mod.rs b/base_layer/p2p/src/signature_verification/mod.rs index a26fd516548..79c78617703 100644 --- a/base_layer/p2p/src/signature_verification/mod.rs +++ b/base_layer/p2p/src/signature_verification/mod.rs @@ -23,11 +23,12 @@ mod error; mod verifier; +use std::io; + pub use error::SignatureVerificationError; use futures; -use pgp::SignedPublicKey; -use reqwest::{self, IntoUrl}; -use std::io; +use pgp::{Deserializable, SignedPublicKey, StandaloneSignature}; +use reqwest::IntoUrl; pub use verifier::SignedMessageVerifier; const LOG_TARGET: &str = "p2p::signature_verification"; @@ -53,9 +54,7 @@ pub async fn download_hashes_file(url: T) -> Result( - url: T, -) -> Result { +pub async fn download_hashes_sig_file(url: T) -> Result { download_signature_file(url).await } @@ -99,8 +98,9 @@ pub async fn verify_signed_hash_file( /// # async fn example() -> Result<(), Box> { /// let content = verify_signed_file( /// "https://example.com/seednodes.json", -/// "https://example.com/seednodes.json.asc" -/// ).await?; +/// "https://example.com/seednodes.json.asc", +/// ) +/// .await?; /// # Ok(()) /// # } /// ``` @@ -126,13 +126,11 @@ pub async fn download_file(url: T) -> Result( - url: T, -) -> Result { +pub async fn download_signature_file(url: T) -> Result { let resp = http_download(url).await?; let sig_bytes = resp.bytes().await?; let cursor = io::Cursor::new(&sig_bytes); - let sig = pgp::StandaloneSignature::from_bytes(cursor).map_err(SignatureVerificationError::SignatureError)?; + let sig = StandaloneSignature::from_bytes(cursor).map_err(SignatureVerificationError::SignatureError)?; Ok(sig) } diff --git a/base_layer/p2p/src/signature_verification/verifier.rs b/base_layer/p2p/src/signature_verification/verifier.rs index c8fbe31ea7e..7a329ae083c 100644 --- a/base_layer/p2p/src/signature_verification/verifier.rs +++ b/base_layer/p2p/src/signature_verification/verifier.rs @@ -20,25 +20,22 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +use pgp::{SignedPublicKey, StandaloneSignature}; use tari_utilities::hex::from_hex; use crate::signature_verification::error::SignatureVerificationError; pub struct SignedMessageVerifier { - maintainers: Vec, + maintainers: Vec, } impl SignedMessageVerifier { - pub fn new(maintainers: Vec) -> Self { + pub fn new(maintainers: Vec) -> Self { Self { maintainers } } /// Verify a standalone signature against a message using the configured maintainers' public keys - pub fn verify_signature( - &self, - signature: &pgp::StandaloneSignature, - message: &str, - ) -> Option<&pgp::SignedPublicKey> { + pub fn verify_signature(&self, signature: &StandaloneSignature, message: &str) -> Option<&SignedPublicKey> { self.maintainers .iter() .find(|pk| signature.verify(pk, message.as_bytes()).is_ok()) @@ -48,9 +45,9 @@ impl SignedMessageVerifier { /// Returns Ok with the signing key if verification succeeds pub fn verify_file_signature( &self, - signature: &pgp::StandaloneSignature, + signature: &StandaloneSignature, file_content: &str, - ) -> Result<&pgp::SignedPublicKey, SignatureVerificationError> { + ) -> Result<&SignedPublicKey, SignatureVerificationError> { self.verify_signature(signature, file_content) .ok_or(SignatureVerificationError::VerificationFailed) } @@ -59,7 +56,7 @@ impl SignedMessageVerifier { /// This function expects the file to contain lines in the format: "HASH filename" pub fn verify_signed_hashes( &self, - signature: &pgp::StandaloneSignature, + signature: &StandaloneSignature, hashes: &str, target_hash: &[u8], ) -> Result<(Vec, String), SignatureVerificationError> { @@ -81,7 +78,7 @@ impl SignedMessageVerifier { #[cfg(test)] mod test { - use pgp::Deserializable; + use pgp::{Deserializable, StandaloneSignature}; use super::*; @@ -158,27 +155,27 @@ l9smp8LtJcXkw4cNgE4MB9VKdx+NhdbvWemt7ccldeL22hmyS24= #[test] fn it_verifies_signed_message() { - let (sig, _) = pgp::StandaloneSignature::from_string(VALID_SIGNATURE.trim()).unwrap(); - let (key, _) = pgp::SignedPublicKey::from_string(PUBLIC_KEY).unwrap(); + let (sig, _) = StandaloneSignature::from_string(VALID_SIGNATURE.trim()).unwrap(); + let (key, _) = SignedPublicKey::from_string(PUBLIC_KEY).unwrap(); let verifier = SignedMessageVerifier::new(vec![key]); let signer = verifier.verify_signature(&sig, MESSAGE).unwrap(); - let (maintainer, _) = pgp::SignedPublicKey::from_string(PUBLIC_KEY).unwrap(); + let (maintainer, _) = SignedPublicKey::from_string(PUBLIC_KEY).unwrap(); assert_eq!(*signer, maintainer); } #[test] fn it_does_not_validate_with_tampered_message() { - let (sig, _) = pgp::StandaloneSignature::from_string(VALID_SIGNATURE.trim()).unwrap(); - let (key, _) = pgp::SignedPublicKey::from_string(PUBLIC_KEY).unwrap(); + let (sig, _) = StandaloneSignature::from_string(VALID_SIGNATURE.trim()).unwrap(); + let (key, _) = SignedPublicKey::from_string(PUBLIC_KEY).unwrap(); let verifier = SignedMessageVerifier::new(vec![key]); assert!(verifier.verify_signature(&sig, "Zilip R. Phimmermann").is_none()); } #[test] fn it_verifies_file_signature() { - let (sig, _) = pgp::StandaloneSignature::from_string(VALID_SIGNATURE.trim()).unwrap(); - let (key, _) = pgp::SignedPublicKey::from_string(PUBLIC_KEY).unwrap(); + let (sig, _) = StandaloneSignature::from_string(VALID_SIGNATURE.trim()).unwrap(); + let (key, _) = SignedPublicKey::from_string(PUBLIC_KEY).unwrap(); let verifier = SignedMessageVerifier::new(vec![key]); // Test valid file signature @@ -194,12 +191,12 @@ l9smp8LtJcXkw4cNgE4MB9VKdx+NhdbvWemt7ccldeL22hmyS24= fn test_verify_json_file() { // Test that we can verify a JSON file content let json_content = r#"{"peer_seeds": ["node1", "node2"]}"#; - let (key, _) = pgp::SignedPublicKey::from_string(PUBLIC_KEY).unwrap(); + let (key, _) = SignedPublicKey::from_string(PUBLIC_KEY).unwrap(); let verifier = SignedMessageVerifier::new(vec![key]); // This would work with a real signature of the JSON content // For now, we just test that the function accepts JSON strings - let (sig, _) = pgp::StandaloneSignature::from_string(VALID_SIGNATURE.trim()).unwrap(); + let (sig, _) = StandaloneSignature::from_string(VALID_SIGNATURE.trim()).unwrap(); let result = verifier.verify_file_signature(&sig, json_content); // This will fail because the signature is for a different message assert!(result.is_err()); @@ -220,7 +217,7 @@ BwIXgAAKCRBrHRtevPqxv5cOAQDR1jrEiLxlsEFLsI6DLd0I7SRQDw+tziT/02ed -----END PGP PUBLIC KEY BLOCK-----"#; // Parse the key - let (key, _) = pgp::SignedPublicKey::from_string(SEED_PEERS_KEY).unwrap(); + let (key, _) = SignedPublicKey::from_string(SEED_PEERS_KEY).unwrap(); // Create verifier with the seed peers key let verifier = SignedMessageVerifier::new(vec![key]); @@ -243,8 +240,8 @@ BwIXgAAKCRBrHRtevPqxv5cOAQDR1jrEiLxlsEFLsI6DLd0I7SRQDw+tziT/02ed =rjiS -----END PGP PUBLIC KEY BLOCK-----"#; - let (key1, _) = pgp::SignedPublicKey::from_string(PUBLIC_KEY).unwrap(); - let (key2, _) = pgp::SignedPublicKey::from_string(SEED_PEERS_KEY).unwrap(); + let (key1, _) = SignedPublicKey::from_string(PUBLIC_KEY).unwrap(); + let (key2, _) = SignedPublicKey::from_string(SEED_PEERS_KEY).unwrap(); let verifier = SignedMessageVerifier::new(vec![key1, key2]); From accbbba5c84949ba34606af75be01fb360e6fdb5 Mon Sep 17 00:00:00 2001 From: Maciej Kozuszek Date: Fri, 5 Sep 2025 16:06:52 +0200 Subject: [PATCH 04/12] Fix signature verification --- .../gpg_keys/seed_peers_http.asc | 9 ++- .../p2p/src/signature_verification/mod.rs | 72 +++++++++++++++++-- .../src/signature_verification/verifier.rs | 57 ++++++++++++--- 3 files changed, 120 insertions(+), 18 deletions(-) diff --git a/base_layer/p2p/src/signature_verification/gpg_keys/seed_peers_http.asc b/base_layer/p2p/src/signature_verification/gpg_keys/seed_peers_http.asc index dfbc941c507..697eb4b60bd 100644 --- a/base_layer/p2p/src/signature_verification/gpg_keys/seed_peers_http.asc +++ b/base_layer/p2p/src/signature_verification/gpg_keys/seed_peers_http.asc @@ -5,6 +5,11 @@ z8vcdm20Sk1hY2llaiAoVGVzdCBmb3Igc2VlZCBwZWVycyBIVFRQIGRvd25sb2Fk KSA8bWFjaWVqLmtvenVzemVrQHNwYWNlaW5jaC5jb20+iJMEExYKADsWIQTaz8Pe 9KT58ia7xIFrHRtevPqxvwUCaLl/ngIbAwULCQgHAgIiAgYVCgkICwIEFgIDAQIe BwIXgAAKCRBrHRtevPqxv5cOAQDR1jrEiLxlsEFLsI6DLd0I7SRQDw+tziT/02ed -7E8wMQD/ZzdO7ZO8oLfneJrrwoWiGk241+yq7ym5uEcBuhnKyQ8= -=rjiS +7E8wMQD/ZzdO7ZO8oLfneJrrwoWiGk241+yq7ym5uEcBuhnKyQ+0Uk1hY2llaiBL +b3p1c3playAoVGVzdGluZyBzZWVkIHBlZXJzIEhUVFAgZG93bmxvYWQpIDxtYWNp +ZWoua296dXN6ZWtAc3BhY2VpbmNoLmNvbT6IkwQTFgoAOxYhBNrPw970pPnyJrvE +gWsdG168+rG/BQJousXyAhsDBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJ +EGsdG168+rG/zIgA/RmbnuNU7/mrSIqV62U5wPPhj3fT7+zR/9Ayn1ME+KbMAQC8 +fSb4bOv48TUskOtzWd9j+AuH+2w1bvi9/niKAPgrAA== +=p4US -----END PGP PUBLIC KEY BLOCK----- diff --git a/base_layer/p2p/src/signature_verification/mod.rs b/base_layer/p2p/src/signature_verification/mod.rs index 79c78617703..03941906a54 100644 --- a/base_layer/p2p/src/signature_verification/mod.rs +++ b/base_layer/p2p/src/signature_verification/mod.rs @@ -27,6 +27,7 @@ use std::io; pub use error::SignatureVerificationError; use futures; +use log::{debug, info, warn}; use pgp::{Deserializable, SignedPublicKey, StandaloneSignature}; use reqwest::IntoUrl; pub use verifier::SignedMessageVerifier; @@ -60,7 +61,10 @@ pub async fn download_hashes_sig_file(url: T) -> Result(url: T) -> Result { + eprintln!("[SIGNATURE_VERIFICATION] Starting HTTP download"); + debug!(target: LOG_TARGET, "Starting HTTP download"); let resp = reqwest::get(url).await?.error_for_status()?; + debug!(target: LOG_TARGET, "HTTP download completed successfully"); Ok(resp) } @@ -75,15 +79,32 @@ pub async fn verify_signed_hash_file( signature_url: &str, target_hash: &[u8], ) -> Result<(Vec, String), SignatureVerificationError> { + info!(target: LOG_TARGET, "Starting verify_signed_hash_file for: {} with signature: {}", hashes_url, signature_url); + // Download both files in parallel + debug!(target: LOG_TARGET, "Downloading hashes and signature in parallel"); let (hashes, sig) = futures::join!(download_file(hashes_url), download_signature_file(signature_url)); let hashes = hashes?; let sig = sig?; + info!(target: LOG_TARGET, "Successfully downloaded hashes file and signature"); + debug!(target: LOG_TARGET, "Hashes content length: {} bytes", hashes.len()); // Verify the signature using maintainer keys let verifier = SignedMessageVerifier::new(maintainers().collect()); - verifier.verify_signed_hashes(&sig, &hashes, target_hash) + let result = verifier.verify_signed_hashes(&sig, &hashes, target_hash); + + match &result { + Ok((hash, filename)) => { + info!(target: LOG_TARGET, "Signature verification successful for file: {}", filename); + debug!(target: LOG_TARGET, "Verified hash: {:?}", hash); + }, + Err(e) => { + warn!(target: LOG_TARGET, "Signature verification failed: {}", e); + }, + } + + result } /// Download and verify a generic file with its PGP signature @@ -105,33 +126,74 @@ pub async fn verify_signed_hash_file( /// # } /// ``` pub async fn verify_signed_file(file_url: &str, signature_url: &str) -> Result { + eprintln!( + "[SIGNATURE_VERIFICATION] Starting verify_signed_file for: {} with signature: {}", + file_url, signature_url + ); + info!(target: LOG_TARGET, "Starting verify_signed_file for: {} with signature: {}", file_url, signature_url); + // Download both files in parallel + eprintln!("[SIGNATURE_VERIFICATION] About to download file and signature in parallel"); + debug!(target: LOG_TARGET, "Downloading file and signature in parallel"); let (content, sig) = futures::join!(download_file(file_url), download_signature_file(signature_url)); let content = content?; let sig = sig?; + info!(target: LOG_TARGET, "Downloaded file: {} and signature: {}", file_url, signature_url); + debug!(target: LOG_TARGET, "File content length: {} bytes", content.len()); // Verify the signature using maintainer keys let verifier = SignedMessageVerifier::new(maintainers().collect()); - verifier.verify_file_signature(&sig, &content)?; - Ok(content) + match verifier.verify_file_signature(&sig, &content) { + Ok(_) => { + info!(target: LOG_TARGET, "Signature verification successful for file: {}", file_url); + Ok(content) + }, + Err(e) => { + warn!(target: LOG_TARGET, "Signature verification failed for {}: {}", file_url, e); + Err(e) + }, + } } /// Download a text file from the given URL pub async fn download_file(url: T) -> Result { + eprintln!("[SIGNATURE_VERIFICATION] download_file: Starting text file download"); + info!(target: LOG_TARGET, "download_file: Starting text file download"); let resp = http_download(url).await?; let txt = resp.text().await?; + info!(target: LOG_TARGET, "download_file: Downloaded text file, size: {} bytes", txt.len()); Ok(txt) } /// Download a PGP signature file from the given URL pub async fn download_signature_file(url: T) -> Result { + eprintln!("[SIGNATURE_VERIFICATION] download_signature_file: Starting signature file download"); + info!(target: LOG_TARGET, "download_signature_file: Starting signature file download"); let resp = http_download(url).await?; let sig_bytes = resp.bytes().await?; + eprintln!( + "[SIGNATURE_VERIFICATION] download_signature_file: Downloaded signature, size: {} bytes", + sig_bytes.len() + ); + info!(target: LOG_TARGET, "download_signature_file: Downloaded signature, size: {} bytes", sig_bytes.len()); let cursor = io::Cursor::new(&sig_bytes); - let sig = StandaloneSignature::from_bytes(cursor).map_err(SignatureVerificationError::SignatureError)?; - Ok(sig) + eprintln!("[SIGNATURE_VERIFICATION] About to parse PGP signature as binary"); + match StandaloneSignature::from_bytes(cursor) { + Ok(sig) => { + info!(target: LOG_TARGET, "download_signature_file: Successfully parsed PGP signature"); + Ok(sig) + }, + Err(e) => { + eprintln!( + "[SIGNATURE_VERIFICATION] download_signature_file: Failed to parse PGP signature: {}", + e + ); + warn!(target: LOG_TARGET, "download_signature_file: Failed to parse PGP signature: {}", e); + Err(SignatureVerificationError::SignatureError(e)) + }, + } } #[cfg(test)] diff --git a/base_layer/p2p/src/signature_verification/verifier.rs b/base_layer/p2p/src/signature_verification/verifier.rs index 7a329ae083c..b679ab7b678 100644 --- a/base_layer/p2p/src/signature_verification/verifier.rs +++ b/base_layer/p2p/src/signature_verification/verifier.rs @@ -20,11 +20,14 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use pgp::{SignedPublicKey, StandaloneSignature}; +use log::{debug, info, warn}; +use pgp::{types::PublicKeyTrait, SignedPublicKey, StandaloneSignature}; use tari_utilities::hex::from_hex; use crate::signature_verification::error::SignatureVerificationError; +const LOG_TARGET: &str = "p2p::signature_verification::verifier"; + pub struct SignedMessageVerifier { maintainers: Vec, } @@ -36,9 +39,17 @@ impl SignedMessageVerifier { /// Verify a standalone signature against a message using the configured maintainers' public keys pub fn verify_signature(&self, signature: &StandaloneSignature, message: &str) -> Option<&SignedPublicKey> { - self.maintainers - .iter() - .find(|pk| signature.verify(pk, message.as_bytes()).is_ok()) + info!(target: LOG_TARGET, "Starting signature verification, maintainers count: {}", self.maintainers.len()); + self.maintainers.iter().find(|pk| { + debug!(target: LOG_TARGET, "Verifying signature with public key fingerprint: {:?}", pk.fingerprint()); + let result = signature.verify(pk, message.as_bytes()).is_ok(); + if result { + info!(target: LOG_TARGET, "Signature verified successfully with key: {:?}", pk.fingerprint()); + } else { + debug!(target: LOG_TARGET, "Signature verification failed with key: {:?}", pk.fingerprint()); + } + result + }) } /// Verify a file's content against its signature @@ -48,8 +59,11 @@ impl SignedMessageVerifier { signature: &StandaloneSignature, file_content: &str, ) -> Result<&SignedPublicKey, SignatureVerificationError> { - self.verify_signature(signature, file_content) - .ok_or(SignatureVerificationError::VerificationFailed) + debug!(target: LOG_TARGET, "Verifying file signature, content length: {} bytes", file_content.len()); + self.verify_signature(signature, file_content).ok_or_else(|| { + warn!(target: LOG_TARGET, "File signature verification failed - no matching maintainer key found"); + SignatureVerificationError::VerificationFailed + }) } /// Verify a signed hash file and return the matching hash and filename for a given target hash @@ -60,10 +74,16 @@ impl SignedMessageVerifier { hashes: &str, target_hash: &[u8], ) -> Result<(Vec, String), SignatureVerificationError> { - self.verify_signature(signature, hashes) - .ok_or(SignatureVerificationError::VerificationFailed)?; + info!(target: LOG_TARGET, "Verifying signed hashes file, looking for target hash"); + debug!(target: LOG_TARGET, "Target hash: {:?}", target_hash); + debug!(target: LOG_TARGET, "Hashes file content length: {} bytes", hashes.len()); + + self.verify_signature(signature, hashes).ok_or_else(|| { + warn!(target: LOG_TARGET, "Hash file signature verification failed - no matching maintainer key found"); + SignatureVerificationError::VerificationFailed + })?; - hashes + let parsed_hashes: Vec<(Vec, String)> = hashes .lines() .filter_map(|line| { let mut parts = line.splitn(2, ' '); @@ -71,8 +91,23 @@ impl SignedMessageVerifier { let filename = parts.next()?; Some((hash, filename.trim().to_string())) }) - .find(|(hash, _)| *hash == target_hash) - .ok_or(SignatureVerificationError::InvalidHashFormat) + .collect(); + + debug!(target: LOG_TARGET, "Parsed {} hash entries from file", parsed_hashes.len()); + + parsed_hashes + .into_iter() + .find(|(hash, filename)| { + let matches = *hash == target_hash; + if matches { + info!(target: LOG_TARGET, "Found matching hash for file: {}", filename); + } + matches + }) + .ok_or_else(|| { + warn!(target: LOG_TARGET, "No matching hash found in the signed hashes file"); + SignatureVerificationError::InvalidHashFormat + }) } } From a5fb753b33e4b9c1b394fb60638315e64aa77a70 Mon Sep 17 00:00:00 2001 From: Maciej Kozuszek Date: Fri, 5 Sep 2025 16:53:58 +0200 Subject: [PATCH 05/12] Remove debug logs and fix linting --- base_layer/p2p/src/initialization.rs | 17 +++--- .../p2p/src/signature_verification/mod.rs | 52 ++----------------- .../src/signature_verification/verifier.rs | 17 ++---- comms/core/src/builder/placeholder.rs | 1 + 4 files changed, 20 insertions(+), 67 deletions(-) diff --git a/base_layer/p2p/src/initialization.rs b/base_layer/p2p/src/initialization.rs index 715f603ce46..eb61b74b1b7 100644 --- a/base_layer/p2p/src/initialization.rs +++ b/base_layer/p2p/src/initialization.rs @@ -747,29 +747,32 @@ mod test { assert_eq!(peers.len(), 6); // Verify the first peer (IPv4) - let first_peer = &peers[0]; + let first_peer = &peers.first().unwrap(); assert_eq!( first_peer.public_key.to_hex(), "4cdfb70e0b38b60c6a3573b2870e32bc3d846419c606ea379f43650b80f38409" ); assert_eq!(first_peer.addresses.len(), 1); - assert_eq!(first_peer.addresses[0].to_string(), "/ip4/51.83.4.85/tcp/18189"); + assert_eq!( + first_peer.addresses.first().unwrap().to_string(), + "/ip4/51.83.4.85/tcp/18189" + ); // Verify an IPv6 peer - let ipv6_peer = &peers[2]; + let ipv6_peer = &peers.get(2).unwrap(); assert_eq!( ipv6_peer.public_key.to_hex(), "1e08628960f75b7e324f010b2ee609a9e28097e9101f4d769d474a38b6ee2d76" ); assert_eq!( - ipv6_peer.addresses[0].to_string(), + ipv6_peer.addresses.first().unwrap().to_string(), "/ip6/2001:41d0:303:a619::1/tcp/18189" ); // Verify an onion peer - let onion_peer = &peers[4]; + let onion_peer = &peers.get(4).unwrap(); assert_eq!( - onion_peer.addresses[0].to_string(), + onion_peer.addresses.first().unwrap().to_string(), "/onion3/tadnxyokalnqjtvu6mlhxndcq4v2tlolotpvrflscdmi7lcautao3had:18141" ); } @@ -785,7 +788,7 @@ mod test { assert_eq!(peers.len(), 2); // Verify conversion to Peer works - let first_peer = &peers[0]; + let first_peer = &peers.first().unwrap(); assert_eq!( first_peer.public_key.to_hex(), "4cdfb70e0b38b60c6a3573b2870e32bc3d846419c606ea379f43650b80f38409" diff --git a/base_layer/p2p/src/signature_verification/mod.rs b/base_layer/p2p/src/signature_verification/mod.rs index 03941906a54..706afe842b5 100644 --- a/base_layer/p2p/src/signature_verification/mod.rs +++ b/base_layer/p2p/src/signature_verification/mod.rs @@ -27,7 +27,7 @@ use std::io; pub use error::SignatureVerificationError; use futures; -use log::{debug, info, warn}; +use log::{debug, warn}; use pgp::{Deserializable, SignedPublicKey, StandaloneSignature}; use reqwest::IntoUrl; pub use verifier::SignedMessageVerifier; @@ -61,10 +61,7 @@ pub async fn download_hashes_sig_file(url: T) -> Result(url: T) -> Result { - eprintln!("[SIGNATURE_VERIFICATION] Starting HTTP download"); - debug!(target: LOG_TARGET, "Starting HTTP download"); let resp = reqwest::get(url).await?.error_for_status()?; - debug!(target: LOG_TARGET, "HTTP download completed successfully"); Ok(resp) } @@ -79,25 +76,15 @@ pub async fn verify_signed_hash_file( signature_url: &str, target_hash: &[u8], ) -> Result<(Vec, String), SignatureVerificationError> { - info!(target: LOG_TARGET, "Starting verify_signed_hash_file for: {} with signature: {}", hashes_url, signature_url); - - // Download both files in parallel - debug!(target: LOG_TARGET, "Downloading hashes and signature in parallel"); let (hashes, sig) = futures::join!(download_file(hashes_url), download_signature_file(signature_url)); - let hashes = hashes?; let sig = sig?; - info!(target: LOG_TARGET, "Successfully downloaded hashes file and signature"); - debug!(target: LOG_TARGET, "Hashes content length: {} bytes", hashes.len()); - - // Verify the signature using maintainer keys let verifier = SignedMessageVerifier::new(maintainers().collect()); let result = verifier.verify_signed_hashes(&sig, &hashes, target_hash); match &result { - Ok((hash, filename)) => { - info!(target: LOG_TARGET, "Signature verification successful for file: {}", filename); - debug!(target: LOG_TARGET, "Verified hash: {:?}", hash); + Ok((_hash, filename)) => { + debug!(target: LOG_TARGET, "Signature verification successful for file: {}", filename); }, Err(e) => { warn!(target: LOG_TARGET, "Signature verification failed: {}", e); @@ -126,28 +113,14 @@ pub async fn verify_signed_hash_file( /// # } /// ``` pub async fn verify_signed_file(file_url: &str, signature_url: &str) -> Result { - eprintln!( - "[SIGNATURE_VERIFICATION] Starting verify_signed_file for: {} with signature: {}", - file_url, signature_url - ); - info!(target: LOG_TARGET, "Starting verify_signed_file for: {} with signature: {}", file_url, signature_url); - - // Download both files in parallel - eprintln!("[SIGNATURE_VERIFICATION] About to download file and signature in parallel"); - debug!(target: LOG_TARGET, "Downloading file and signature in parallel"); let (content, sig) = futures::join!(download_file(file_url), download_signature_file(signature_url)); - let content = content?; let sig = sig?; - info!(target: LOG_TARGET, "Downloaded file: {} and signature: {}", file_url, signature_url); - debug!(target: LOG_TARGET, "File content length: {} bytes", content.len()); - - // Verify the signature using maintainer keys let verifier = SignedMessageVerifier::new(maintainers().collect()); match verifier.verify_file_signature(&sig, &content) { Ok(_) => { - info!(target: LOG_TARGET, "Signature verification successful for file: {}", file_url); + debug!(target: LOG_TARGET, "Signature verification successful for file: {}", file_url); Ok(content) }, Err(e) => { @@ -159,37 +132,22 @@ pub async fn verify_signed_file(file_url: &str, signature_url: &str) -> Result(url: T) -> Result { - eprintln!("[SIGNATURE_VERIFICATION] download_file: Starting text file download"); - info!(target: LOG_TARGET, "download_file: Starting text file download"); let resp = http_download(url).await?; let txt = resp.text().await?; - info!(target: LOG_TARGET, "download_file: Downloaded text file, size: {} bytes", txt.len()); Ok(txt) } /// Download a PGP signature file from the given URL pub async fn download_signature_file(url: T) -> Result { - eprintln!("[SIGNATURE_VERIFICATION] download_signature_file: Starting signature file download"); - info!(target: LOG_TARGET, "download_signature_file: Starting signature file download"); let resp = http_download(url).await?; let sig_bytes = resp.bytes().await?; - eprintln!( - "[SIGNATURE_VERIFICATION] download_signature_file: Downloaded signature, size: {} bytes", - sig_bytes.len() - ); - info!(target: LOG_TARGET, "download_signature_file: Downloaded signature, size: {} bytes", sig_bytes.len()); let cursor = io::Cursor::new(&sig_bytes); - eprintln!("[SIGNATURE_VERIFICATION] About to parse PGP signature as binary"); match StandaloneSignature::from_bytes(cursor) { Ok(sig) => { - info!(target: LOG_TARGET, "download_signature_file: Successfully parsed PGP signature"); + debug!(target: LOG_TARGET, "download_signature_file: Successfully parsed PGP signature"); Ok(sig) }, Err(e) => { - eprintln!( - "[SIGNATURE_VERIFICATION] download_signature_file: Failed to parse PGP signature: {}", - e - ); warn!(target: LOG_TARGET, "download_signature_file: Failed to parse PGP signature: {}", e); Err(SignatureVerificationError::SignatureError(e)) }, diff --git a/base_layer/p2p/src/signature_verification/verifier.rs b/base_layer/p2p/src/signature_verification/verifier.rs index b679ab7b678..d4d2ab8d7f9 100644 --- a/base_layer/p2p/src/signature_verification/verifier.rs +++ b/base_layer/p2p/src/signature_verification/verifier.rs @@ -20,7 +20,7 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use log::{debug, info, warn}; +use log::{debug, warn}; use pgp::{types::PublicKeyTrait, SignedPublicKey, StandaloneSignature}; use tari_utilities::hex::from_hex; @@ -39,14 +39,12 @@ impl SignedMessageVerifier { /// Verify a standalone signature against a message using the configured maintainers' public keys pub fn verify_signature(&self, signature: &StandaloneSignature, message: &str) -> Option<&SignedPublicKey> { - info!(target: LOG_TARGET, "Starting signature verification, maintainers count: {}", self.maintainers.len()); self.maintainers.iter().find(|pk| { - debug!(target: LOG_TARGET, "Verifying signature with public key fingerprint: {:?}", pk.fingerprint()); let result = signature.verify(pk, message.as_bytes()).is_ok(); if result { - info!(target: LOG_TARGET, "Signature verified successfully with key: {:?}", pk.fingerprint()); + debug!(target: LOG_TARGET, "Signature verified successfully with key: {:?}", pk.fingerprint()); } else { - debug!(target: LOG_TARGET, "Signature verification failed with key: {:?}", pk.fingerprint()); + warn!(target: LOG_TARGET, "Signature verification failed with key: {:?}", pk.fingerprint()); } result }) @@ -59,7 +57,6 @@ impl SignedMessageVerifier { signature: &StandaloneSignature, file_content: &str, ) -> Result<&SignedPublicKey, SignatureVerificationError> { - debug!(target: LOG_TARGET, "Verifying file signature, content length: {} bytes", file_content.len()); self.verify_signature(signature, file_content).ok_or_else(|| { warn!(target: LOG_TARGET, "File signature verification failed - no matching maintainer key found"); SignatureVerificationError::VerificationFailed @@ -74,10 +71,6 @@ impl SignedMessageVerifier { hashes: &str, target_hash: &[u8], ) -> Result<(Vec, String), SignatureVerificationError> { - info!(target: LOG_TARGET, "Verifying signed hashes file, looking for target hash"); - debug!(target: LOG_TARGET, "Target hash: {:?}", target_hash); - debug!(target: LOG_TARGET, "Hashes file content length: {} bytes", hashes.len()); - self.verify_signature(signature, hashes).ok_or_else(|| { warn!(target: LOG_TARGET, "Hash file signature verification failed - no matching maintainer key found"); SignatureVerificationError::VerificationFailed @@ -93,14 +86,12 @@ impl SignedMessageVerifier { }) .collect(); - debug!(target: LOG_TARGET, "Parsed {} hash entries from file", parsed_hashes.len()); - parsed_hashes .into_iter() .find(|(hash, filename)| { let matches = *hash == target_hash; if matches { - info!(target: LOG_TARGET, "Found matching hash for file: {}", filename); + debug!(target: LOG_TARGET, "Found matching hash for file: {}", filename); } matches }) diff --git a/comms/core/src/builder/placeholder.rs b/comms/core/src/builder/placeholder.rs index 220cf99ec31..30ffefa39a6 100644 --- a/comms/core/src/builder/placeholder.rs +++ b/comms/core/src/builder/placeholder.rs @@ -29,6 +29,7 @@ use futures::future; use tower::Service; /// A service which is used as a placeholder type. This service will panic if used. +#[allow(dead_code)] pub struct PlaceholderService(PhantomData<(TReq, TResp, TErr)>); impl Service for PlaceholderService { From e242c0d8b9efcb28761401209ce984885fd4b90e Mon Sep 17 00:00:00 2001 From: Maciej Kozuszek Date: Fri, 5 Sep 2025 17:20:40 +0200 Subject: [PATCH 06/12] Rewrite test cases --- .../src/signature_verification/verifier.rs | 336 +++++++++++++----- 1 file changed, 246 insertions(+), 90 deletions(-) diff --git a/base_layer/p2p/src/signature_verification/verifier.rs b/base_layer/p2p/src/signature_verification/verifier.rs index d4d2ab8d7f9..8e94bb407c5 100644 --- a/base_layer/p2p/src/signature_verification/verifier.rs +++ b/base_layer/p2p/src/signature_verification/verifier.rs @@ -108,7 +108,103 @@ mod test { use super::*; - const PUBLIC_KEY: &str = r#"-----BEGIN PGP PUBLIC KEY BLOCK----- + // Real seed_peers_http.asc public key + const SEED_PEERS_PUBLIC_KEY: &str = r#"-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEaLl/nhYJKwYBBAHaRw8BAQdAocXM74pI54REY9Y0fESxir/iq8We9wp6JHFP +z8vcdm20Sk1hY2llaiAoVGVzdCBmb3Igc2VlZCBwZWVycyBIVFRQIGRvd25sb2Fk +KSA8bWFjaWVqLmtvenVzemVrQHNwYWNlaW5jaC5jb20+iJMEExYKADsWIQTaz8Pe +9KT58ia7xIFrHRtevPqxvwUCaLl/ngIbAwULCQgHAgIiAgYVCgkICwIEFgIDAQIe +BwIXgAAKCRBrHRtevPqxv5cOAQDR1jrEiLxlsEFLsI6DLd0I7SRQDw+tziT/02ed +7E8wMQD/ZzdO7ZO8oLfneJrrwoWiGk241+yq7ym5uEcBuhnKyQ+0Uk1hY2llaiBL +b3p1c3playAoVGVzdGluZyBzZWVkIHBlZXJzIEhUVFAgZG93bmxvYWQpIDxtYWNp +ZWoua296dXN6ZWtAc3BhY2VpbmNoLmNvbT6IkwQTFgoAOxYhBNrPw970pPnyJrvE +gWsdG168+rG/BQJousXyAhsDBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJ +EGsdG168+rG/zIgA/RmbnuNU7/mrSIqV62U5wPPhj3fT7+zR/9Ayn1ME+KbMAQC8 +fSb4bOv48TUskOtzWd9j+AuH+2w1bvi9/niKAPgrAA== +=p4US +-----END PGP PUBLIC KEY BLOCK-----"#; + + // Real signature from seednodes.json.asc (current live version) + const SEEDNODES_SIGNATURE: &str = r#"-----BEGIN PGP SIGNATURE----- + +iHUEABYKAB0WIQTaz8Pe9KT58ia7xIFrHRtevPqxvwUCaLmBrwAKCRBrHRtevPqx +v/oSAP92nITPC9TNDwfsIow7IBKxHqNNvOB6FjMy0ZCgpN1ouwEA4xGcg7aodWu/ +G0eKB6s7pbpSyu3XdQqJwozutRuCzA0= +=Y0ye +-----END PGP SIGNATURE-----"#; + + // Real content of seednodes.json (with trailing newline as in the actual file) + const SEEDNODES_JSON: &str = r#"{ + "peer_seeds": [ + "4cdfb70e0b38b60c6a3573b2870e32bc3d846419c606ea379f43650b80f38409::/ip4/51.83.4.85/tcp/18189", + "1e08628960f75b7e324f010b2ee609a9e28097e9101f4d769d474a38b6ee2d76::/ip4/51.83.102.25/tcp/18189", + "1e08628960f75b7e324f010b2ee609a9e28097e9101f4d769d474a38b6ee2d76::/ip6/2001:41d0:303:a619::1/tcp/18189", + "4cdfb70e0b38b60c6a3573b2870e32bc3d846419c606ea379f43650b80f38409::/ip6/2001:41d0:303:9a55::1/tcp/18189", + "1e08628960f75b7e324f010b2ee609a9e28097e9101f4d769d474a38b6ee2d76::/onion3/tadnxyokalnqjtvu6mlhxndcq4v2tlolotpvrflscdmi7lcautao3had:18141", + "4cdfb70e0b38b60c6a3573b2870e32bc3d846419c606ea379f43650b80f38409::/onion3/mhfptgpcj6htjkr5zwurom32wvt7x76ovqzn2ttnwo2bnku6baeaaiyd:18141" + ] +} +"#; + + #[test] + fn test_verify_real_seednodes_signature() { + // Test verification of the actual seednodes.json with its signature + let (sig, _) = StandaloneSignature::from_string(SEEDNODES_SIGNATURE.trim()).unwrap(); + let (key, _) = SignedPublicKey::from_string(SEED_PEERS_PUBLIC_KEY).unwrap(); + + // Debug: Print key fingerprint + println!("Key fingerprint: {:?}", key.fingerprint()); + + let verifier = SignedMessageVerifier::new(vec![key.clone()]); + + // Debug: Try to understand what's happening + println!("Attempting to verify signature..."); + println!("Content length: {}", SEEDNODES_JSON.len()); + println!("First 50 chars: {:?}", &SEEDNODES_JSON[..50]); + + // Try verification with debug info + let verify_result = sig.verify(&key, SEEDNODES_JSON.as_bytes()); + println!("Direct pgp verification result: {:?}", verify_result); + + if let Err(e) = &verify_result { + println!("Verification error details: {:?}", e); + } + + // This should successfully verify the real seednodes.json content + let result = verifier.verify_file_signature(&sig, SEEDNODES_JSON); + assert!( + result.is_ok(), + "Failed to verify real seednodes.json signature: {:?}", + result + ); + + // Verify we get the right key back + let signer = verifier.verify_signature(&sig, SEEDNODES_JSON).unwrap(); + let (expected_key, _) = SignedPublicKey::from_string(SEED_PEERS_PUBLIC_KEY).unwrap(); + assert_eq!(*signer, expected_key); + } + + #[test] + fn test_seednodes_signature_fails_with_tampered_content() { + // Test that verification fails when the content is modified + let (sig, _) = StandaloneSignature::from_string(SEEDNODES_SIGNATURE.trim()).unwrap(); + let (key, _) = SignedPublicKey::from_string(SEED_PEERS_PUBLIC_KEY).unwrap(); + let verifier = SignedMessageVerifier::new(vec![key]); + + // Tampered content should fail verification + let tampered_json = r#"{"peer_seeds": ["malicious_node"]}"#; + assert!(verifier.verify_signature(&sig, tampered_json).is_none()); + assert!(verifier.verify_file_signature(&sig, tampered_json).is_err()); + } + + #[test] + fn test_verify_seednodes_with_wrong_key() { + // Test that verification fails with a different key + let (sig, _) = StandaloneSignature::from_string(SEEDNODES_SIGNATURE.trim()).unwrap(); + + // Create a different key (using a test key that's not the signer) + const OTHER_KEY: &str = r#"-----BEGIN PGP PUBLIC KEY BLOCK----- mQINBF6y/8YBEAC+9x9jq0q8sle/M8aYlp4b9cHJPb6sataUaMzOxx/hQ9WCrhU1 GhJrDk+QPBMBtvT1oWMWa5KhMFNS1S0KTYbXensnF2tOdT6kSAWKXufW4hQ32p4B @@ -161,117 +257,177 @@ tQrYpH9CNXgX9dC9 =7S7i -----END PGP PUBLIC KEY BLOCK-----"#; - const VALID_SIGNATURE: &str = r#"-----BEGIN PGP SIGNATURE----- -iQIzBAEBCAAdFiEEM3uR78XxAn2K7fY9GIWxSVBMCmQFAmDYhicACgkQGIWxSVBM -CmRVuBAAkdFqPmJAHAu03CBTC6RjHlN+dxVgZ2UjfHzY80pVbiKTLeRoz7bMdVyZ -nVnf7QEcBMrK21LA/sBp/QmSGhym3AN3QjrFvOLJMWcfKj0gMdFV+z1TxNpZoKhD -EZheXNf+/Sy8sTdBJQhbGnD/Rs8+7IZbxKCCD43w26Z/Re+BOOeSFcARu4pka1e2 -EUJRUbV6UAB21TO/A+fAl4FuOgyWrNnrF/4Fy7Fk0jLaqf5kpYpvgC6SAKlkOhBz -x0zleJAxzvIBIomGJsS2FrV17mEATJiflgMslCeZAzoggnmlbv9tDOIXnYKA46+T -O7krar5DnHHLrLOVoAOQrfLVHVbp7Z4IdBegzer3Q7FE6Sgt+hscrw/nq37OOVjL -cj6S7+IsM4Vlsrwvu5E3VHt5DBvoFszxPq4eP6MRCoO6QvuYhB5L1sT1bvdhs+qM -DMe11D0lQakx1240GJK0J0fFEvlPPG+F+Q6bHXSGDu7D0bUNk2siSKy+IdpUrvwa -HFwxr8+CkSk5pNVZdusBZabXDnLxJz9k+rEvrB1F/9ZbLP3PzV9nyWcu3htxjcPo -Ckvq+QUz80XM69HPwpAgFW6QORZdxv4ED/ek4gth3fqmu/bkQ4/vYKozMtr6Rx7D -l9smp8LtJcXkw4cNgE4MB9VKdx+NhdbvWemt7ccldeL22hmyS24= -=vcW8 ------END PGP SIGNATURE-----"#; + let (wrong_key, _) = SignedPublicKey::from_string(OTHER_KEY).unwrap(); + let verifier = SignedMessageVerifier::new(vec![wrong_key]); - const MESSAGE: &str = "Philip R. Zimmermann"; + // Should fail because the key didn't sign this message + assert!(verifier.verify_signature(&sig, SEEDNODES_JSON).is_none()); + assert!(verifier.verify_file_signature(&sig, SEEDNODES_JSON).is_err()); + } #[test] - fn it_verifies_signed_message() { - let (sig, _) = StandaloneSignature::from_string(VALID_SIGNATURE.trim()).unwrap(); - let (key, _) = SignedPublicKey::from_string(PUBLIC_KEY).unwrap(); + fn test_seednodes_json_exact_formatting() { + // Test that the exact formatting of the JSON matters + let (sig, _) = StandaloneSignature::from_string(SEEDNODES_SIGNATURE.trim()).unwrap(); + let (key, _) = SignedPublicKey::from_string(SEED_PEERS_PUBLIC_KEY).unwrap(); let verifier = SignedMessageVerifier::new(vec![key]); - let signer = verifier.verify_signature(&sig, MESSAGE).unwrap(); - let (maintainer, _) = SignedPublicKey::from_string(PUBLIC_KEY).unwrap(); - assert_eq!(*signer, maintainer); - } + // Different formatting (minified) should fail + let minified_json = r#"{"peer_seeds":["4cdfb70e0b38b60c6a3573b2870e32bc3d846419c606ea379f43650b80f38409::/ip4/51.83.4.85/tcp/18189"]}"#; + assert!(verifier.verify_signature(&sig, minified_json).is_none()); - #[test] - fn it_does_not_validate_with_tampered_message() { - let (sig, _) = StandaloneSignature::from_string(VALID_SIGNATURE.trim()).unwrap(); - let (key, _) = SignedPublicKey::from_string(PUBLIC_KEY).unwrap(); - let verifier = SignedMessageVerifier::new(vec![key]); - assert!(verifier.verify_signature(&sig, "Zilip R. Phimmermann").is_none()); + // The exact content should succeed + assert!(verifier.verify_signature(&sig, SEEDNODES_JSON).is_some()); } #[test] - fn it_verifies_file_signature() { - let (sig, _) = StandaloneSignature::from_string(VALID_SIGNATURE.trim()).unwrap(); - let (key, _) = SignedPublicKey::from_string(PUBLIC_KEY).unwrap(); - let verifier = SignedMessageVerifier::new(vec![key]); - - // Test valid file signature - let result = verifier.verify_file_signature(&sig, MESSAGE); - assert!(result.is_ok()); + fn test_seed_peers_key_fingerprint() { + // Test that the seed_peers_http key has the correct fingerprint + let (key, _) = SignedPublicKey::from_string(SEED_PEERS_PUBLIC_KEY).unwrap(); - // Test invalid file signature - let result = verifier.verify_file_signature(&sig, "Wrong content"); - assert!(result.is_err()); - } + // The fingerprint should match what we expect (from the OpenPGP output) + // DACFC3DEF4A4F9F226BBC4816B1D1B5EBCFAB1BF + let fingerprint = key.fingerprint(); - #[test] - fn test_verify_json_file() { - // Test that we can verify a JSON file content - let json_content = r#"{"peer_seeds": ["node1", "node2"]}"#; - let (key, _) = SignedPublicKey::from_string(PUBLIC_KEY).unwrap(); + // Create verifier and ensure it can verify our real signature let verifier = SignedMessageVerifier::new(vec![key]); - - // This would work with a real signature of the JSON content - // For now, we just test that the function accepts JSON strings - let (sig, _) = StandaloneSignature::from_string(VALID_SIGNATURE.trim()).unwrap(); - let result = verifier.verify_file_signature(&sig, json_content); - // This will fail because the signature is for a different message - assert!(result.is_err()); + let (sig, _) = StandaloneSignature::from_string(SEEDNODES_SIGNATURE.trim()).unwrap(); + + // Should successfully verify + let result = verifier.verify_signature(&sig, SEEDNODES_JSON); + assert!( + result.is_some(), + "Key with fingerprint {:?} should verify the signature", + fingerprint + ); } #[test] - fn test_seed_peers_http_key() { - // Test that the seed_peers_http key can be parsed and used for verification - const SEED_PEERS_KEY: &str = r#"-----BEGIN PGP PUBLIC KEY BLOCK----- + fn test_multiple_maintainer_keys_with_real_signature() { + // Test that having multiple keys works and the right one is selected + const OTHER_KEY: &str = r#"-----BEGIN PGP PUBLIC KEY BLOCK----- -mDMEaLl/nhYJKwYBBAHaRw8BAQdAocXM74pI54REY9Y0fESxir/iq8We9wp6JHFP -z8vcdm20Sk1hY2llaiAoVGVzdCBmb3Igc2VlZCBwZWVycyBIVFRQIGRvd25sb2Fk -KSA8bWFjaWVqLmtvenVzemVrQHNwYWNlaW5jaS5jb20+iJMEExYKADsWIQTaz8Pe -9KT58ia7xIFrHRtevPqxvwUCaLl/ngIbAwULCQgHAgIiAgYVCgkICwIEFgIDAQIe -BwIXgAAKCRBrHRtevPqxv5cOAQDR1jrEiLxlsEFLsI6DLd0I7SRQDw+tziT/02ed -7E8wMQD/ZzdO7ZO8oLfneJrrwoWiGk241+yq7ym5uEcBuhnKyQ8= -=rjiS +mQINBF6y/8YBEAC+9x9jq0q8sle/M8aYlp4b9cHJPb6sataUaMzOxx/hQ9WCrhU1 +GhJrDk+QPBMBtvT1oWMWa5KhMFNS1S0KTYbXensnF2tOdT6kSAWKXufW4hQ32p4B +NW6aqrOxKMLj7jI2hwlCgRvlK+51J/l7e1OvCpQFL3wH/VMPBG5TgIRmgLeFZWWB +WtD6VjOAJROBiESb5DW+ox3hyxFEKMmwdC+B8b346GJedGFZem9eaN3ApjYBz/Ev +YsQQk2zL/eK5HeSYfboFBCWQrpIFtaJwyhzRlW2s5jz79Jv6kULZ+SVmfRerqk9c +jCzp48R5SJxIulk/PThqZ7sE6vEvwoGGSUzhQ0z1LhhFXt/0qg0qNeIvGkO5HRIR +R5i73/WG1PlgmcjtZHV54M86sTwm3yMevlHI5+i8Y4PAcYulftX9fVf85SitnWS5 +oAg3xP0pIWWztk8Ng4hWMM7sGE7q7BpjxuuGjrb9SNOTQuK8I7hg81p08LSNioOm +RD2OTkgbzew4YIMy+SmkmrFWvoKCRrWxNsQl4osVhOcLOlVBYoIjnBxy7AmHZzZC +ftgH5n6ODoB0CqZrc+UcMX4CFVtI7vaZOp1mcHN8geMhq1TjMJoGGlYuonaO34wM +2o+n+HbFJBCzfz/Q4pnuGwPDlumFU08E++ch63joMtXM1qAD5rNJMHfebQARAQAB +tDBTdGFubGV5IEJvbmRpIDxzZGJvbmRpQHVzZXJzLm5vcmVwbHkuZ2l0aHViLmNv +bT6JAk4EEwEIADgWIQQze5HvxfECfYrt9j0YhbFJUEwKZAUCXrL/xgIbAwULCQgH +AgYVCgkICwIEFgIDAQIeAQIXgAAKCRAYhbFJUEwKZIvVEAC3uGPAduK06FWxERWj +qXDR/tj7rNh6MaYXTLDM79sXP9nOj9aZOmA6lKZDRZ8lQyoZykwlVriHkJLYFotj +mxBPfgy1j5a2I52sF1sZMxwCg1nChvDivvnXTORMMcTWtIFkKu3cdzmO1Jil1tFB +zb205DG6gJ4JtXPpXKdAPkaJ68pqGcsAUU0N1KXla6ob/QwNlvp5aQ7cdR7uNbuI +kRx/KpsFNpA4jeP0+hK6kSaJgBdIUWzUWkfz9ubBdCRN8oWG+aazq4Y3DvaSnmbr +VCdb78Ni+QP98VtQhdk0UEc+T7vdbS9c71t6qMqNlRUWoiBZORnWa2QTqxhFGsM0 +FZhGX4UIZsdqMkTn/egf5zy/UmgqvmX2ujgQVj4OzkXT022wKgnr4z09/jymUPXE +o4QU15kTmjwTkNk8E3Cj1HbppyEgPNJ2bO3wnJbt6XMKejIXJC8X7G5v4WomOe8j +HVhqpAeOuML4u7KYg73wgRnIIMXCLR2VeS4iSZ42x/L6lWS5NzaGMV6nZv8t5ehh +otZ3uaWlHa4rRK2wrwveN/JdoYXqmZIoOb5Ivt9PlbUZ6NgHXDyHC7rCShtyPK2j +tY6BkoFz4HAloxhFGjRxBfDFjx9nefJ418owI1tOP1rNCoblROT1ggLlQ9a6URIF +R5WvoQC843hWwspzi7ll1Vz5JbkCDQResv/GARAArIvngo2dj+bZgu9/edkrKKbq +JZQj9fqaZDJrHXOmg/3t29qvEnyFJnyl9VYhSmLCppuy0k4YY4DaaCebBPafyV8e +Q/JNF3Le1FO7LHmoHuXFvcOvOVJhANpFKmNX3jaEYT7zDTbJ705FGldaC3udn12n +nEFlAEJjYQA6bgQAXXS02JjeVfl82IEgYpR0yFJjbL690tQ87Emlk3zeRrd/Esuv +Au9jHDTILSkUxa2dHTOgbtPwkk0N1NeGYIvWLYtwVcQ7KF+1xv/WVjO0dyr2qoia +4guJejBkNXAfYbodg5f7KjUYOcmTotSFurens5SdS+KUuaQtbfxGOt6nthwEU/N5 +x2/M64Y4l4vXtrjV+6d6RtvlPHnMTMAdfE6f3F/+wEsVlBQFbV2kn0nbDIJSlwys +L/kR6R9fHPtjSmS1omZWqE7bOu288j/M7/aP4Jcflj1t0+0WGfliS+0IgrNphUUA +1tpC7PXzXKzMtdK5xzLIZWAnjoXpzjVhcFglQpQSk9y4V9lqZbawx+RfHW1U2RYp +rVfvm42wg0DPYanWXzgO4nZdwSzu9RQQUdhdJAxCVV9ODh6CAVj0G7q2XEerjAUE +ZTxf1WKCJTpCy1B6w2lf1PN2zKDVpha0/76u/QcZGg5dAqklpSAaRNj3uDnq1HEP +RQOm6ladgLXO46J+ao0AEQEAAYkCNgQYAQgAIBYhBDN7ke/F8QJ9iu32PRiFsUlQ +TApkBQJesv/GAhsMAAoJEBiFsUlQTApk6HsP/A/sNwdzhTKIWGpdyxXz2YdUSK++ +kaQdZwtDIVcSZQ0yIFf0fPLkeoSd7jZfANmu2O1vnocBjdMcNOvPNjxKpkExJLVs +ttMiqla0ood8LuA9wteRFKRgoJc3Y71bWsxavLTfA4jDK+CaJG+K+vRDU7gwAdF+ +5rKhUIyn7pph7eWGHOv4bzGLEjV4NlLSzZGBA0aMDaWMGgStNzCD25yU7zYEJIWn +8gq2Rq0by8H6NLg6tygh5w8s2NUhPI5V31kZhsC1Kn5kExn4rVxFusqwG63gkPz1 +avx7E5kfChTgjaDlf0gnC73/alMeO4vTJKeDJaq581dza9jwJqaDC1+/ozYdGt7u +3KUxjhiSnWe38/AGna9cB4mAD4reCczH51gthlyeYNaSw+L0rsSMKvth9EYAHknP +ZFT97SIDPF1/2bRgO05I+J4BaSMA+2Euv/O3RWk953l+eR8MoZlr5mnMRM4Guy7K +nfTh5LZFccJyvW+CsxKKfwe/RNQPZLBuScqAogjsd+I6sVlmgLSyKkR2B3voRQ0g +l6J2669tX0wMPM/XsVlZ/UDdfUe6spRO8PXBwe+zdAAejUotLk4aMyhxxZVKCEwO +CrdiSo3ds50gaF1BXP72gfZW0E8djcD9ATfONqxFfftUwPbnbAqKh8t+L+If5H5r +tQrYpH9CNXgX9dC9 +=7S7i -----END PGP PUBLIC KEY BLOCK-----"#; - // Parse the key - let (key, _) = SignedPublicKey::from_string(SEED_PEERS_KEY).unwrap(); + let (seed_key, _) = SignedPublicKey::from_string(SEED_PEERS_PUBLIC_KEY).unwrap(); + let (other_key, _) = SignedPublicKey::from_string(OTHER_KEY).unwrap(); - // Create verifier with the seed peers key - let verifier = SignedMessageVerifier::new(vec![key]); + // Create verifier with multiple keys + let verifier = SignedMessageVerifier::new(vec![other_key, seed_key]); - // The verifier should be created successfully - assert_eq!(verifier.maintainers.len(), 1); - } + // Parse the real signature + let (sig, _) = StandaloneSignature::from_string(SEEDNODES_SIGNATURE.trim()).unwrap(); - #[test] - fn test_multiple_maintainer_keys() { - // Test with both the original test key and the seed peers key - const SEED_PEERS_KEY: &str = r#"-----BEGIN PGP PUBLIC KEY BLOCK----- - -mDMEaLl/nhYJKwYBBAHaRw8BAQdAocXM74pI54REY9Y0fESxir/iq8We9wp6JHFP -z8vcdm20Sk1hY2llaiAoVGVzdCBmb3Igc2VlZCBwZWVycyBIVFRQIGRvd25sb2Fk -KSA8bWFjaWVqLmtvenVzemVrQHNwYWNlaW5jaC5jb20+iJMEExYKADsWIQTaz8Pe -9KT58ia7xIFrHRtevPqxvwUCaLl/ngIbAwULCQgHAgIiAgYVCgkICwIEFgIDAQIe -BwIXgAAKCRBrHRtevPqxv5cOAQDR1jrEiLxlsEFLsI6DLd0I7SRQDw+tziT/02ed -7E8wMQD/ZzdO7ZO8oLfneJrrwoWiGk241+yq7ym5uEcBuhnKyQ8= -=rjiS ------END PGP PUBLIC KEY BLOCK-----"#; - - let (key1, _) = SignedPublicKey::from_string(PUBLIC_KEY).unwrap(); - let (key2, _) = SignedPublicKey::from_string(SEED_PEERS_KEY).unwrap(); + // Should successfully verify with the correct key (seed_key) + let result = verifier.verify_signature(&sig, SEEDNODES_JSON); + assert!(result.is_some(), "Should verify with one of the maintainer keys"); - let verifier = SignedMessageVerifier::new(vec![key1, key2]); + // The signer should be the seed_peers key, not the other key + let signer = result.unwrap(); + let (expected_key, _) = SignedPublicKey::from_string(SEED_PEERS_PUBLIC_KEY).unwrap(); + assert_eq!(*signer, expected_key); + } - // Should have both keys - assert_eq!(verifier.maintainers.len(), 2); + #[test] + fn test_debug_signature_parsing() { + // Test to debug the signature parsing issue + use pgp::Deserializable; + + println!("\n=== Debug Signature Parsing ==="); + + // Try to parse the signature + match StandaloneSignature::from_string(SEEDNODES_SIGNATURE.trim()) { + Ok((_, _)) => { + println!("Signature parsed successfully"); + // Try to get more info about the signature + println!("Signature type: EdDSA (based on error message)"); + }, + Err(e) => { + println!("Failed to parse signature: {:?}", e); + }, + } + + // Try parsing the key + match SignedPublicKey::from_string(SEED_PEERS_PUBLIC_KEY) { + Ok((key, _)) => { + println!("Key parsed successfully"); + println!("Key fingerprint: {:?}", key.fingerprint()); + }, + Err(e) => { + println!("Failed to parse key: {:?}", e); + }, + } + + // Test with exact bytes from downloaded file + println!("\n=== Testing with exact content ==="); + + // The content needs to match exactly what was signed + let test_content = SEEDNODES_JSON; + println!("Content bytes: {} bytes", test_content.len()); + + // Show hex of first and last few bytes to check for whitespace issues + let bytes = test_content.as_bytes(); + print!("First 20 bytes (hex): "); + for b in bytes.get(..20.min(bytes.len())).unwrap() { + print!("{:02x} ", b); + } + println!(); + + if bytes.len() > 20 { + print!("Last 20 bytes (hex): "); + for b in bytes.get(bytes.len() - 20..).unwrap() { + print!("{:02x} ", b); + } + println!(); + } } } From b927dff9f29b9e21cb5938448a0999646c57b2e0 Mon Sep 17 00:00:00 2001 From: Maciej Kozuszek Date: Fri, 5 Sep 2025 19:27:02 +0200 Subject: [PATCH 07/12] Update esmeralda peer seeds preset config --- base_layer/p2p/src/initialization.rs | 17 ----------------- common/config/presets/b_peer_seeds.toml | 1 + 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/base_layer/p2p/src/initialization.rs b/base_layer/p2p/src/initialization.rs index eb61b74b1b7..777803a5b82 100644 --- a/base_layer/p2p/src/initialization.rs +++ b/base_layer/p2p/src/initialization.rs @@ -101,23 +101,6 @@ const LOG_TARGET: &str = "p2p::initialization"; /// ProtocolId for minotari messaging protocol pub static MESSAGING_PROTOCOL_ID: ProtocolId = ProtocolId::from_static(b"t/msg/0.1"); -// Example usage of signature verification module: -// -// use crate::signature_verification::{verify_signed_file, verify_signed_hash_file, SignedMessageVerifier, maintainers}; -// -// // For verifying generic files like seednodes.json: -// async fn verify_seed_nodes(url: &str, sig_url: &str) -> Result { -// let content = verify_signed_file(url, sig_url).await?; -// Ok(content) -// } -// -// // For verifying hash files: -// async fn verify_hash_file(hash_url: &str, sig_url: &str, file_hash: &[u8]) -> Result<(), Error> { -// let (hash, filename) = verify_signed_hash_file(hash_url, sig_url, file_hash).await?; -// Ok(()) -// } -// - #[derive(Debug, Error)] pub enum CommsInitializationError { #[error("Comms builder error: `{0}`")] diff --git a/common/config/presets/b_peer_seeds.toml b/common/config/presets/b_peer_seeds.toml index 8e7b79d5f1c..f79e4a047fc 100644 --- a/common/config/presets/b_peer_seeds.toml +++ b/common/config/presets/b_peer_seeds.toml @@ -58,6 +58,7 @@ peer_seeds = [ "4cdfb70e0b38b60c6a3573b2870e32bc3d846419c606ea379f43650b80f38409::/ip6/2001:41d0:303:9a55::1/tcp/18189", "4cdfb70e0b38b60c6a3573b2870e32bc3d846419c606ea379f43650b80f38409::/onion3/mhfptgpcj6htjkr5zwurom32wvt7x76ovqzn2ttnwo2bnku6baeaaiyd:18141", ] +download_url = "https://cdn-universe.tari.com/tari-project/tari/esmeralda/seednodes.json" [igor.p2p.seeds] # DNS seeds hosts - DNS TXT records are queried from these hosts and the resulting peers added to the comms peer list. From 164b6db0f8967e00e8f1bd4184ca41a54b11cd5a Mon Sep 17 00:00:00 2001 From: Maciej Kozuszek Date: Sun, 7 Sep 2025 13:34:11 +0200 Subject: [PATCH 08/12] Add fallback function to old DNS TXT records seed peers --- base_layer/p2p/src/initialization.rs | 97 +++++++++++++++++++++++----- 1 file changed, 80 insertions(+), 17 deletions(-) diff --git a/base_layer/p2p/src/initialization.rs b/base_layer/p2p/src/initialization.rs index 777803a5b82..b555b28d08e 100644 --- a/base_layer/p2p/src/initialization.rs +++ b/base_layer/p2p/src/initialization.rs @@ -29,7 +29,9 @@ use std::{ }; use anyhow::anyhow; +use futures::future; use log::*; +use serde::Deserialize; use serde_json; use tari_common::{ configuration::{DnsNameServerList, Network}, @@ -90,6 +92,7 @@ use crate::{ config::{P2pConfig, PeerSeedsConfig}, dns::DnsClientError, peer_seeds::{DnsSeedResolver, SeedPeer}, + signature_verification::verify_signed_file, transport::{TorTransportConfig, TransportType}, TransportConfig, MAJOR_NETWORK_VERSION, @@ -475,10 +478,6 @@ impl P2pInitializer { async fn download_seed_peers_files( (url, addr): (String, String), ) -> Result, ServiceInitializationError> { - use serde::Deserialize; - - use crate::signature_verification::verify_signed_file; - #[derive(Deserialize)] struct SeedNodesJson { peer_seeds: Vec, @@ -486,19 +485,16 @@ impl P2pInitializer { let timer = Instant::now(); - // Download and verify the seed nodes file with its signature let content = verify_signed_file(&url, &format!("{}.asc", url)).await.map_err(|e| { warn!(target: LOG_TARGET, "Failed to verify seed nodes file from {}: {}", url, e); anyhow!("Signature verification failed: {}", e) })?; - // Parse the JSON content let seed_nodes: SeedNodesJson = serde_json::from_str(&content).map_err(|e: serde_json::Error| { warn!(target: LOG_TARGET, "Failed to parse seed nodes JSON from {}: {}", url, e); anyhow!("Invalid JSON: {}", e) })?; - // Convert strings to SeedPeer objects let mut peers = Vec::new(); for peer_str in seed_nodes.peer_seeds { match peer_str.parse::() { @@ -521,9 +517,7 @@ impl P2pInitializer { Ok(peers) } - async fn try_resolve_dns_seeds(config: &PeerSeedsConfig) -> Result, ServiceInitializationError> { - use futures::future; - + async fn resolve_http_download_seeds(config: &PeerSeedsConfig) -> Result, ServiceInitializationError> { if config.dns_seeds.is_empty() { debug!(target: LOG_TARGET, "No DNS Seeds configured"); return Ok(Vec::new()); @@ -570,8 +564,12 @@ impl P2pInitializer { .into_iter() .map(|url_pair| async move { P2pInitializer::download_seed_peers_files(url_pair).await }); - let all_seed_peers: Vec = future::join_all(downloading) - .await + let seed_peers = future::join_all(downloading).await; + if seed_peers.iter().all(|downlaod_res| downlaod_res.is_err()) { + return Err(anyhow!("Failed to download and verify seed peer files")); + } + + let all_seed_peers: Vec = seed_peers .into_iter() .filter_map(|result| match result { Ok(peers) => Some(peers), @@ -583,9 +581,7 @@ impl P2pInitializer { .flatten() .collect(); - // Convert SeedPeer to Peer let peers: Vec = all_seed_peers.into_iter().map(Peer::from).collect(); - info!( target: LOG_TARGET, "Resolved {} seed peers from DNS in {:.0?}", @@ -596,6 +592,73 @@ impl P2pInitializer { Ok(peers) } + async fn try_resolve_dns_seeds(config: &PeerSeedsConfig) -> Result, ServiceInitializationError> { + if config.dns_seeds.is_empty() { + debug!(target: LOG_TARGET, "No DNS Seeds configured"); + return Ok(Vec::new()); + } + + debug!( + target: LOG_TARGET, + "Resolving DNS seeds (DNSSEC is enabled: {}, name servers: {}, addresses: {}) ...", + config.dns_seeds_use_dnssec, + config.dns_seed_name_servers, + config + .dns_seeds + .iter() + .map(ToString::to_string) + .collect::>() + .join(",") + ); + let start = Instant::now(); + + let resolver = + P2pInitializer::get_dns_seed_resolver(config.dns_seeds_use_dnssec, &config.dns_seed_name_servers).await?; + let resolving = config.dns_seeds.iter().map(|addr| { + let mut resolver = resolver.clone(); + async move { + let timer = Instant::now(); + let seeds_res = match timeout(Duration::from_secs(5), resolver.resolve(addr)).await { + Ok(res) => res, + Err(_) => { + warn!(target: LOG_TARGET, "Timeout resolving DNS seed `{addr}`"); + Err(DnsClientError::Timeout) + }, + }; + // let res = (resolver.resolve(addr).await, addr); + let res = (seeds_res, addr.clone()); + info!(target: LOG_TARGET, "Resolved DNS seed `{}` in {:.0?}", addr, timer.elapsed()); + res + } + }); + + let peers = future::join_all(resolving) + .await + .into_iter() + // Log and ignore errors + .filter_map(|(result, addr)| match result { + Ok(peers) => { + info!( + target: LOG_TARGET, + "Found {} peer(s) from `{}` in {:.0?}", + peers.len(), + addr, + start.elapsed() + ); + Some(peers) + }, + Err(err) => { + warn!(target: LOG_TARGET, "DNS seed `{addr}` failed to resolve: {err}"); + None + }, + }) + .flatten() + .map(Into::into) + .collect::>(); + + Ok(peers) + } + async fn get_dns_seed_resolver( dns_seeds_use_dnssec: bool, dns_seed_name_servers: &DnsNameServerList, @@ -662,11 +725,11 @@ impl ServiceInitializer for P2pInitializer { let peer_manager = comms.peer_manager(); let node_identity = comms.node_identity(); - let peers = match Self::try_resolve_dns_seeds(&self.seed_config).await { + let peers = match Self::resolve_http_download_seeds(&self.seed_config).await { Ok(peers) => peers, Err(err) => { - warn!(target: LOG_TARGET, "Failed to resolve DNS seeds: {err}"); - Vec::new() + warn!(target: LOG_TARGET, "Failed to resolve seeds through HTTP, fallback to DNS: {err}"); + Self::try_resolve_dns_seeds(&self.seed_config).await.unwrap_or_default() }, }; add_seed_peers(&peer_manager, &node_identity, peers).await?; From f467152d1e067c37cfe37235e2ca2fb1a1d9e1a5 Mon Sep 17 00:00:00 2001 From: Maciej Kozuszek Date: Mon, 8 Sep 2025 10:23:33 +0200 Subject: [PATCH 09/12] Fix parsing armored PGP key in armored ASCII format --- base_layer/p2p/src/initialization.rs | 2 +- .../p2p/src/signature_verification/mod.rs | 32 +++++++++++++++---- .../src/signature_verification/verifier.rs | 3 +- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/base_layer/p2p/src/initialization.rs b/base_layer/p2p/src/initialization.rs index b555b28d08e..8448c0fe746 100644 --- a/base_layer/p2p/src/initialization.rs +++ b/base_layer/p2p/src/initialization.rs @@ -470,7 +470,7 @@ impl P2pInitializer { }, }?; let res = (download_url_res, addr.to_string()); - info!(target: LOG_TARGET, "Resolved DNS download URL `{}` in {:.0?}", addr, timer.elapsed()); + info!(target: LOG_TARGET, "Resolved DNS download URL `{}` in {:.0?}", res.0, timer.elapsed()); Ok(res) } diff --git a/base_layer/p2p/src/signature_verification/mod.rs b/base_layer/p2p/src/signature_verification/mod.rs index 706afe842b5..39ff1085d0f 100644 --- a/base_layer/p2p/src/signature_verification/mod.rs +++ b/base_layer/p2p/src/signature_verification/mod.rs @@ -23,8 +23,6 @@ mod error; mod verifier; -use std::io; - pub use error::SignatureVerificationError; use futures; use log::{debug, warn}; @@ -140,10 +138,9 @@ pub async fn download_file(url: T) -> Result(url: T) -> Result { let resp = http_download(url).await?; - let sig_bytes = resp.bytes().await?; - let cursor = io::Cursor::new(&sig_bytes); - match StandaloneSignature::from_bytes(cursor) { - Ok(sig) => { + let sig_text = resp.text().await?; + match StandaloneSignature::from_string(&sig_text) { + Ok((sig, _)) => { debug!(target: LOG_TARGET, "download_signature_file: Successfully parsed PGP signature"); Ok(sig) }, @@ -162,4 +159,27 @@ mod test { fn all_maintainers_well_formed() { assert_eq!(maintainers().count(), MAINTAINERS.len()); } + + #[tokio::test] + async fn test_parse_ascii_armored_signature() { + // This is the actual signature from the error log + let ascii_signature = r#"-----BEGIN PGP SIGNATURE----- + +iHUEABYKAB0WIQTaz8Pe9KT58ia7xIFrHRtevPqxvwUCaLmBrwAKCRBrHRtevPqx +v/oSAP92nITPC9TNDwfsIow7IBKxHqNNvOB6FjMy0ZCgpN1ouwEA4xGcg7aodWu/ +G0eKB6s7pbpSyu3XdQqJwozutRuCzA0= +=Y0ye +-----END PGP SIGNATURE-----"#; + + // Test that from_string can parse ASCII-armored signatures + let result = StandaloneSignature::from_string(ascii_signature); + assert!( + result.is_ok(), + "Failed to parse ASCII-armored signature: {:?}", + result.err() + ); + + let (_sig, _) = result.unwrap(); + // Successfully parsed the ASCII-armored signature + } } diff --git a/base_layer/p2p/src/signature_verification/verifier.rs b/base_layer/p2p/src/signature_verification/verifier.rs index 8e94bb407c5..18bc893b373 100644 --- a/base_layer/p2p/src/signature_verification/verifier.rs +++ b/base_layer/p2p/src/signature_verification/verifier.rs @@ -44,7 +44,8 @@ impl SignedMessageVerifier { if result { debug!(target: LOG_TARGET, "Signature verified successfully with key: {:?}", pk.fingerprint()); } else { - warn!(target: LOG_TARGET, "Signature verification failed with key: {:?}", pk.fingerprint()); + // It's debug since other keys are not checked + debug!(target: LOG_TARGET, "Signature verification failed with key: {:?}", pk.fingerprint()); } result }) From ab2e592491e628b5e72b75614596b0964f8f729b Mon Sep 17 00:00:00 2001 From: Maciej Kozuszek Date: Mon, 8 Sep 2025 10:35:50 +0200 Subject: [PATCH 10/12] Fix logging --- base_layer/p2p/src/initialization.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/base_layer/p2p/src/initialization.rs b/base_layer/p2p/src/initialization.rs index 8448c0fe746..50969628408 100644 --- a/base_layer/p2p/src/initialization.rs +++ b/base_layer/p2p/src/initialization.rs @@ -510,7 +510,7 @@ impl P2pInitializer { target: LOG_TARGET, "Downloaded and verified {} seed peers from {} in {:.0?}", peers.len(), - addr, + url, timer.elapsed() ); @@ -584,7 +584,7 @@ impl P2pInitializer { let peers: Vec = all_seed_peers.into_iter().map(Peer::from).collect(); info!( target: LOG_TARGET, - "Resolved {} seed peers from DNS in {:.0?}", + "Resolved {} seed peers from download URL in {:.0?}", peers.len(), start.elapsed() ); From aaf9c291ab355fab77f0bd226faa2bdeba5ed704 Mon Sep 17 00:00:00 2001 From: Maciej Kozuszek Date: Mon, 8 Sep 2025 10:54:20 +0200 Subject: [PATCH 11/12] Apply some code rabbit suggestions --- base_layer/p2p/src/initialization.rs | 2 +- base_layer/p2p/src/signature_verification/mod.rs | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/base_layer/p2p/src/initialization.rs b/base_layer/p2p/src/initialization.rs index 50969628408..0e9691380f7 100644 --- a/base_layer/p2p/src/initialization.rs +++ b/base_layer/p2p/src/initialization.rs @@ -476,7 +476,7 @@ impl P2pInitializer { /// downloads seed peers files - json with peers and .asc for verification async fn download_seed_peers_files( - (url, addr): (String, String), + (url, _): (String, String), ) -> Result, ServiceInitializationError> { #[derive(Deserialize)] struct SeedNodesJson { diff --git a/base_layer/p2p/src/signature_verification/mod.rs b/base_layer/p2p/src/signature_verification/mod.rs index 39ff1085d0f..dc90cd38f1f 100644 --- a/base_layer/p2p/src/signature_verification/mod.rs +++ b/base_layer/p2p/src/signature_verification/mod.rs @@ -23,11 +23,13 @@ mod error; mod verifier; +use std::time::Duration; + pub use error::SignatureVerificationError; use futures; use log::{debug, warn}; use pgp::{Deserializable, SignedPublicKey, StandaloneSignature}; -use reqwest::IntoUrl; +use reqwest::{Client, IntoUrl, Response}; pub use verifier::SignedMessageVerifier; const LOG_TARGET: &str = "p2p::signature_verification"; @@ -58,8 +60,13 @@ pub async fn download_hashes_sig_file(url: T) -> Result(url: T) -> Result { - let resp = reqwest::get(url).await?.error_for_status()?; +async fn http_download(url: T) -> Result { + let client = Client::builder() + .timeout(Duration::from_secs(15)) + .connect_timeout(Duration::from_secs(5)) + .build() + .expect("HTTP client build failed"); + let resp = client.get(url).send().await?.error_for_status()?; Ok(resp) } From a324244e2b9fb564d4d9d01130a24b212581c5b0 Mon Sep 17 00:00:00 2001 From: Maciej Kozuszek Date: Mon, 8 Sep 2025 11:16:06 +0200 Subject: [PATCH 12/12] Allow only https endpoints --- base_layer/p2p/src/peer_seeds.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/base_layer/p2p/src/peer_seeds.rs b/base_layer/p2p/src/peer_seeds.rs index 8cd298053a8..65177959793 100644 --- a/base_layer/p2p/src/peer_seeds.rs +++ b/base_layer/p2p/src/peer_seeds.rs @@ -96,11 +96,13 @@ impl DnsSeedResolver { pub async fn resolve_download_url(&mut self, addr: &str) -> Result { let records = self.client.query_txt(addr).await?; + trace!(target: LOG_TARGET, "DNS TXT records (download URL lookup) for {addr}: {:?}", records); let download_url = records .into_iter() - .find(|record| record.starts_with("https://") || record.starts_with("http://")) + .map(|r| r.trim().to_string()) + .find(|r| r.starts_with("https://")) .ok_or(DnsClientError::NoDownloadUrlFound)?; - Ok(download_url.to_string()) + Ok(download_url) } }