Skip to content

Commit

Permalink
Test create first asset signature
Browse files Browse the repository at this point in the history
  • Loading branch information
ThetaSinner committed Apr 4, 2024
1 parent 2d3c4f1 commit 9969b9e
Show file tree
Hide file tree
Showing 11 changed files with 269 additions and 97 deletions.
1 change: 1 addition & 0 deletions checked_cli/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions checked_cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ rpassword = "7.3.1"
[dev-dependencies]
holochain = { version = "0.3.0-beta-dev.43", default-features = false, features = ["sweetest"] }
signing_keys_types = { path = "../types/signing_keys" }
warp = "0.3.6"
34 changes: 34 additions & 0 deletions checked_cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,43 @@ pub struct FetchArgs {
#[arg(long, short, default_value_t = String::from("default"))]
pub name: String,

/// The directory or file to save the fetched asset to.
///
/// When a directory is provided:
/// - The directory must exist
/// - The filename is taken from the last component in the fetch URL's path.
///
/// When a file is provided:
/// - The directory containing the file, and any required parent directories, will be created.
///
/// Defaults to the directory that the CLI is running in.
#[arg(long, short)]
pub output: Option<PathBuf>,

/// Provide a password on the command line instead of prompting for it.
///
/// If this flag is not provided, then an interactive prompt is used to get the password.
///
/// This is not recommended when using as a CLI flag because the password may stay in your
/// shell history. Use the interactive prompt instead if possible!
#[arg(long)]
pub password: Option<String>,

/// The directory to find the signing key in.
///
/// Defaults to `.config/checked` in your home directory.
#[arg(long, short)]
pub path: Option<PathBuf>,

/// Continue if no existing signatures are found.
///
/// If this flag is not provided, then an interactive prompt is used to confirm.
#[arg(long, short)]
pub allow_no_signatures: Option<bool>,

/// Sign the asset after downloading and publish the signature on Holochain.
///
/// If this flag is not provided, then an interactive prompt is used to confirm.
#[arg(long, short)]
pub sign: Option<bool>,
}
2 changes: 1 addition & 1 deletion checked_cli/src/distribute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use checked_types::{DistributeVfKeyRequest, VerificationKeyType};
use crate::cli::DistributeArgs;
use crate::common::{get_store_dir, get_verification_key_path};
use crate::hc_client::{get_authenticated_app_agent_client, maybe_handle_holochain_error};
use crate::password::GetPassword;
use crate::interactive::GetPassword;
use crate::prelude::SignArgs;
use crate::sign::sign;

Expand Down
78 changes: 52 additions & 26 deletions checked_cli/src/fetch.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::cli::FetchArgs;
use crate::common::{get_store_dir, get_verification_key_path};
use crate::hc_client;
use crate::interactive::GetPassword;
use crate::prelude::SignArgs;
use crate::sign::sign;
use anyhow::Context;
Expand All @@ -15,15 +16,23 @@ use std::path::PathBuf;
use std::sync::atomic::AtomicUsize;
use std::sync::Arc;
use tempfile::NamedTempFile;
use url::Url;
use crate::hc_client::maybe_handle_holochain_error;

struct FetchState {
asset_size: AtomicUsize,
downloaded_size: AtomicUsize,
}

