auths_cli/commands/
log.rs1use std::time::Duration;
2
3use anyhow::{Context, Result, bail};
4use auths_infra_http::HttpRegistryClient;
5use auths_sdk::ports::RegistryClient;
6use auths_transparency::SignedCheckpoint;
7use clap::{Args, Subcommand};
8use serde::Serialize;
9
10use super::executable::ExecutableCommand;
11use crate::config::CliConfig;
12use crate::ux::format::{JsonResponse, is_json_mode};
13
14#[derive(Args, Debug, Clone)]
15#[command(about = "Inspect and verify the transparency log")]
16pub struct LogCommand {
17 #[command(subcommand)]
18 pub command: LogSubcommand,
19}
20
21#[derive(Subcommand, Debug, Clone)]
22pub enum LogSubcommand {
23 Inspect(InspectArgs),
25 Verify(VerifyArgs),
27}
28
29#[derive(Args, Debug, Clone)]
30pub struct InspectArgs {
31 pub sequence: u128,
33
34 #[clap(long, default_value = "https://public.auths.dev")]
36 pub registry: String,
37}
38
39#[derive(Args, Debug, Clone)]
40pub struct VerifyArgs {
41 #[clap(long, default_value = "https://public.auths.dev")]
43 pub registry: String,
44}
45
46#[derive(Serialize)]
47struct VerifyResult {
48 consistent: bool,
49 cached_size: u64,
50 latest_size: u64,
51 cached_root: String,
52 latest_root: String,
53}
54
55impl ExecutableCommand for LogCommand {
56 fn execute(&self, _ctx: &CliConfig) -> Result<()> {
57 let rt = tokio::runtime::Runtime::new().context("Failed to create async runtime")?;
58 rt.block_on(async {
59 match &self.command {
60 LogSubcommand::Inspect(args) => handle_inspect(args).await,
61 LogSubcommand::Verify(args) => handle_verify(args).await,
62 }
63 })
64 }
65}
66
67async fn handle_inspect(args: &InspectArgs) -> Result<()> {
68 let registry_url = args.registry.trim_end_matches('/');
69 let client =
70 HttpRegistryClient::new_with_timeouts(Duration::from_secs(30), Duration::from_secs(60));
71
72 let path = format!("v1/log/entries/{}", args.sequence);
73 let response_bytes = client
74 .fetch_registry_data(registry_url, &path)
75 .await
76 .context("Failed to fetch log entry")?;
77
78 let entry: serde_json::Value =
79 serde_json::from_slice(&response_bytes).context("Failed to parse log entry response")?;
80
81 if is_json_mode() {
82 println!(
83 "{}",
84 serde_json::to_string_pretty(&entry).context("Failed to serialize entry")?
85 );
86 } else {
87 let entry_type = entry
88 .get("content")
89 .and_then(|c| c.get("entry_type"))
90 .and_then(|t| t.as_str())
91 .unwrap_or("unknown");
92 let actor = entry
93 .get("content")
94 .and_then(|c| c.get("actor_did"))
95 .and_then(|a| a.as_str())
96 .unwrap_or("unknown");
97 let timestamp = entry
98 .get("timestamp")
99 .and_then(|t| t.as_str())
100 .unwrap_or("unknown");
101 let sequence: u128 = entry
102 .get("sequence")
103 .and_then(|s| s.as_u64())
104 .map(u128::from)
105 .unwrap_or(args.sequence);
106
107 println!("Log Entry #{sequence}");
108 println!(" Type: {entry_type}");
109 println!(" Actor: {actor}");
110 println!(" Timestamp: {timestamp}");
111
112 if let Some(body) = entry.get("content").and_then(|c| c.get("body")) {
113 println!(
114 " Body: {}",
115 serde_json::to_string_pretty(body).unwrap_or_default()
116 );
117 }
118 }
119
120 Ok(())
121}
122
123#[allow(clippy::disallowed_methods)] async fn handle_verify(args: &VerifyArgs) -> Result<()> {
125 let cache_path = dirs::home_dir()
126 .map(|h| h.join(".auths").join("log_checkpoint.json"))
127 .ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?;
128
129 let cached_checkpoint: SignedCheckpoint = match std::fs::read_to_string(&cache_path) {
130 Ok(json) => serde_json::from_str(&json).context("Failed to parse cached checkpoint")?,
131 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
132 if is_json_mode() {
133 let result = VerifyResult {
134 consistent: false,
135 cached_size: 0,
136 latest_size: 0,
137 cached_root: String::new(),
138 latest_root: String::new(),
139 };
140 JsonResponse::success("log verify", result).print()?;
141 } else {
142 println!("No cached checkpoint found at {}", cache_path.display());
143 println!("Run 'auths artifact verify' with a bundle to establish initial trust.");
144 }
145 return Ok(());
146 }
147 Err(e) => return Err(e).context("Failed to read cached checkpoint"),
148 };
149
150 let registry_url = args.registry.trim_end_matches('/');
151 let client =
152 HttpRegistryClient::new_with_timeouts(Duration::from_secs(30), Duration::from_secs(60));
153
154 let response_bytes = client
155 .fetch_registry_data(registry_url, "v1/log/checkpoint")
156 .await
157 .context("Failed to fetch latest checkpoint from registry")?;
158
159 let latest_checkpoint: SignedCheckpoint =
160 serde_json::from_slice(&response_bytes).context("Failed to parse latest checkpoint")?;
161
162 let report = auths_sdk::workflows::transparency::try_cache_checkpoint(
163 &crate::adapters::config_store::FileConfigStore,
164 &cache_path,
165 &latest_checkpoint,
166 None,
167 );
168
169 match report {
170 Ok(consistency) => {
171 if is_json_mode() {
172 let result = VerifyResult {
173 consistent: consistency.consistent,
174 cached_size: consistency.old_size,
175 latest_size: consistency.new_size,
176 cached_root: hex::encode(cached_checkpoint.checkpoint.root.as_bytes()),
177 latest_root: hex::encode(latest_checkpoint.checkpoint.root.as_bytes()),
178 };
179 JsonResponse::success("log verify", result).print()?;
180 } else {
181 println!("Log Consistency: verified");
182 println!(
183 " Cached: size={}, root={}",
184 consistency.old_size,
185 hex::encode(cached_checkpoint.checkpoint.root.as_bytes())
186 );
187 println!(
188 " Latest: size={}, root={}",
189 consistency.new_size,
190 hex::encode(latest_checkpoint.checkpoint.root.as_bytes())
191 );
192 println!(" Checkpoint updated.");
193 }
194 }
195 Err(e) => {
196 if is_json_mode() {
197 let result = VerifyResult {
198 consistent: false,
199 cached_size: cached_checkpoint.checkpoint.size,
200 latest_size: latest_checkpoint.checkpoint.size,
201 cached_root: hex::encode(cached_checkpoint.checkpoint.root.as_bytes()),
202 latest_root: hex::encode(latest_checkpoint.checkpoint.root.as_bytes()),
203 };
204 let resp: JsonResponse<VerifyResult> = JsonResponse {
205 success: false,
206 command: "log verify".into(),
207 data: Some(result),
208 error: Some(e.to_string()),
209 };
210 println!(
211 "{}",
212 serde_json::to_string(&resp).context("Failed to serialize error response")?
213 );
214 } else {
215 eprintln!("Log Consistency: FAILED");
216 eprintln!(" Error: {e}");
217 eprintln!(" This may indicate a split-view attack.");
218 }
219 bail!("Log consistency verification failed: {e}");
220 }
221 }
222
223 Ok(())
224}