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

Add local verification on plexi cli #15

Merged
merged 1 commit into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
21 changes: 21 additions & 0 deletions plexi_cli/src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::path::PathBuf;

use clap::{Parser, Subcommand};
use plexi_core::Epoch;

Expand Down Expand Up @@ -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<String>,
/// 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<PathBuf>,
/// Path to a file containing an epoch to verify
/// Format is { ciphersuite, namespace, timestamp, epoch, digest, signature }
signature_path_or_stdin: Option<PathBuf>,
},
}

#[allow(dead_code)]
Expand Down
137 changes: 117 additions & 20 deletions plexi_cli/src/cmd.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
use std::{
fmt, fs,
io::{self, Write},
io::{self, Read},
path::PathBuf,
time::Duration,
};

use akd::local_auditing::AuditBlobName;
Expand All @@ -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<PathBuf>) -> Result<Box<dyn io::Read>> {
let reader: Box<dyn io::Read> = match input {
Some(path) => Box::new(io::BufReader::new(
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<PathBuf>,
input: Option<PathBuf>,
) -> Result<String> {
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,
)
}
17 changes: 17 additions & 0 deletions plexi_cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::process;

mod cli;
mod cmd;
mod print;

#[tokio::main]
pub async fn main() -> anyhow::Result<()> {
Expand Down Expand Up @@ -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 {
Expand Down
22 changes: 22 additions & 0 deletions plexi_cli/src/print.rs
Original file line number Diff line number Diff line change
@@ -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())
}
32 changes: 31 additions & 1 deletion plexi_core/src/auditor.rs
Original file line number Diff line number Diff line change
@@ -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 _;
Expand Down Expand Up @@ -93,6 +98,31 @@ impl Configuration {
}
}

#[cfg(feature = "auditor")]
pub async fn compute_start_root_hash(raw_proof: &[u8]) -> anyhow::Result<Digest> {
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::<WhatsAppV1Configuration, _>(&manager).await?;
azks.batch_insert_nodes::<WhatsAppV1Configuration, _>(
&manager,
proof.unchanged_nodes.clone(),
InsertMode::Auditor,
)
.await?;

Ok(azks
.get_root_hash::<WhatsAppV1Configuration, _>(&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)
Expand Down
Loading