pub async fn fetch(fetch_args: FetchArgs) -> anyhow::Result<()> {
let fetch_url = url::Url::parse(&fetch_args.url).context("Invalid URL")?;
println!("Fetching from {}", fetch_url);

let output_path = get_output_path(&fetch_args, &fetch_url)?;

let mut app_client =
hc_client::get_authenticated_app_agent_client(fetch_args.port, fetch_args.path).await?;
hc_client::get_authenticated_app_agent_client(fetch_args.port, fetch_args.path.clone())
.await?;

// TODO if this fails because the credentials are no longer valid then we need a recovery mechanism that isn't `rm ~/.checked/credentials.json`
let response = app_client
Expand All @@ -37,27 +46,24 @@ pub async fn fetch(fetch_args: FetchArgs) -> anyhow::Result<()> {
.unwrap(),
)
.await
.map_err(|e| anyhow::anyhow!("Failed to get signatures for the asset: {:?}", e))?;
.map_err(|e| {
maybe_handle_holochain_error(&e, fetch_args.path.clone());
anyhow::anyhow!("Failed to get signatures for the asset: {:?}", e)
})?;

let response: Vec<FetchCheckSignature> = response.decode()?;

if response.is_empty() {
println!("No signatures found for this asset. This is normal but please consider asking the author to create a signature!");

let decision = dialoguer::Confirm::new()
.with_prompt("Download anyway?")
.interact()?;

if !decision {
let allow = fetch_args.allow_no_signatures()?;
if !allow {
return Ok(());
}
}

println!("Found {} signatures to check against", response.len());

let fetch_url = url::Url::parse(&fetch_args.url).context("Invalid URL")?;
println!("Fetching from {}", fetch_url);

let mut tmp_file = tempfile::Builder::new()
.prefix("checked-")
.suffix(".unverified")
Expand Down Expand Up @@ -115,31 +121,22 @@ pub async fn fetch(fetch_args: FetchArgs) -> anyhow::Result<()> {

// TODO validate the signatures here and report

let file = fetch_url
.path_segments()
.ok_or_else(|| anyhow::anyhow!("Invalid URL"))?
.last()
.ok_or_else(|| anyhow::anyhow!("Invalid URL"))?;
std::fs::rename(path.clone(), file)?;

let decision = dialoguer::Confirm::new()
.with_prompt("Sign this asset?")
.interact()?;
std::fs::rename(path.clone(), &output_path)?;

if !decision {
let should_sign = fetch_args.sign_asset()?;
if !should_sign {
return Ok(());
}

let signature_path = sign(SignArgs {
name: fetch_args.name.clone(),
password: None,
path: None,
file: PathBuf::from(file),
password: Some(fetch_args.get_password()?),
path: fetch_args.path.clone(),
file: output_path,
output: None,
})?;

// TODO dir as arg
let store_dir = get_store_dir(None)?;
let store_dir = get_store_dir(fetch_args.path)?;
let vk_path = get_verification_key_path(&store_dir, &fetch_args.name);
app_client
.call_zome(
Expand All @@ -162,6 +159,35 @@ pub async fn fetch(fetch_args: FetchArgs) -> anyhow::Result<()> {
Ok(())
}

fn get_output_path(fetch_args: &FetchArgs, fetch_url: &Url) -> anyhow::Result<PathBuf> {
let guessed_file_name = fetch_url
.path_segments()
.ok_or_else(|| anyhow::anyhow!("Invalid URL"))?
.last()
.ok_or_else(|| anyhow::anyhow!("Invalid URL"))?;

let output_path = match &fetch_args.output {
Some(output) => {
if output.is_dir() {
output.join(guessed_file_name)
} else {
let mut out = output.clone();
out.pop();
std::fs::create_dir_all(&out)?;

output.clone()
}
}
None => {
let mut out = std::env::current_dir()?;
out.push(guessed_file_name);
out
}
};

Ok(output_path)
}

/// Download from `fetch_url` into `writer` and update `state` with the download progress.
async fn run_download<W>(
fetch_url: url::Url,
Expand Down
2 changes: 1 addition & 1 deletion checked_cli/src/generate.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::cli::{DistributeArgs, GenerateArgs};
use crate::common::{get_signing_key_path, get_store_dir, get_verification_key_path, open_file};
use crate::distribute::distribute;
use crate::password::GetPassword;
use crate::interactive::GetPassword;
use minisign::KeyPair;
use std::io::Write;
use std::path::PathBuf;
Expand Down
31 changes: 30 additions & 1 deletion checked_cli/src/password.rs → checked_cli/src/interactive.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::cli::{DistributeArgs, GenerateArgs, SignArgs};
use crate::cli::{DistributeArgs, FetchArgs, GenerateArgs, SignArgs};

pub trait GetPassword {
fn get_password(&self) -> anyhow::Result<String>;
Expand Down Expand Up @@ -28,6 +28,15 @@ impl GetPassword for DistributeArgs {
}
}

impl GetPassword for FetchArgs {
fn get_password(&self) -> anyhow::Result<String> {
get_password_common(
self.password.as_ref(),
format!("Password for '{}': ", self.name),
)
}
}

fn get_password_common(
maybe_password: Option<&String>,
prompt: impl ToString,
Expand All @@ -37,3 +46,23 @@ fn get_password_common(
None => Ok(rpassword::prompt_password(prompt)?),
}
}

impl FetchArgs {
pub fn allow_no_signatures(&self) -> anyhow::Result<bool> {
match self.allow_no_signatures {
Some(allow_no_signatures) => Ok(allow_no_signatures),
None => Ok(dialoguer::Confirm::new()
.with_prompt("Download anyway?")
.interact()?),
}
}

pub fn sign_asset(&self) -> anyhow::Result<bool> {
match self.sign {
Some(sign) => Ok(sign),
None => Ok(dialoguer::Confirm::new()
.with_prompt("Sign this asset?")
.interact()?),
}
}
}
2 changes: 1 addition & 1 deletion checked_cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ mod distribute;
mod fetch;
pub mod generate;
pub(crate) mod hc_client;
mod password;
mod interactive;
pub mod sign;
pub mod verify;

Expand Down
2 changes: 1 addition & 1 deletion checked_cli/src/sign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::cli::SignArgs;
use crate::common::{
get_signing_key_path, get_store_dir, get_verification_key_path, open_file, unix_timestamp,
};
use crate::password::GetPassword;
use crate::interactive::GetPassword;
use minisign::{PublicKey, SecretKey};
use std::io::{BufReader, Write};
use std::path::PathBuf;
Expand Down
23 changes: 17 additions & 6 deletions checked_cli/tests/commands.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use std::fs::File;
use std::io::Write;
use checked_cli::cli::VerifyArgs;
use checked_cli::prelude::{generate, GenerateArgs, SignArgs};
use checked_cli::sign::sign;
use checked_cli::verify::verify;
use std::fs::File;
use std::io::Write;

// Generate a signing keypair, do not distribute
#[tokio::test(flavor = "multi_thread")]
Expand Down Expand Up @@ -40,7 +40,11 @@ async fn sign_file() -> anyhow::Result<()> {
.await?;

let test_file = dir.path().join("test.txt");
File::options().write(true).create_new(true).open(&test_file)?.write_all(b"test")?;
File::options()
.write(true)
.create_new(true)
.open(&test_file)?
.write_all(b"test")?;

let sig_path = sign(SignArgs {
name: name.clone(),
Expand All @@ -51,7 +55,10 @@ async fn sign_file() -> anyhow::Result<()> {
})?;

assert!(sig_path.exists());
assert_eq!(test_file.to_str().unwrap().to_string() + ".minisig", sig_path.to_str().unwrap());
assert_eq!(
test_file.to_str().unwrap().to_string() + ".minisig",
sig_path.to_str().unwrap()
);

Ok(())
}
Expand All @@ -68,10 +75,14 @@ async fn verify_signed_file() -> anyhow::Result<()> {
distribute: Some(false),
path: Some(dir.as_ref().to_path_buf()),
})
.await?;
.await?;

let test_file = dir.path().join("test.txt");
File::options().write(true).create_new(true).open(&test_file)?.write_all(b"test")?;
File::options()
.write(true)
.create_new(true)
.open(&test_file)?
.write_all(b"test")?;

sign(SignArgs {
name: name.clone(),
Expand Down
Loading

0 comments on commit 9969b9e

Please sign in to comment.