Skip to main content

auths_cli/commands/
ipex.rs

1//! `auths ipex` — IPEX (Issuance & Presentation EXchange) credential handover.
2//!
3//! IPEX is KERI's standard peer-to-peer handshake for moving an ACDC credential
4//! between two controllers: the discloser sends a `grant` `exn` carrying the
5//! credential, and the holder answers with an `admit` `exn` that references the
6//! grant's SAID. It is the interoperable alternative to a bespoke presentation
7//! wire — a credential exchanged this way is one keripy/KERIA can ingest, and a
8//! grant a keripy peer sends is one auths can parse.
9//!
10//! Two directions, mirroring the two roles in a disclosure:
11//!
12//! * `auths ipex grant` — discloser → holder: read a saidified ACDC, embed it in
13//!   a `/ipex/grant` `exn` addressed to the recipient, and print the `exn`.
14//! * `auths ipex admit` — holder → discloser: read a peer's grant `exn`, verify
15//!   it (and the credential inside it), and print an `/ipex/admit` `exn` whose
16//!   prior is the grant's SAID.
17//!
18//! The wire definitions (the `exn` records, their SAIDs, the embeds block) live
19//! in `auths-keri::ipex` and are byte-exact with keripy 1.3.4; this is a thin
20//! file-based CLI adapter over them. Signing the `exn` and putting it on a
21//! transport are the caller's concern — this surface produces the canonical
22//! bytes to sign and send.
23
24use std::path::{Path, PathBuf};
25
26use anyhow::{Result, anyhow};
27use auths_keri::{Acdc, IpexAdmit, IpexGrant, Prefix};
28use auths_utils::path::expand_tilde;
29use clap::{Parser, Subcommand};
30
31use crate::config::CliConfig;
32
33/// Default datetime stamp — the epoch, so output is deterministic unless the
34/// operator passes a real `now`. Matches the OOBI command's convention.
35const DEFAULT_DT: &str = "1970-01-01T00:00:00.000000+00:00";
36
37/// Exchange an ACDC credential over IPEX (grant/admit), interoperable with keripy/KERIA.
38#[derive(Parser, Debug, Clone)]
39#[command(
40    about = "Exchange a credential over IPEX (grant/admit) — interoperable with keripy/KERIA",
41    after_help = "Examples:
42  auths ipex grant --acdc cred.json --sender EOoC... --recipient EBHn...
43  auths ipex admit --grant grant.json --sender EBHn..."
44)]
45pub struct IpexCommand {
46    /// The IPEX direction to run.
47    #[command(subcommand)]
48    pub action: IpexAction,
49}
50
51/// The two IPEX directions: grant a credential, or admit a received grant.
52#[derive(Subcommand, Debug, Clone)]
53pub enum IpexAction {
54    /// Grant (disclose) a credential: embed an ACDC in a `/ipex/grant` `exn`.
55    Grant(GrantArgs),
56    /// Admit (accept) a received grant: emit an `/ipex/admit` `exn` for it.
57    Admit(AdmitArgs),
58}
59
60/// `auths ipex grant` — disclose a credential to a holder.
61#[derive(Parser, Debug, Clone)]
62pub struct GrantArgs {
63    /// The saidified ACDC credential to disclose (its JSON body, `{v,d,i,ri,s,a}`).
64    #[clap(long, value_name = "ACDC.json")]
65    pub acdc: PathBuf,
66
67    /// The discloser (sender) AID granting the credential.
68    #[clap(long, value_name = "AID")]
69    pub sender: String,
70
71    /// The recipient (holder) AID the credential is granted to.
72    #[clap(long, value_name = "AID")]
73    pub recipient: String,
74
75    /// Optional human-readable disclosure message (`a.m`).
76    #[clap(long, default_value = "")]
77    pub message: String,
78
79    /// Timestamp (RFC 3339) to stamp the `exn` with. Defaults to the epoch so
80    /// output stays deterministic; pass the real `now` to send.
81    #[clap(long, default_value = DEFAULT_DT)]
82    pub dt: String,
83}
84
85/// `auths ipex admit` — accept a credential a peer granted.
86#[derive(Parser, Debug, Clone)]
87pub struct AdmitArgs {
88    /// The peer's grant `exn` to admit (its JSON body).
89    #[clap(long, value_name = "GRANT.json")]
90    pub grant: PathBuf,
91
92    /// The holder (sender) AID admitting the disclosure.
93    #[clap(long, value_name = "AID")]
94    pub sender: String,
95
96    /// Optional human-readable admission message (`a.m`).
97    #[clap(long, default_value = "")]
98    pub message: String,
99
100    /// Timestamp (RFC 3339) to stamp the `exn` with. Defaults to the epoch so
101    /// output stays deterministic; pass the real `now` to send.
102    #[clap(long, default_value = DEFAULT_DT)]
103    pub dt: String,
104}
105
106impl IpexCommand {
107    /// Run the command (grant a credential or admit a received grant).
108    pub fn execute(&self, _ctx: &CliConfig) -> Result<()> {
109        match &self.action {
110            IpexAction::Grant(args) => args.run(),
111            IpexAction::Admit(args) => args.run(),
112        }
113    }
114}
115
116impl GrantArgs {
117    fn run(&self) -> Result<()> {
118        // Parse the AIDs at the boundary — an invalid prefix never reaches the
119        // grant builder.
120        let sender = Prefix::new(self.sender.clone())
121            .map_err(|e| anyhow!("parse sender AID {:?}: {e}", self.sender))?;
122        let recipient = Prefix::new(self.recipient.clone())
123            .map_err(|e| anyhow!("parse recipient AID {:?}: {e}", self.recipient))?;
124
125        let acdc = read_acdc(&self.acdc)?;
126
127        let grant = IpexGrant::new(
128            sender,
129            recipient,
130            acdc,
131            self.message.clone(),
132            self.dt.clone(),
133        )
134        .map_err(|e| anyhow!("build IPEX grant: {e}"))?;
135        println!("{}", serde_json::to_string(&grant)?);
136        Ok(())
137    }
138}
139
140impl AdmitArgs {
141    fn run(&self) -> Result<()> {
142        let sender = Prefix::new(self.sender.clone())
143            .map_err(|e| anyhow!("parse sender AID {:?}: {e}", self.sender))?;
144
145        // Parse the peer's grant — its `exn` SAID and the embedded ACDC are both
146        // verified by `IpexGrant::parse`, so we never admit a tampered grant.
147        let path = expand_tilde(&self.grant)?;
148        let json = std::fs::read_to_string(&path)
149            .map_err(|e| anyhow!("read grant {}: {e}", path.display()))?;
150        let grant = IpexGrant::parse(&json).map_err(|e| anyhow!("parse IPEX grant: {e}"))?;
151
152        let admit = IpexAdmit::new(
153            sender,
154            grant.d.clone(),
155            self.message.clone(),
156            self.dt.clone(),
157        )
158        .map_err(|e| anyhow!("build IPEX admit: {e}"))?;
159        println!("{}", serde_json::to_string(&admit)?);
160        Ok(())
161    }
162}
163
164/// Reads a saidified ACDC from a JSON file, verifying its SAID at the boundary —
165/// a grant cannot disclose a credential that doesn't stand on its own digest.
166fn read_acdc(acdc_path: &Path) -> Result<Acdc> {
167    let path = expand_tilde(acdc_path)?;
168    let json =
169        std::fs::read_to_string(&path).map_err(|e| anyhow!("read ACDC {}: {e}", path.display()))?;
170    let acdc: Acdc =
171        serde_json::from_str(&json).map_err(|e| anyhow!("parse ACDC {}: {e}", path.display()))?;
172    acdc.verify_said()
173        .map_err(|e| anyhow!("verify ACDC SAID {}: {e}", path.display()))?;
174    Ok(acdc)
175}