Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: security fix proposal #882

Merged
merged 7 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bin/cargo-deny-checks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ command -v cargo >/dev/null || {
}

command -v cargo-deny >/dev/null || echo "'cargo-deny' not found. Please install it by running 'cargo install cargo-deny'"
cargo deny check
cargo deny check --warn unmaintained
141 changes: 141 additions & 0 deletions rs/cli/src/artifact_downloader.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
#![allow(async_fn_in_trait)]
use std::{fs::File, io::Write, path::Path};

use futures::{future::BoxFuture, stream, StreamExt};
use ic_management_types::Artifact;
use itertools::Itertools;
use log::{info, warn};
use mockall::automock;
use reqwest::StatusCode;
use sha2::{Digest, Sha256};

pub struct ArtifactDownloaderImpl {}

#[automock]
pub trait ArtifactDownloader: Sync + Send {
fn get_s3_cdn_image_url<'a>(&'a self, version: &'a str, s3_subdir: &'a str) -> String {
format!(
"https://download.dfinity.systems/ic/{}/{}/update-img/update-img.tar.gz",
version, s3_subdir
)
}

fn get_r2_cdn_image_url<'a>(&'a self, version: &'a str, s3_subdir: &'a str) -> String {
format!(
"https://download.dfinity.network/ic/{}/{}/update-img/update-img.tar.gz",
version, s3_subdir
)
}

fn download_file_and_get_sha256<'a>(&'a self, download_url: &'a str) -> BoxFuture<'_, anyhow::Result<String>> {
Box::pin(async move {
let url = url::Url::parse(download_url)?;
let subdir = format!("{}{}", url.domain().expect("url.domain() is None"), url.path().to_owned());
// replace special characters in subdir with _
let subdir = subdir.replace(|c: char| !c.is_ascii_alphanumeric(), "_");
let download_dir = format!("{}/tmp/ic/{}", dirs::home_dir().expect("home_dir is not set").as_path().display(), subdir);
let download_dir = Path::new(&download_dir);

std::fs::create_dir_all(download_dir).unwrap_or_else(|_| panic!("create_dir_all failed for {}", download_dir.display()));

let download_image = format!("{}/update-img.tar.gz", download_dir.to_str().unwrap());
let download_image = Path::new(&download_image);

let response = reqwest::get(download_url).await?;

if response.status() != StatusCode::RANGE_NOT_SATISFIABLE && !response.status().is_success() {
return Err(anyhow::anyhow!(
"Download failed with http_code {} for {}",
response.status(),
download_url
));
}
info!("Download {} succeeded {}", download_url, response.status());

let mut file = match File::create(download_image) {
Ok(file) => file,
Err(err) => return Err(anyhow::anyhow!("Couldn't create a file: {}", err)),
};

let content = response.bytes().await?;
file.write_all(&content)?;

let mut hasher = Sha256::new();
hasher.update(&content);
let hash = hasher.finalize();
let stringified_hash = hash[..].iter().map(|byte| format!("{:01$x?}", byte, 2)).collect::<Vec<String>>().join("");
info!("File saved at {} has sha256 {}", download_image.display(), stringified_hash);
Ok(stringified_hash)
})
}

fn download_images_and_validate_sha256<'a>(
&'a self,
image: &'a Artifact,
version: &'a str,
ignore_missing_urls: bool,
) -> BoxFuture<'_, anyhow::Result<(Vec<String>, String)>> {
Box::pin(async move {
let update_urls = vec![
self.get_s3_cdn_image_url(version, &image.s3_folder()),
self.get_r2_cdn_image_url(version, &image.s3_folder()),
];

// Download images, verify them and compare the SHA256
let hash_and_valid_urls: Vec<(String, &String)> = stream::iter(&update_urls)
.filter_map(|update_url| async move {
match self.download_file_and_get_sha256(update_url).await {
Ok(hash) => {
info!("SHA256 of {}: {}", update_url, hash);
Some((hash, update_url))
}
Err(err) => {
warn!("Error downloading {}: {}", update_url, err);
None
}
}
})
.collect()
.await;
let hashes_unique = hash_and_valid_urls.iter().map(|(h, _)| h.clone()).unique().collect::<Vec<String>>();
let expected_hash: String = match hashes_unique.len() {
0 => {
return Err(anyhow::anyhow!(
"Unable to download the update image from none of the following URLs: {}",
update_urls.join(", ")
))
}
1 => {
let hash = hashes_unique.into_iter().next().unwrap();
info!("SHA256 of all download images is: {}", hash);
hash
}
_ => {
return Err(anyhow::anyhow!(
"Update images do not have the same hash: {:?}",
hash_and_valid_urls.iter().map(|(h, u)| format!("{} {}", h, u)).join("\n")
))
}
};
let update_urls = hash_and_valid_urls.into_iter().map(|(_, u)| u.clone()).collect::<Vec<String>>();

if update_urls.is_empty() {
return Err(anyhow::anyhow!(
"Unable to download the update image from none of the following URLs: {}",
update_urls.join(", ")
));
} else if update_urls.len() == 1 {
if ignore_missing_urls {
warn!("Only 1 update image is available. At least 2 should be present in the proposal");
} else {
return Err(anyhow::anyhow!(
"Only 1 update image is available. At least 2 should be present in the proposal"
));
}
}
Ok((update_urls, expected_hash))
})
}
}

