diff --git a/Cargo.lock b/Cargo.lock index 443459566b0..5a440c7eacd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7957,6 +7957,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..48bb7666c43 100644 --- a/base_layer/p2p/Cargo.toml +++ b/base_layer/p2p/Cargo.toml @@ -20,12 +20,13 @@ 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" thiserror = "1.0.26" tokio = { version = "1.44", features = ["macros"] } tokio-stream = { version = "0.1.9", default-features = false, features = [ @@ -52,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 50cdeb8511c..62bbede2750 100644 --- a/base_layer/p2p/src/auto_update/error.rs +++ b/base_layer/p2p/src/auto_update/error.rs @@ -20,14 +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 crate::dns::DnsClientError; +use thiserror::Error; -#[derive(Debug, thiserror::Error)] +use crate::{dns::DnsClientError, signature_verification::SignatureVerificationError}; + +#[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..259bcf0104a 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}; @@ -54,7 +50,10 @@ use tari_common::{ }; use tari_utilities::hex::Hex; -use crate::auto_update::{dns::UpdateSpec, signature::SignedMessageVerifier}; +use crate::{ + auto_update::dns::UpdateSpec, + signature_verification::{self, SignedMessageVerifier}, +}; const LOG_TARGET: &str = "p2p::auto_update"; @@ -122,24 +121,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 +182,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/auto_update/signature.rs b/base_layer/p2p/src/auto_update/signature.rs deleted file mode 100644 index fb770e99924..00000000000 --- a/base_layer/p2p/src/auto_update/signature.rs +++ /dev/null @@ -1,157 +0,0 @@ -// 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 tari_utilities::hex::from_hex; - -use crate::auto_update::dns::UpdateSpec; - -pub struct SignedMessageVerifier { - maintainers: Vec, -} - -impl SignedMessageVerifier { - pub fn new(maintainers: Vec) -> Self { - Self { maintainers } - } - - pub fn verify_signed_update( - &self, - signature: &pgp::StandaloneSignature, - hashes: &str, - update: &UpdateSpec, - ) -> Option<(Vec, String)> { - self.verify_signature(signature, hashes)?; - - hashes - .lines() - .filter_map(|line| { - let mut parts = line.splitn(2, ' '); - let hash = parts.next().map(|s| s.trim()).map(from_hex)?.ok()?; - 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()) - } -} - -#[cfg(test)] -mod test { - use pgp::Deserializable; - - use super::*; - use crate::auto_update::maintainers; - - const PUBLIC_KEY: &str = r#"-----BEGIN PGP PUBLIC KEY BLOCK----- - -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 -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-----"#; - - const MESSAGE: &str = "Philip R. Zimmermann"; - - #[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 verifier = SignedMessageVerifier::new(vec![key]); - let signer = verifier.verify_signature(&sig, MESSAGE).unwrap(); - - let (maintainer, _) = pgp::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 verifier = SignedMessageVerifier::new(maintainers().collect()); - assert!(verifier.verify_signature(&sig, "Zilip R. Phimmermann").is_none()); - } -} 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..0e9691380f7 100644 --- a/base_layer/p2p/src/initialization.rs +++ b/base_layer/p2p/src/initialization.rs @@ -28,8 +28,11 @@ use std::{ time::{Duration, Instant}, }; +use anyhow::anyhow; use futures::future; use log::*; +use serde::Deserialize; +use serde_json; use tari_common::{ configuration::{DnsNameServerList, Network}, exit_codes::{ExitCode, ExitError}, @@ -89,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, @@ -456,6 +460,138 @@ 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?}", res.0, timer.elapsed()); + Ok(res) + } + + /// downloads seed peers files - json with peers and .asc for verification + async fn download_seed_peers_files( + (url, _): (String, String), + ) -> Result, ServiceInitializationError> { + #[derive(Deserialize)] + struct SeedNodesJson { + peer_seeds: Vec, + } + + let timer = Instant::now(); + + 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) + })?; + + 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) + })?; + + 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(), + url, + timer.elapsed() + ); + + Ok(peers) + } + + 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()); + } + + 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?; + + // First, resolve all DNS records to get download URLs + let resolving = config.dns_seeds.iter().map(|addr| { + let mut resolver = resolver.clone(); + let addr = addr.clone(); + async move { P2pInitializer::get_url_from_dns(&mut resolver, &addr).await } + }); + + 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 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), + Err(e) => { + warn!(target: LOG_TARGET, "Failed to download/verify seed peers: {}", e); + None + }, + }) + .flatten() + .collect(); + + let peers: Vec = all_seed_peers.into_iter().map(Peer::from).collect(); + info!( + target: LOG_TARGET, + "Resolved {} seed peers from download URL in {:.0?}", + peers.len(), + start.elapsed() + ); + + 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"); @@ -497,28 +633,28 @@ impl P2pInitializer { }); 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::>(); + .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) } @@ -589,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?; @@ -615,9 +751,174 @@ impl ServiceInitializer for P2pInitializer { mod test { 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; 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.first().unwrap(); + assert_eq!( + first_peer.public_key.to_hex(), + "4cdfb70e0b38b60c6a3573b2870e32bc3d846419c606ea379f43650b80f38409" + ); + assert_eq!(first_peer.addresses.len(), 1); + assert_eq!( + first_peer.addresses.first().unwrap().to_string(), + "/ip4/51.83.4.85/tcp/18189" + ); + + // Verify an IPv6 peer + let ipv6_peer = &peers.get(2).unwrap(); + assert_eq!( + ipv6_peer.public_key.to_hex(), + "1e08628960f75b7e324f010b2ee609a9e28097e9101f4d769d474a38b6ee2d76" + ); + assert_eq!( + ipv6_peer.addresses.first().unwrap().to_string(), + "/ip6/2001:41d0:303:a619::1/tcp/18189" + ); + + // Verify an onion peer + let onion_peer = &peers.get(4).unwrap(); + assert_eq!( + onion_peer.addresses.first().unwrap().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.first().unwrap(); + 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, 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/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 6c3416fb2a5..65177959793 100644 --- a/base_layer/p2p/src/peer_seeds.rs +++ b/base_layer/p2p/src/peer_seeds.rs @@ -93,6 +93,17 @@ impl DnsSeedResolver { trace!(target: LOG_TARGET, "Seed peers: {:?}", peers.iter().map(|p| format!("{}", p)).collect::>()); Ok(peers) } + + 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() + .map(|r| r.trim().to_string()) + .find(|r| r.starts_with("https://")) + .ok_or(DnsClientError::NoDownloadUrlFound)?; + Ok(download_url) + } } /// 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/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..697eb4b60bd --- /dev/null +++ b/base_layer/p2p/src/signature_verification/gpg_keys/seed_peers_http.asc @@ -0,0 +1,15 @@ +-----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----- 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..dc90cd38f1f --- /dev/null +++ b/base_layer/p2p/src/signature_verification/mod.rs @@ -0,0 +1,192 @@ +// 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; + +use std::time::Duration; + +pub use error::SignatureVerificationError; +use futures; +use log::{debug, warn}; +use pgp::{Deserializable, SignedPublicKey, StandaloneSignature}; +use reqwest::{Client, IntoUrl, Response}; +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"), + include_str!("gpg_keys/seed_peers_http.asc"), +]; + +/// Returns an iterator over all configured maintainer public keys +pub fn maintainers() -> impl Iterator { + MAINTAINERS.iter().map(|s| { + let (pk, _) = SignedPublicKey::from_string(s).expect("Malformed maintainer PGP signature"); + pk + }) +} + +// 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 { + download_file(url).await +} + +/// Download a PGP signature file from the given URL (legacy name for compatibility) +pub async fn download_hashes_sig_file(url: T) -> Result { + download_signature_file(url).await +} + +/// Perform an HTTP GET request and return the response +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) +} + +/// 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> { + let (hashes, sig) = futures::join!(download_file(hashes_url), download_signature_file(signature_url)); + let hashes = hashes?; + let sig = sig?; + let verifier = SignedMessageVerifier::new(maintainers().collect()); + let result = verifier.verify_signed_hashes(&sig, &hashes, target_hash); + + match &result { + Ok((_hash, filename)) => { + debug!(target: LOG_TARGET, "Signature verification successful for file: {}", filename); + }, + Err(e) => { + warn!(target: LOG_TARGET, "Signature verification failed: {}", e); + }, + } + + result +} + +/// 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 { + let (content, sig) = futures::join!(download_file(file_url), download_signature_file(signature_url)); + let content = content?; + let sig = sig?; + let verifier = SignedMessageVerifier::new(maintainers().collect()); + + match verifier.verify_file_signature(&sig, &content) { + Ok(_) => { + debug!(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 { + 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_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) + }, + Err(e) => { + warn!(target: LOG_TARGET, "download_signature_file: Failed to parse PGP signature: {}", e); + Err(SignatureVerificationError::SignatureError(e)) + }, + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[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 new file mode 100644 index 00000000000..18bc893b373 --- /dev/null +++ b/base_layer/p2p/src/signature_verification/verifier.rs @@ -0,0 +1,434 @@ +// 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 log::{debug, 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, +} + +impl SignedMessageVerifier { + 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: &StandaloneSignature, message: &str) -> Option<&SignedPublicKey> { + self.maintainers.iter().find(|pk| { + let result = signature.verify(pk, message.as_bytes()).is_ok(); + if result { + debug!(target: LOG_TARGET, "Signature verified successfully with key: {:?}", pk.fingerprint()); + } else { + // It's debug since other keys are not checked + debug!(target: LOG_TARGET, "Signature verification failed with key: {:?}", pk.fingerprint()); + } + result + }) + } + + /// Verify a file's content against its signature + /// Returns Ok with the signing key if verification succeeds + pub fn verify_file_signature( + &self, + signature: &StandaloneSignature, + file_content: &str, + ) -> Result<&SignedPublicKey, SignatureVerificationError> { + 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 + /// This function expects the file to contain lines in the format: "HASH filename" + pub fn verify_signed_hashes( + &self, + signature: &StandaloneSignature, + hashes: &str, + target_hash: &[u8], + ) -> Result<(Vec, String), SignatureVerificationError> { + self.verify_signature(signature, hashes).ok_or_else(|| { + warn!(target: LOG_TARGET, "Hash file signature verification failed - no matching maintainer key found"); + SignatureVerificationError::VerificationFailed + })?; + + let parsed_hashes: Vec<(Vec, String)> = hashes + .lines() + .filter_map(|line| { + let mut parts = line.splitn(2, ' '); + let hash = parts.next().map(|s| s.trim()).map(from_hex)?.ok()?; + let filename = parts.next()?; + Some((hash, filename.trim().to_string())) + }) + .collect(); + + parsed_hashes + .into_iter() + .find(|(hash, filename)| { + let matches = *hash == target_hash; + if matches { + debug!(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 + }) + } +} + +#[cfg(test)] +mod test { + use pgp::{Deserializable, StandaloneSignature}; + + use super::*; + + // 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----- + +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 +tQrYpH9CNXgX9dC9 +=7S7i +-----END PGP PUBLIC KEY BLOCK-----"#; + + let (wrong_key, _) = SignedPublicKey::from_string(OTHER_KEY).unwrap(); + let verifier = SignedMessageVerifier::new(vec![wrong_key]); + + // 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 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]); + + // 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()); + + // The exact content should succeed + assert!(verifier.verify_signature(&sig, SEEDNODES_JSON).is_some()); + } + + #[test] + 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(); + + // The fingerprint should match what we expect (from the OpenPGP output) + // DACFC3DEF4A4F9F226BBC4816B1D1B5EBCFAB1BF + let fingerprint = key.fingerprint(); + + // Create verifier and ensure it can verify our real signature + let verifier = SignedMessageVerifier::new(vec![key]); + 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_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----- + +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 +tQrYpH9CNXgX9dC9 +=7S7i +-----END PGP PUBLIC KEY BLOCK-----"#; + + let (seed_key, _) = SignedPublicKey::from_string(SEED_PEERS_PUBLIC_KEY).unwrap(); + let (other_key, _) = SignedPublicKey::from_string(OTHER_KEY).unwrap(); + + // Create verifier with multiple keys + let verifier = SignedMessageVerifier::new(vec![other_key, seed_key]); + + // Parse the real signature + let (sig, _) = StandaloneSignature::from_string(SEEDNODES_SIGNATURE.trim()).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"); + + // 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); + } + + #[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!(); + } + } +} 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. 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 {