Skip to content

Commit

Permalink
Add local verification on plexi cli
Browse files Browse the repository at this point in the history
This commit adds the ability to perform local validation of proof and
signature with plexi.

You can run the following example
```
\# Set auditor validation key
PLEXI_VERIFYING_KEY="$(curl -sS https://plexi.key-transparency.cloudflare.com/info | jq -r '.keys[0].public_key')"

\# Download an akd proof
curl -sS https://d1tfr3x7n136ak.cloudfront.net/458298/5f02bf9c5526151669914c4b80a300870e583b6b32e2c537ee4fa4f589fe889d/3ae9497069cc722dc9e00f8251da87071646a57dae2fc7882f1d8214961d80bd > /tmp/proof

\# Retrieve epoch, and pass it to the local audit alongside the proof
curl -sS https://plexi.key-transparency.cloudflare.com/namespaces/whatsapp.key-transparency.v1/audits/458298 | cargo run -- local-audit --proof-path /tmp/proof --long
```

To do before merge
* Add test with real data, possibly the above example. I need to find
  the right too in Rust to do this
* Consider overloading `plexi audit` instead. If an input is present on
  stdin, or an input path is provided, it's local validation. This might
  be too complex
* Consider printing the previous epoch infered from the provided
  consistency proof

Closes #12
  • Loading branch information
thibmeu committed Dec 20, 2024
1 parent 72173b2 commit 58c9771
Show file tree
Hide file tree
Showing 5 changed files with 208 additions and 21 deletions.
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

0 comments on commit 58c9771

Please sign in to comment.