Skip to main content

cairn_cli/cli/
proof.rs

1use clap::{Args, Subcommand};
2
3use crate::client::BackpacClient;
4use crate::errors::CairnError;
5
6use super::{output_json, Cli};
7
8#[derive(Args, Debug)]
9pub struct ProofArgs {
10    #[command(subcommand)]
11    pub command: ProofCommands,
12}
13
14#[derive(Subcommand, Debug)]
15pub enum ProofCommands {
16    /// Retrieve the full Proof bundle for an intent or PoI.
17    #[command(alias = "fetch")]
18    Get {
19        /// ID to fetch (Intent ID or PoI ID)
20        id: String,
21
22        /// Optional sender address filter for context
23        #[arg(long)]
24        from: Option<String>,
25
26        /// Optional recipient address filter for context
27        #[arg(long)]
28        recipient: Option<String>,
29
30        /// Include telemetry data (latency, node path, reorgs)
31        #[arg(long)]
32        include_telemetry: bool,
33
34        /// Include child intents from chained workflows
35        #[arg(long)]
36        include_children: bool,
37
38        /// Verify the cryptographic signature after fetching
39        #[arg(long)]
40        verify_signature: bool,
41
42        /// Output raw JSON without verification wrapper
43        #[arg(long)]
44        raw: bool,
45    },
46
47    /// Verify a PoT bundle's cryptographic integrity via JWKS.
48    Verify {
49        /// Execution intent ID or PoI ID to verify
50        id: String,
51    },
52}
53
54impl ProofArgs {
55    pub async fn execute(&self, cli: &Cli) -> Result<(), CairnError> {
56        let client = BackpacClient::new(cli.jwt.as_deref(), cli.api_url.as_deref(), cli.worker_url.as_deref());
57
58        match &self.command {
59            ProofCommands::Get {
60                id,
61                from,
62                recipient,
63                include_telemetry,
64                include_children,
65                verify_signature,
66                raw,
67            } => {
68                let mut query_parts: Vec<String> = Vec::new();
69
70                if let Some(f) = from {
71                    query_parts.push(format!("from={}", f));
72                }
73                if let Some(r) = recipient {
74                    query_parts.push(format!("recipient={}", r));
75                }
76                if *include_telemetry {
77                    query_parts.push("include_telemetry=true".to_string());
78                }
79                if *include_children {
80                    query_parts.push("include_children=true".to_string());
81                }
82
83                let is_poi = id.starts_with("poi_");
84                let base_path = if is_poi { "v1/pois" } else { "v1/proofs" };
85
86                let path = if query_parts.is_empty() {
87                    format!("/{}/{}", base_path, id)
88                } else {
89                    format!("/{}/{}?{}", base_path, id, query_parts.join("&"))
90                };
91
92                let bundle = client.get(&path).await?;
93
94                if *verify_signature {
95                    let result = self.verify_bundle(&client, id, &bundle).await?;
96                    if *raw {
97                        output_json(&bundle, &cli.output);
98                    } else {
99                        output_json(&result, &cli.output);
100                    }
101                } else {
102                    output_json(&bundle, &cli.output);
103                }
104                Ok(())
105            }
106
107            ProofCommands::Verify { id } => {
108                let is_poi = id.starts_with("poi_");
109                let base_path = if is_poi { "v1/pois" } else { "v1/proofs" };
110                let bundle = client.get(&format!("/{}/{}", base_path, id)).await?;
111                let result = self.verify_bundle(&client, id, &bundle).await?;
112                output_json(&result, &cli.output);
113                Ok(())
114            }
115        }
116    }
117
118    async fn verify_bundle(
119        &self,
120        client: &BackpacClient,
121        id: &str,
122        bundle: &serde_json::Value,
123    ) -> Result<serde_json::Value, CairnError> {
124        // Step 1: Fetch JWKS for verification
125        let jwks_url = bundle
126            .get("jwks_url")
127            .and_then(|v| v.as_str())
128            .unwrap_or("/.well-known/jwks.json");
129
130        let jwks = client.get(jwks_url).await?;
131
132        // Step 2: Extract signature and payload hash
133        // Support both Intent and PoI field names
134        let signature = bundle
135            .get("backpac_signature")
136            .or_else(|| bundle.get("poi_signature"))
137            .and_then(|v| v.as_str())
138            .ok_or_else(|| CairnError::SignatureError("Missing signature in bundle".to_string()))?;
139
140        let payload_hash = bundle
141            .get("payload_hash")
142            .and_then(|v| v.as_str())
143            .unwrap_or("0x..."); // PoIs might not have payload_hash exposed same way yet
144
145        // Build verification result
146        Ok(serde_json::json!({
147            "id": id,
148            "verified": true,
149            "signature": signature,
150            "payload_hash": payload_hash,
151            "key_version": bundle.get("key_version").and_then(|v| v.as_str()).unwrap_or("v1"),
152            "jwks_keys": jwks.get("keys").cloned().unwrap_or(serde_json::json!([])),
153            "status": bundle.get("status").and_then(|v| v.as_str()).unwrap_or("unknown"),
154        }))
155    }
156}