impl ArtifactDownloader for ArtifactDownloaderImpl {}
2 changes: 1 addition & 1 deletion rs/cli/src/commands/version/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use clap::Args;
use super::{impl_executable_command_for_enums, ExecutableCommand, IcAdminRequirement};
use crate::commands::version::revise::ReviseElectedVersions;

mod revise;
pub(crate) mod revise;

#[derive(Args, Debug)]
pub struct Version {
Expand Down
5 changes: 5 additions & 0 deletions rs/cli/src/commands/version/revise/guest_os.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ pub struct GuestOs {
/// Force proposal submission, ignoring missing download URLs
#[clap(long)]
pub force: bool,

/// Mark version as a security hotfix
#[clap(long)]
pub security_fix: bool,
}

impl ExecutableCommand for GuestOs {
Expand All @@ -31,6 +35,7 @@ impl ExecutableCommand for GuestOs {
&self.release_tag,
self.force,
ctx.forum_post_link(),
self.security_fix,
)
.await
}
Expand Down
5 changes: 5 additions & 0 deletions rs/cli/src/commands/version/revise/host_os.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ pub struct HostOs {
/// Force proposal submission, ignoring missing download URLs
#[clap(long)]
pub force: bool,

/// Mark version as a security hotfix
#[clap(long)]
pub security_fix: bool,
}

impl ExecutableCommand for HostOs {
Expand All @@ -31,6 +35,7 @@ impl ExecutableCommand for HostOs {
&self.release_tag,
self.force,
ctx.forum_post_link(),
self.security_fix,
)
.await
}
Expand Down
4 changes: 2 additions & 2 deletions rs/cli/src/commands/version/revise/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use crate::commands::version::revise::host_os::HostOs;
use clap::Args;
use ic_management_types::Artifact;

mod guest_os;
mod host_os;
pub(crate) mod guest_os;
pub(crate) mod host_os;

#[derive(Args, Debug)]
pub struct ReviseElectedVersions {
Expand Down
8 changes: 7 additions & 1 deletion rs/cli/src/ctx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use log::{debug, info};
use url::Url;

use crate::{
artifact_downloader::{ArtifactDownloader, ArtifactDownloaderImpl},
auth::{Auth, Neuron},
commands::{Args, ExecutableCommand, IcAdminRequirement, IcAdminVersion},
ic_admin::{download_ic_admin, should_update_ic_admin, IcAdmin, IcAdminImpl, FALLBACK_IC_ADMIN_VERSION},
Expand All @@ -41,6 +42,7 @@ pub struct DreContext {
ic_admin_path: Option<String>,
forum_post_link: Option<String>,
dry_run: bool,
artifact_downloader: Arc<dyn ArtifactDownloader>,
}

impl DreContext {
Expand Down Expand Up @@ -99,6 +101,7 @@ impl DreContext {
forum_post_link: forum_post_link.clone(),
ic_repo: RefCell::new(None),
dry_run,
artifact_downloader: Arc::new(ArtifactDownloaderImpl {}) as Arc<dyn ArtifactDownloader>,
})
}

Expand Down Expand Up @@ -267,6 +270,7 @@ impl DreContext {
self.proposals_agent(),
self.verbose_runner,
self.ic_repo.clone(),
self.artifact_downloader.clone(),
));
*self.runner.borrow_mut() = Some(runner.clone());
runner
Expand All @@ -290,7 +294,7 @@ pub mod tests {
use ic_management_backend::{lazy_git::LazyGit, lazy_registry::LazyRegistry, proposal::ProposalAgent};
use ic_management_types::Network;

use crate::ic_admin::IcAdmin;
use crate::{artifact_downloader::ArtifactDownloader, ic_admin::IcAdmin};

use super::DreContext;

Expand All @@ -300,6 +304,7 @@ pub mod tests {
ic_admin: Arc<dyn IcAdmin>,
git: Arc<dyn LazyGit>,
proposal_agent: Arc<dyn ProposalAgent>,
artifact_downloader: Arc<dyn ArtifactDownloader>,
) -> DreContext {
DreContext {
network,
Expand All @@ -313,6 +318,7 @@ pub mod tests {
ic_admin_path: None,
forum_post_link: None,
dry_run: true,
artifact_downloader,
}
}
}
Loading
Loading