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}