From 216c72d1ebd4cef94c1b1c89acade710db724351 Mon Sep 17 00:00:00 2001 From: ThetaSinner Date: Wed, 3 Apr 2024 03:59:08 +0100 Subject: [PATCH] First sweettest for generate --- checked_cli/Cargo.lock | 11 ++++ checked_cli/Cargo.toml | 1 + checked_cli/src/cli.rs | 55 +++++++++++++--- checked_cli/src/distribute.rs | 13 ++-- checked_cli/src/fetch.rs | 4 +- checked_cli/src/generate.rs | 23 +++---- checked_cli/src/hc_client.rs | 30 +++++---- checked_cli/src/password.rs | 37 +++++++---- checked_cli/tests/cli.rs | 65 +++++++++++++++++++ dnas/checked/zomes/integrity/fetch/src/lib.rs | 1 + 10 files changed, 190 insertions(+), 50 deletions(-) create mode 100644 checked_cli/tests/cli.rs diff --git a/checked_cli/Cargo.lock b/checked_cli/Cargo.lock index 6316ddb..7858fb1 100644 --- a/checked_cli/Cargo.lock +++ b/checked_cli/Cargo.lock @@ -839,6 +839,7 @@ dependencies = [ "rpassword 7.3.1", "serde", "serde_json", + "signing_keys_types", "tempfile", "tokio", "url 2.5.0", @@ -6626,6 +6627,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "signing_keys_types" +version = "0.1.0" +dependencies = [ + "checked_types", + "chrono", + "hdk", + "serde", +] + [[package]] name = "simba" version = "0.6.0" diff --git a/checked_cli/Cargo.toml b/checked_cli/Cargo.toml index 5176375..7423e00 100644 --- a/checked_cli/Cargo.toml +++ b/checked_cli/Cargo.toml @@ -31,3 +31,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" } diff --git a/checked_cli/src/cli.rs b/checked_cli/src/cli.rs index d9f6c39..2508150 100644 --- a/checked_cli/src/cli.rs +++ b/checked_cli/src/cli.rs @@ -34,14 +34,24 @@ pub struct GenerateArgs { #[arg(long, short, default_value_t = String::from("default"))] pub name: String, - /// The admin port for Holochain + /// The admin port for Holochain. #[arg(long, short)] pub port: Option, - /// Provide a password on the command line instead of prompting for it on platforms - /// where a prompt isn't supported. - #[cfg(not(any(windows, unix)))] - pub password: String, + /// 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, + + /// Whether to distribute the key on Holochain after generating it. + /// + /// If this flag is not provided, then an interactive prompt is used to confirm. + #[arg(long, short)] + pub distribute: Option, /// The directory to save the key in. /// @@ -58,12 +68,16 @@ pub struct SignArgs { #[arg(long, short, default_value_t = String::from("default"))] pub name: String, - /// Provide a password on the command line instead of prompting for it on platforms - /// where a prompt isn't supported. - #[cfg(not(any(windows, unix)))] - pub password: String, + /// 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, - /// The directory to save the key in. + /// The directory to find the signing key in. /// /// Defaults to `.config/checked` in your home directory. #[arg(long, short)] @@ -108,6 +122,21 @@ pub struct DistributeArgs { /// Defaults to `default`. #[arg(long, short, default_value_t = String::from("default"))] pub name: String, + + /// 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, + + /// The directory to find the verification key in. + /// + /// Defaults to `.config/checked` in your home directory. + #[arg(long, short)] + pub path: Option, } #[derive(clap::Args)] @@ -124,4 +153,10 @@ pub struct FetchArgs { /// Defaults to `default`. #[arg(long, short, default_value_t = String::from("default"))] pub name: String, + + /// The directory to find the signing key in. + /// + /// Defaults to `.config/checked` in your home directory. + #[arg(long, short)] + pub path: Option, } diff --git a/checked_cli/src/distribute.rs b/checked_cli/src/distribute.rs index 5f77dd5..6628887 100644 --- a/checked_cli/src/distribute.rs +++ b/checked_cli/src/distribute.rs @@ -8,6 +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::prelude::SignArgs; use crate::sign::sign; @@ -57,10 +58,11 @@ const PROOF_WORDS: [&str; 40] = [ pub async fn distribute(distribute_args: DistributeArgs) -> anyhow::Result<()> { println!("Distributing key: {}", distribute_args.name); - let mut app_client = get_authenticated_app_agent_client(distribute_args.port).await?; + let mut app_client = + get_authenticated_app_agent_client(distribute_args.port, distribute_args.path.clone()) + .await?; - // TODO path arg - let store_dir = get_store_dir(None)?; + let store_dir = get_store_dir(distribute_args.path.clone())?; let vk_path = get_verification_key_path(&store_dir, &distribute_args.name); let proof = generate_proof(); @@ -76,7 +78,8 @@ pub async fn distribute(distribute_args: DistributeArgs) -> anyhow::Result<()> { let sig_path = sign(SignArgs { name: distribute_args.name.clone(), - path: None, + password: Some(distribute_args.get_password()?), + path: distribute_args.path.clone(), file: tmp_file.path().to_path_buf(), output: None, })?; @@ -99,7 +102,7 @@ pub async fn distribute(distribute_args: DistributeArgs) -> anyhow::Result<()> { ) .await .map_err(|e| { - maybe_handle_holochain_error(&e); + maybe_handle_holochain_error(&e, distribute_args.path); anyhow::anyhow!("Failed to get signatures for the asset: {:?}", e) })?; diff --git a/checked_cli/src/fetch.rs b/checked_cli/src/fetch.rs index a169ad8..5ebc2cf 100644 --- a/checked_cli/src/fetch.rs +++ b/checked_cli/src/fetch.rs @@ -22,7 +22,8 @@ struct FetchState { } pub async fn fetch(fetch_args: FetchArgs) -> anyhow::Result<()> { - let mut app_client = hc_client::get_authenticated_app_agent_client(fetch_args.port).await?; + let mut app_client = + hc_client::get_authenticated_app_agent_client(fetch_args.port, fetch_args.path).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 @@ -131,6 +132,7 @@ pub async fn fetch(fetch_args: FetchArgs) -> anyhow::Result<()> { let signature_path = sign(SignArgs { name: fetch_args.name.clone(), + password: None, path: None, file: PathBuf::from(file), output: None, diff --git a/checked_cli/src/generate.rs b/checked_cli/src/generate.rs index 4f88366..a6edf3d 100644 --- a/checked_cli/src/generate.rs +++ b/checked_cli/src/generate.rs @@ -1,11 +1,12 @@ 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 minisign::KeyPair; use std::io::Write; pub async fn generate(generate_args: GenerateArgs) -> anyhow::Result<()> { - let store_dir = get_store_dir(generate_args.path)?; + let store_dir = get_store_dir(generate_args.path.clone())?; // Signing key let sk_path = get_signing_key_path(&store_dir, &generate_args.name); @@ -15,16 +16,13 @@ pub async fn generate(generate_args: GenerateArgs) -> anyhow::Result<()> { let vk_path = get_verification_key_path(&store_dir, &generate_args.name); let mut vk_file = open_file(&vk_path)?; - #[cfg(not(any(windows, unix)))] - let password = generate_args.password; - #[cfg(any(windows, unix))] - let password = rpassword::prompt_password("New password: ")?; + let password = generate_args.get_password()?; let _pk = KeyPair::generate_and_write_encrypted_keypair( &mut vk_file, &mut sk_file, None, - Some(password), + Some(password.clone()), )? .pk; @@ -39,12 +37,13 @@ pub async fn generate(generate_args: GenerateArgs) -> anyhow::Result<()> { "The public key was saved as {} - That one can be public.\n", vk_path.display() ); - // println!("Files signed using this key can be verified with the following command:\n"); - // println!("checked verify -P {}", _pk.to_base64()); - let should_distribute = dialoguer::Confirm::new() - .with_prompt("Would you like to distribute this key on Holochain?") - .interact()?; + let should_distribute = match generate_args.distribute { + Some(distribute) => distribute, + None => dialoguer::Confirm::new() + .with_prompt("Would you like to distribute this key on Holochain?") + .interact()?, + }; if !should_distribute { return Ok(()); @@ -60,6 +59,8 @@ pub async fn generate(generate_args: GenerateArgs) -> anyhow::Result<()> { distribute(DistributeArgs { port: admin_port, name: generate_args.name, + password: Some(password), + path: generate_args.path, }) .await?; diff --git a/checked_cli/src/hc_client.rs b/checked_cli/src/hc_client.rs index 3b0117a..94cf094 100644 --- a/checked_cli/src/hc_client.rs +++ b/checked_cli/src/hc_client.rs @@ -9,17 +9,19 @@ use holochain_types::websocket::AllowedOrigins; use serde::{Deserialize, Serialize}; use std::fs::{File, Permissions}; use std::io::Write; +use std::path::PathBuf; const DEFAULT_INSTALLED_APP_ID: &str = "checked"; pub async fn get_authenticated_app_agent_client( admin_port: u16, + path: Option, ) -> anyhow::Result { // TODO connect timeout not configurable! Really slow if Holochain is not running. let mut admin_client = AdminWebsocket::connect(format!("localhost:{admin_port}")).await?; let mut signer = ClientAgentSigner::new(); - load_or_create_signing_credentials(&mut admin_client, &mut signer).await?; + load_or_create_signing_credentials(&mut admin_client, &mut signer, path).await?; let app_port = find_or_create_app_interface(&mut admin_client).await?; @@ -31,12 +33,15 @@ pub async fn get_authenticated_app_agent_client( .await } -pub fn maybe_handle_holochain_error(conductor_api_error: &ConductorApiError) { +pub fn maybe_handle_holochain_error( + conductor_api_error: &ConductorApiError, + path: Option, +) { match conductor_api_error { // TODO brittle, would be nice if the errors for some important failures were more specific. ConductorApiError::SignZomeCallError(e) if e == "Provenance not found" => { eprintln!("Saved credentials for Holochain appear invalid, removing them. Please re-run this command"); - if let Ok(e) = get_credentials_path() { + if let Ok(e) = get_credentials_path(path) { if std::fs::remove_file(e).is_ok() { println!("Successfully removed credentials"); return; @@ -71,14 +76,15 @@ async fn find_or_create_app_interface(admin_client: &mut AdminWebsocket) -> anyh async fn load_or_create_signing_credentials( admin_client: &mut AdminWebsocket, signer: &mut ClientAgentSigner, + path: Option, ) -> anyhow::Result<()> { - match try_load_credentials()? { + match try_load_credentials(path.clone())? { Some((cell_id, credentials)) => { signer.add_credentials(cell_id, credentials); } None => { let (cell_id, credentials) = create_new_credentials(admin_client).await?; - dump_credentials(cell_id.clone(), &credentials)?; + dump_credentials(cell_id.clone(), &credentials, path)?; signer.add_credentials(cell_id, credentials); } } @@ -136,6 +142,7 @@ struct SavedCredentials { fn dump_credentials( cell_id: CellId, signing_credentials: &SigningCredentials, + path: Option, ) -> anyhow::Result<()> { let saved = SavedCredentials { cell_id: cell_id.clone(), @@ -147,8 +154,7 @@ fn dump_credentials( let serialized = serde_json::to_string(&saved) .map_err(|e| anyhow::anyhow!("Error serializing credentials: {:?}", e))?; - // generate_args.path - let credentials_path = get_credentials_path()?; + let credentials_path = get_credentials_path(path)?; let mut f = File::options() .create(true) @@ -171,8 +177,10 @@ fn dump_credentials( Ok(()) } -fn try_load_credentials() -> anyhow::Result> { - let credentials_path = get_credentials_path()?; +fn try_load_credentials( + path: Option, +) -> anyhow::Result> { + let credentials_path = get_credentials_path(path)?; let f = match File::open(credentials_path) { Ok(f) => f, @@ -212,6 +220,6 @@ fn try_load_credentials() -> anyhow::Result ))) } -fn get_credentials_path() -> anyhow::Result { - Ok(get_store_dir(None)?.join("credentials.json")) +fn get_credentials_path(path: Option) -> anyhow::Result { + Ok(get_store_dir(path)?.join("credentials.json")) } diff --git a/checked_cli/src/password.rs b/checked_cli/src/password.rs index 36742b7..14e31b4 100644 --- a/checked_cli/src/password.rs +++ b/checked_cli/src/password.rs @@ -1,4 +1,4 @@ -use crate::cli::{GenerateArgs, SignArgs}; +use crate::cli::{DistributeArgs, GenerateArgs, SignArgs}; pub trait GetPassword { fn get_password(&self) -> anyhow::Result; @@ -6,21 +6,34 @@ pub trait GetPassword { impl GetPassword for GenerateArgs { fn get_password(&self) -> anyhow::Result { - #[cfg(not(any(windows, unix)))] - return Ok(self.password); - #[cfg(any(windows, unix))] - return Ok(rpassword::prompt_password("New password: ")?); + get_password_common(self.password.as_ref(), "New password: ") } } impl GetPassword for SignArgs { fn get_password(&self) -> anyhow::Result { - #[cfg(not(any(windows, unix)))] - return Ok(self.password); - #[cfg(any(windows, unix))] - return Ok(rpassword::prompt_password(format!( - "Password for '{}': ", - self.name - ))?); + get_password_common( + self.password.as_ref(), + format!("Password for '{}': ", self.name), + ) + } +} + +impl GetPassword for DistributeArgs { + fn get_password(&self) -> anyhow::Result { + get_password_common( + self.password.as_ref(), + format!("Password for '{}': ", self.name), + ) + } +} + +fn get_password_common( + maybe_password: Option<&String>, + prompt: impl ToString, +) -> anyhow::Result { + match maybe_password { + Some(password) => Ok(password.clone()), + None => Ok(rpassword::prompt_password(prompt)?), } } diff --git a/checked_cli/tests/cli.rs b/checked_cli/tests/cli.rs new file mode 100644 index 0000000..644da35 --- /dev/null +++ b/checked_cli/tests/cli.rs @@ -0,0 +1,65 @@ +use checked_cli::prelude::{generate, GenerateArgs}; +use holochain::sweettest::{SweetAgents, SweetConductor, SweetZome}; +use holochain_conductor_api::{AdminInterfaceConfig, InterfaceDriver}; +use holochain_types::app::InstallAppPayload; +use holochain_types::prelude::AppBundleSource; +use holochain_types::websocket::AllowedOrigins; +use signing_keys_types::VfKeyResponse; +use std::collections::HashMap; + +#[tokio::test(flavor = "multi_thread")] +async fn test_generate() -> anyhow::Result<()> { + let conductor = SweetConductor::from_standard_config().await; + + let agent = SweetAgents::one(conductor.keystore()).await; + + conductor + .clone() + .install_app_bundle(InstallAppPayload { + source: AppBundleSource::Path("../workdir/checked.happ".into()), + agent_key: agent, + installed_app_id: Some("checked".into()), + membrane_proofs: HashMap::with_capacity(0), + network_seed: None, + }) + .await?; + + conductor.clone().enable_app("checked".to_string()).await?; + + let admin_port = conductor + .clone() + .add_admin_interfaces(vec![AdminInterfaceConfig { + driver: InterfaceDriver::Websocket { + port: 0, + allowed_origins: AllowedOrigins::Any, + }, + }]) + .await?; + let admin_port = admin_port.first().unwrap(); + + let dir = tempfile::tempdir()?; + + generate(GenerateArgs { + name: "test_generate".to_string(), + port: Some(*admin_port), + password: Some("test".to_string()), + distribute: Some(true), + path: Some(dir.as_ref().to_path_buf()), + }) + .await?; + + let cell_ids = conductor.running_cell_ids(); + let cell_id = cell_ids.iter().next().unwrap(); + + let zome = SweetZome::new(cell_id.clone(), "signing_keys".into()); + + let keys: Vec = conductor + .call_fallible(&zome, "get_my_verification_key_distributions", ()) + .await?; + + assert_eq!(keys.len(), 1); + + println!("Keys: {:?}", keys); + + Ok(()) +} diff --git a/dnas/checked/zomes/integrity/fetch/src/lib.rs b/dnas/checked/zomes/integrity/fetch/src/lib.rs index add9a47..642a147 100644 --- a/dnas/checked/zomes/integrity/fetch/src/lib.rs +++ b/dnas/checked/zomes/integrity/fetch/src/lib.rs @@ -18,5 +18,6 @@ pub enum EntryTypes { #[hdk_link_types] pub enum LinkTypes { + // TODO create links to make my own signatures discoverable in the UI AssetUrlToSignature, }