diff --git a/crates/quicnprotochat-client/src/lib.rs b/crates/quicnprotochat-client/src/lib.rs index 7418d83..e5cbb69 100644 --- a/crates/quicnprotochat-client/src/lib.rs +++ b/crates/quicnprotochat-client/src/lib.rs @@ -66,6 +66,85 @@ pub fn init_auth(ctx: ClientAuth) { // -- Subcommand implementations ----------------------------------------------- +/// Print local identity information from the state file (no server connection). +pub fn cmd_whoami(state_path: &Path, password: Option<&str>) -> anyhow::Result<()> { + let state = load_existing_state(state_path, password)?; + let identity = IdentityKeypair::from_seed(state.identity_seed); + + let pk_bytes = identity.public_key_bytes(); + let fingerprint = sha256(&pk_bytes); + + println!("identity_key : {}", hex::encode(&pk_bytes)); + println!("fingerprint : {}", hex::encode(&fingerprint)); + println!( + "hybrid_key : {}", + if state.hybrid_key.is_some() { + "present (X25519 + ML-KEM-768)" + } else { + "not generated" + } + ); + println!( + "group : {}", + if state.group.is_some() { + "active" + } else { + "none" + } + ); + println!("state_file : {}", state_path.display()); + + Ok(()) +} + +/// Check server connectivity via the health RPC. +pub async fn cmd_health(server: &str, ca_cert: &Path, server_name: &str) -> anyhow::Result<()> { + let sent_at = current_timestamp_ms(); + let client = connect_node(server, ca_cert, server_name).await?; + + let req = client.health_request(); + let resp = req.send().promise.await.context("health RPC failed")?; + + let status = resp + .get() + .context("health: bad response")? + .get_status() + .context("health: missing status")? + .to_str() + .unwrap_or("invalid"); + + let rtt_ms = current_timestamp_ms().saturating_sub(sent_at); + + println!("server : {server}"); + println!("status : {status}"); + println!("rtt : {rtt_ms}ms"); + Ok(()) +} + +/// Check if a peer identity has registered a hybrid public key (non-consuming). +pub async fn cmd_check_key( + server: &str, + ca_cert: &Path, + server_name: &str, + identity_key_hex: &str, +) -> anyhow::Result<()> { + let identity_key = decode_identity_key(identity_key_hex)?; + let node_client = connect_node(server, ca_cert, server_name).await?; + + let hybrid_pk = fetch_hybrid_key(&node_client, &identity_key).await?; + + println!("identity_key : {identity_key_hex}"); + println!( + "hybrid_key : {}", + if hybrid_pk.is_some() { + "available (X25519 + ML-KEM-768)" + } else { + "not found" + } + ); + Ok(()) +} + /// Connect to `server`, call health, and print RTT over QUIC/TLS. pub async fn cmd_ping(server: &str, ca_cert: &Path, server_name: &str) -> anyhow::Result<()> { let sent_at = current_timestamp_ms(); diff --git a/crates/quicnprotochat-client/src/main.rs b/crates/quicnprotochat-client/src/main.rs index f928b0f..0406a74 100644 --- a/crates/quicnprotochat-client/src/main.rs +++ b/crates/quicnprotochat-client/src/main.rs @@ -5,8 +5,9 @@ use std::path::PathBuf; use clap::{Parser, Subcommand}; use quicnprotochat_client::{ - cmd_create_group, cmd_demo_group, cmd_fetch_key, cmd_invite, cmd_join, cmd_login, cmd_ping, - cmd_recv, cmd_register, cmd_register_state, cmd_register_user, cmd_send, init_auth, ClientAuth, + cmd_check_key, cmd_create_group, cmd_demo_group, cmd_fetch_key, cmd_health, cmd_invite, + cmd_join, cmd_login, cmd_ping, cmd_recv, cmd_register, cmd_register_state, cmd_register_user, + cmd_send, cmd_whoami, init_auth, ClientAuth, }; // ── CLI ─────────────────────────────────────────────────────────────────────── @@ -79,6 +80,34 @@ enum Command { password: String, }, + /// Show local identity key, fingerprint, group status, and hybrid key status. + Whoami { + /// State file path (identity + MLS state). + #[arg( + long, + default_value = "quicnprotochat-state.bin", + env = "QUICNPROTOCHAT_STATE" + )] + state: PathBuf, + }, + + /// Check server connectivity and print status. + Health { + /// Server address (host:port). + #[arg(long, default_value = "127.0.0.1:7000", env = "QUICNPROTOCHAT_SERVER")] + server: String, + }, + + /// Check if a peer has registered a hybrid key (non-consuming lookup). + CheckKey { + /// Server address (host:port). + #[arg(long, default_value = "127.0.0.1:7000", env = "QUICNPROTOCHAT_SERVER")] + server: String, + + /// Peer's Ed25519 identity public key (64 hex chars = 32 bytes). + identity_key: String, + }, + /// Send a Ping to the server and print the round-trip time. Ping { /// Server address (host:port). @@ -262,6 +291,27 @@ async fn main() -> anyhow::Result<()> { )) .await } + Command::Whoami { state } => cmd_whoami(&state, state_pw), + Command::Health { server } => { + let local = tokio::task::LocalSet::new(); + local + .run_until(cmd_health(&server, &args.ca_cert, &args.server_name)) + .await + } + Command::CheckKey { + server, + identity_key, + } => { + let local = tokio::task::LocalSet::new(); + local + .run_until(cmd_check_key( + &server, + &args.ca_cert, + &args.server_name, + &identity_key, + )) + .await + } Command::Ping { server } => cmd_ping(&server, &args.ca_cert, &args.server_name).await, Command::Register { server } => { let local = tokio::task::LocalSet::new();