From f3fe01bfa8294af04d9a819819354e5e4cebde4b Mon Sep 17 00:00:00 2001 From: Kevin Lewi Date: Tue, 7 Apr 2026 13:32:17 -0700 Subject: [PATCH] Updating whatsapp-kt-auditor CLI --- examples/README.md | 15 +---- examples/src/whatsapp_kt_auditor/README.md | 45 +++++++++++++++ examples/src/whatsapp_kt_auditor/auditor.rs | 21 ------- examples/src/whatsapp_kt_auditor/mod.rs | 62 ++++++++++++++------- 4 files changed, 88 insertions(+), 55 deletions(-) create mode 100644 examples/src/whatsapp_kt_auditor/README.md diff --git a/examples/README.md b/examples/README.md index 9a878ed1..392809e5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -11,20 +11,7 @@ There are currently three examples supported in this library: ### WhatsApp Key Transparency Auditor -To run this example: -``` -cargo run -p examples --release -- whatsapp-kt-auditor -``` -and this will bring up an interactive interface which allows you to load the current epochs, and choose which epochs to audit. - -You can also automatically audit the latest epoch with the `-l` parameter (for "latest"), by running: -``` -cargo run -p examples --release -- whatsapp-kt-auditor -l -``` -or if you want to audit a specific epoch: -``` -cargo run -p examples --release -- whatsapp-kt-auditor -e 42 -``` +See [whatsapp_kt_auditor/README.md](src/whatsapp_kt_auditor/README.md) for full documentation. ### MySQL Demo diff --git a/examples/src/whatsapp_kt_auditor/README.md b/examples/src/whatsapp_kt_auditor/README.md new file mode 100644 index 00000000..9e56db0e --- /dev/null +++ b/examples/src/whatsapp_kt_auditor/README.md @@ -0,0 +1,45 @@ +# WhatsApp Key Transparency Auditor + +An independent auditor for verifying [WhatsApp key transparency](https://engineering.fb.com/2023/04/13/security/whatsapp-key-transparency/) audit proofs. This tool fetches published audit proofs from WhatsApp's CloudFront-backed storage and cryptographically verifies each epoch's append-only proof using the [AKD](https://github.com/facebook/akd) library. + +## Log Versions + +| Log | Epochs | Date Range (PT) | Description | +|-----|--------|-----------------|-------------| +| **v2** (default) | 722,606+ | 2026-03-13 18:41 PT – present | The current WhatsApp KT log, actively published to | +| **v1** | 1 – 2,026,881 | 2023-02-14 14:45 PT – 2026-03-13 08:37 PT | The legacy WhatsApp KT log (no longer updated) | + +The **v1** log contains historical audit proofs from WhatsApp's original key transparency deployment, covering epochs 1 through 2,026,881. It is no longer being published to. The **v2** log is a separate log starting from epoch 722,606 onward; epoch numbers between the two logs are not correlated. The v2 log is the log that WhatsApp actively publishes to going forward. Both logs use the same cryptographic configuration and proof format, so the auditor verifies them identically. + +By default, the auditor operates against the **v2** log. Use `--log v1` to audit proofs from the legacy log. + +## Usage + +### Audit the latest epoch + +```bash +cargo run -p examples --release -- whatsapp-kt-auditor -l +``` + +### Audit a specific epoch + +```bash +cargo run -p examples --release -- whatsapp-kt-auditor -e 722610 +``` + +### Interactive mode + +Load all available epochs and choose which to audit interactively: + +```bash +cargo run -p examples --release -- whatsapp-kt-auditor -i +``` + +### Auditing the legacy log + +Pass `--log v1` before the subcommand to audit the legacy log: + +```bash +cargo run -p examples --release -- whatsapp-kt-auditor --log v1 -l +cargo run -p examples --release -- whatsapp-kt-auditor --log v1 -e 42 +``` diff --git a/examples/src/whatsapp_kt_auditor/auditor.rs b/examples/src/whatsapp_kt_auditor/auditor.rs index 8460dec5..a69cde17 100644 --- a/examples/src/whatsapp_kt_auditor/auditor.rs +++ b/examples/src/whatsapp_kt_auditor/auditor.rs @@ -10,7 +10,6 @@ use super::EpochSummary; use anyhow::{anyhow, bail, Result}; -use clap::{Parser, Subcommand}; use std::convert::TryFrom; use xml::reader::XmlEvent; use xml::EventReader; @@ -20,26 +19,6 @@ const KEY_STR: &str = "Key"; const IS_TRUNCATED_STR: &str = "IsTruncated"; const TRUE_STR: &str = "true"; -/// Storage options for retrieving audit proofs -#[derive(Subcommand, Clone, Debug)] -pub enum AuditCommand { - /// Audit a specific epoch - Audit { - /// The epoch to audit - #[clap(long)] - epoch: u64, - }, - /// Load the available epochs to audit - LoadEpochs, -} - -/// Audit operations supported by the client -#[derive(Parser, Clone, Debug)] -pub struct AuditArgs { - #[clap(subcommand)] - command: AuditCommand, -} - pub(crate) async fn audit_epoch(blob: akd::local_auditing::AuditBlob) -> Result { // decode the proof let (end_epoch, p_hash, c_hash, proof) = blob.decode().map_err(|err| anyhow!("{:?}", err))?; diff --git a/examples/src/whatsapp_kt_auditor/mod.rs b/examples/src/whatsapp_kt_auditor/mod.rs index bdd05d7e..80942033 100644 --- a/examples/src/whatsapp_kt_auditor/mod.rs +++ b/examples/src/whatsapp_kt_auditor/mod.rs @@ -19,7 +19,27 @@ use std::convert::TryFrom; use std::time::Duration; // Default domain for WhatsApp's key transparency audit proofs -const WHATSAPP_KT_DOMAIN: &str = "https://d1tfr3x7n136ak.cloudfront.net"; +const WHATSAPP_KT_V1_DOMAIN: &str = "https://d1tfr3x7n136ak.cloudfront.net"; +const WHATSAPP_KT_V2_DOMAIN: &str = "https://d4ttn6vhp3mg0.cloudfront.net"; + +#[derive(Clone, Debug, Default, clap::ValueEnum)] +pub(crate) enum LogVersion { + /// The legacy WhatsApp KT log + V1, + /// The current WhatsApp KT log + #[default] + V2, +} + +impl LogVersion { + fn url(&self) -> &'static str { + match self { + LogVersion::V1 => WHATSAPP_KT_V1_DOMAIN, + LogVersion::V2 => WHATSAPP_KT_V2_DOMAIN, + } + } +} + type TC = akd::WhatsAppV1Configuration; /// Represents the summary of an epoch, and a unique key referring to the raw object in native storage (if needed) @@ -47,6 +67,10 @@ impl TryFrom<&str> for EpochSummary { #[derive(Parser, Debug, Clone)] #[clap(author, about, long_about = None)] pub(crate) struct CliArgs { + /// Which log version to audit (v1 = legacy, v2 = current) + #[clap(long, default_value = "v2")] + log: LogVersion, + /// The type of command to run #[clap(subcommand)] command: Command, @@ -54,17 +78,14 @@ pub(crate) struct CliArgs { #[derive(Debug, Clone, Subcommand)] enum Command { - /// For auditing a specific epoch - #[clap(short_flag = 'e', name = "Choose a specific epoch to audit")] + /// Audit a specific epoch + #[clap(short_flag = 'e')] Epoch { epoch: u64 }, - /// For auditing all epochs through an interactive interface - #[clap( - short_flag = 'i', - name = "Load all epochs to be audited (this can take some time...)" - )] + /// Load all epochs and choose which to audit interactively + #[clap(short_flag = 'i')] Interactive, - /// Indicate that only the latest epoch should be audited - #[clap(short_flag = 'l', name = "Audit only the latest epoch")] + /// Audit only the latest epoch + #[clap(short_flag = 'l')] AuditLatest, } @@ -80,21 +101,22 @@ struct CliOption { } pub(crate) async fn render_cli(args: CliArgs) -> Result<()> { + let url = args.log.url(); match args.command { Command::AuditLatest => { // Just audit the latest epoch and exit - let proofs = load_all_proofs().await?; + let proofs = load_all_proofs(url).await?; let latest_epoch_summary = proofs.last().expect("No epochs found"); - do_epoch_audit(latest_epoch_summary).await?; + do_epoch_audit(url, latest_epoch_summary).await?; return Ok(()); } Command::Epoch { epoch } => { - let epoch_summary = auditor::get_proof_from_epoch(WHATSAPP_KT_DOMAIN, epoch).await?; - do_epoch_audit(&epoch_summary).await?; + let epoch_summary = auditor::get_proof_from_epoch(url, epoch).await?; + do_epoch_audit(url, &epoch_summary).await?; return Ok(()); } Command::Interactive => { - let proofs = load_all_proofs().await?; + let proofs = load_all_proofs(url).await?; let items: Vec = vec![ CliOption { cli_type: CliType::Audit, @@ -137,7 +159,7 @@ pub(crate) async fn render_cli(args: CliArgs) -> Result<()> { let Some(epoch_summary) = maybe_proof else { bail!("Could not find epoch {epoch}"); }; - do_epoch_audit(epoch_summary).await?; + do_epoch_audit(url, epoch_summary).await?; } CliType::Quit => { break; @@ -154,16 +176,16 @@ pub(crate) async fn render_cli(args: CliArgs) -> Result<()> { Ok(()) } -async fn load_all_proofs() -> Result> { +async fn load_all_proofs(url: &str) -> Result> { let pb = start_progress_bar("Loading epochs..."); - let mut proofs = auditor::list_proofs(WHATSAPP_KT_DOMAIN).await?; + let mut proofs = auditor::list_proofs(url).await?; finish_progress_bar(pb, auditor::display_audit_proofs_info(&mut proofs)?); Ok(proofs) } -pub(crate) async fn do_epoch_audit(epoch_summary: &EpochSummary) -> Result<()> { +pub(crate) async fn do_epoch_audit(url: &str, epoch_summary: &EpochSummary) -> Result<()> { let pb1 = start_progress_bar("Downloading proof..."); - let proof = auditor::get_proof(WHATSAPP_KT_DOMAIN, epoch_summary).await?; + let proof = auditor::get_proof(url, epoch_summary).await?; finish_progress_bar( pb1, format!(