diff --git a/plexi_cli/src/cli.rs b/plexi_cli/src/cli.rs index 223855f..faf30d8 100644 --- a/plexi_cli/src/cli.rs +++ b/plexi_cli/src/cli.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use clap::{Parser, Subcommand}; use plexi_core::Epoch; @@ -48,6 +50,25 @@ pub enum Commands { #[arg(short, long, default_value_t = false, group = "format")] long: bool, }, + #[command(verbatim_doc_comment)] + LocalAudit { + /// Ed25519 public key in hex format. + #[arg(long, env = "PLEXI_VERIFYING_KEY")] + verifying_key: Option, + /// Enable detailed output + #[arg(short, long, default_value_t = false, group = "format")] + long: bool, + /// Disable signature and proof validation + #[arg(long, default_value_t = false, env = "PLEXI_VERIFICATION_DISABLED")] + no_verify: bool, + /// Path to a file containing an epoch consistency proof + /// Format is still ad-hoc, based on AKD + #[arg(long, env = "PLEXI_PROOF_PATH")] + proof_path: Option, + /// Path to a file containing an epoch to verify + /// Format is { ciphersuite, namespace, timestamp, epoch, digest, signature } + signature_path_or_stdin: Option, + }, } #[allow(dead_code)] diff --git a/plexi_cli/src/cmd.rs b/plexi_cli/src/cmd.rs index 0da4f18..f31bae3 100644 --- a/plexi_cli/src/cmd.rs +++ b/plexi_cli/src/cmd.rs @@ -1,8 +1,7 @@ use std::{ fmt, fs, - io::{self, Write}, + io::{self, Read}, path::PathBuf, - time::Duration, }; use akd::local_auditing::AuditBlobName; @@ -13,11 +12,11 @@ use plexi_core::{ auditor, client::PlexiClient, namespaces::Namespaces, Ciphersuite, Epoch, SignatureResponse, }; use reqwest::Url; -use tokio::time::interval; + +use crate::print::print_dots; const APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); -#[allow(dead_code)] pub fn file_or_stdin(input: Option) -> Result> { let reader: Box = match input { Some(path) => Box::new(io::BufReader::new( @@ -282,18 +281,8 @@ pub async fn audit( if log_enabled!(log::Level::Error) { eprintln!("Audit proof verification enabled. It can take a few seconds"); } - async fn print_dots() { - let mut interval = interval(Duration::from_secs(1)); - loop { - interval.tick().await; - if log_enabled!(log::Level::Error) { - eprint!("."); - } - std::io::stderr().flush().unwrap(); - } - } - let dots_handle = tokio::spawn(print_dots()); + let dots_handle = print_dots(); // given Cloudflare does not expose the proof at the time of writing, uses the log directory and assume it's formatted like what WhatsApp provides let Some(namespace_info) = client.namespace(namespace).await? else { @@ -413,18 +402,126 @@ pub async fn audit( } dots_handle.abort(); - match verification { - Ok(_) => format_audit_response( + if let Err(e) = verification { + return format_audit_response( long, &signature, &VerificationStatus::Success, + &VerificationStatus::Failed(e.to_string()), + ); + } + format_audit_response( + long, + &signature, + &VerificationStatus::Success, + &VerificationStatus::Success, + ) +} + +pub async fn audit_local( + verifying_key: Option<&str>, + long: bool, + verify: bool, + proof_path: Option, + input: Option, +) -> Result { + let src = file_or_stdin(input)?; + let signature: SignatureResponse = serde_json::from_reader(src)?; + + // no verification requested, we can stop here + if !verify { + return format_audit_response( + long, + &signature, + &VerificationStatus::Disabled, + &VerificationStatus::Disabled, + ); + } + + // verify the signature against the log signature + let verifying_key = match verifying_key { + Some(key) => key, + None => { + return format_audit_response( + long, + &signature, + &VerificationStatus::Failed("auditor does not have key with key_id".to_string()), + &VerificationStatus::Disabled, + ); + } + }; + + let Ok(verifying_key) = hex::decode(verifying_key) else { + return format_audit_response( + long, + &signature, + &VerificationStatus::Failed("auditor key is not valid hex".to_string()), + &VerificationStatus::Disabled, + ); + }; + + if signature.verify(&verifying_key).is_err() { + return format_audit_response( + long, + &signature, + &VerificationStatus::Failed( + "signature does not verify for the auditor key".to_string(), + ), + &VerificationStatus::Disabled, + ); + } + + let Some(proof_path) = proof_path else { + return format_audit_response( + long, + &signature, + &VerificationStatus::Success, + &VerificationStatus::Disabled, + ); + }; + + let mut src = fs::File::open(proof_path).context("cannot read input file")?; + + let mut raw_proof = vec![]; + if let Err(e) = src.read_to_end(&mut raw_proof) { + return format_audit_response( + long, + &signature, &VerificationStatus::Success, - ), - Err(e) => format_audit_response( + &VerificationStatus::Failed(e.to_string()), + ); + }; + let raw_proof = raw_proof; + let blob = AuditBlobName { + epoch: signature.epoch().into(), + previous_hash: auditor::compute_start_root_hash(&raw_proof).await?, + current_hash: signature.digest().as_slice().try_into()?, + }; + + if log_enabled!(log::Level::Error) { + eprintln!("Audit proof verification enabled. It can take a few seconds"); + } + let dots_handle = print_dots(); + + let verification = auditor::verify_raw_proof(&blob, &raw_proof).await; + + if log_enabled!(log::Level::Error) { + eprintln!(); + } + dots_handle.abort(); + + if let Err(e) = verification { + return format_audit_response( long, &signature, &VerificationStatus::Success, &VerificationStatus::Failed(e.to_string()), - ), + ); } + format_audit_response( + long, + &signature, + &VerificationStatus::Success, + &VerificationStatus::Success, + ) } diff --git a/plexi_cli/src/main.rs b/plexi_cli/src/main.rs index 418c81e..58318eb 100644 --- a/plexi_cli/src/main.rs +++ b/plexi_cli/src/main.rs @@ -2,6 +2,7 @@ use std::process; mod cli; mod cmd; +mod print; #[tokio::main] pub async fn main() -> anyhow::Result<()> { @@ -35,6 +36,22 @@ pub async fn main() -> anyhow::Result<()> { ) .await } + cli::Commands::LocalAudit { + verifying_key, + long, + no_verify, + proof_path, + signature_path_or_stdin, + } => { + cmd::audit_local( + verifying_key.as_deref(), + long, + !no_verify, + proof_path, + signature_path_or_stdin, + ) + .await + } }; match output { diff --git a/plexi_cli/src/print.rs b/plexi_cli/src/print.rs new file mode 100644 index 0000000..21c0191 --- /dev/null +++ b/plexi_cli/src/print.rs @@ -0,0 +1,22 @@ +use std::io::Write as _; + +use log::log_enabled; +use tokio::{ + task::JoinHandle, + time::{interval, Duration}, +}; + +pub fn print_dots() -> JoinHandle<()> { + async fn print_dots_routine() { + let mut interval = interval(Duration::from_secs(1)); + loop { + interval.tick().await; + if log_enabled!(log::Level::Error) { + eprint!("."); + } + std::io::stderr().flush().unwrap(); + } + } + + tokio::spawn(print_dots_routine()) +} diff --git a/plexi_core/src/auditor.rs b/plexi_core/src/auditor.rs index 01c706b..1e18374 100644 --- a/plexi_core/src/auditor.rs +++ b/plexi_core/src/auditor.rs @@ -1,7 +1,12 @@ use std::collections::HashMap; #[cfg(feature = "auditor")] -use akd::{local_auditing::AuditBlobName, SingleAppendOnlyProof, WhatsAppV1Configuration}; +use akd::{ + append_only_zks::InsertMode, + local_auditing::AuditBlobName, + storage::{memory::AsyncInMemoryDatabase, StorageManager}, + Azks, Digest, SingleAppendOnlyProof, WhatsAppV1Configuration, +}; #[cfg(feature = "auditor")] use anyhow::anyhow; use anyhow::Context as _; @@ -93,6 +98,31 @@ impl Configuration { } } +#[cfg(feature = "auditor")] +pub async fn compute_start_root_hash(raw_proof: &[u8]) -> anyhow::Result { + let proto = akd::proto::specs::types::SingleAppendOnlyProof::parse_from_bytes(raw_proof) + .context("unable to parse proof bytes")?; + + let proof = SingleAppendOnlyProof::try_from(&proto) + .map_err(|e| anyhow::anyhow!(e.to_string())) + .context("converting parsed protobuf proof to `SingleAppendOnlyProof`")?; + + let db = AsyncInMemoryDatabase::new(); + let manager = StorageManager::new_no_cache(db); + + let mut azks = Azks::new::(&manager).await?; + azks.batch_insert_nodes::( + &manager, + proof.unchanged_nodes.clone(), + InsertMode::Auditor, + ) + .await?; + + Ok(azks + .get_root_hash::(&manager) + .await?) +} + #[cfg(feature = "auditor")] pub async fn verify_raw_proof(blob: &AuditBlobName, raw_proof: &[u8]) -> anyhow::Result<()> { let proto = akd::proto::specs::types::SingleAppendOnlyProof::parse_from_bytes(raw_proof)