Skip to main content

auths_cli/commands/
multi_sig.rs

1//! `auths multi-sig` subcommand group — file-based multi-sig event aggregation.
2//!
3//! Wraps the SDK `workflows::multi_sig` module. Produces and consumes two
4//! on-disk formats:
5//!   - `UnsignedEventBundle` — canonical bytes + signer metadata; each device
6//!     reads this before signing.
7//!   - `IndexedSignature` — one partial signature per device.
8//!
9//! The final `combine` step validates threshold satisfaction and emits a
10//! ready-to-append `SignedEvent`.
11
12use anyhow::{Context, Result, anyhow};
13use clap::{Args, Subcommand};
14use std::path::PathBuf;
15use std::sync::Arc;
16
17use auths_keri::{IndexedSignature, SignedEvent, Threshold};
18use auths_sdk::keychain::{KeyAlias, get_platform_keychain_with_config};
19use auths_sdk::workflows::multi_sig::{
20    begin_multi_sig_event, combine, read_partial, sign_partial, write_partial,
21};
22
23use crate::commands::executable::ExecutableCommand;
24use crate::config::CliConfig;
25
26/// `auths multi-sig ...` — aggregate indexed signatures for multi-device
27/// KEL events.
28#[derive(Args, Debug)]
29pub struct MultiSigCommand {
30    #[command(subcommand)]
31    pub subcommand: MultiSigSubcommand,
32}
33
34#[derive(Subcommand, Debug)]
35#[command(rename_all = "lowercase")]
36pub enum MultiSigSubcommand {
37    /// Create an unsigned-event bundle for distribution to signers.
38    Begin {
39        /// Path to the JSON-encoded finalized `SignedEvent` (signatures empty).
40        #[arg(long)]
41        event: PathBuf,
42
43        /// Comma-separated signer aliases, in slot order.
44        #[arg(long, value_delimiter = ',')]
45        signers: Vec<String>,
46
47        /// Output path for the unsigned bundle.
48        #[arg(long)]
49        output: PathBuf,
50    },
51
52    /// Sign an unsigned bundle with a single device key.
53    Sign {
54        /// Path to the unsigned-event bundle.
55        #[arg(long)]
56        unsigned: PathBuf,
57
58        /// Keychain alias of the signing key.
59        #[arg(long)]
60        key_alias: String,
61
62        /// Slot index for the indexed signature (0-based).
63        #[arg(long)]
64        index: u32,
65
66        /// Output path for the partial signature.
67        #[arg(long)]
68        output: PathBuf,
69    },
70
71    /// Combine partial signatures into a SignedEvent, enforcing the threshold.
72    Combine {
73        /// Path to the unsigned-event bundle.
74        #[arg(long)]
75        unsigned: PathBuf,
76
77        /// Comma-separated paths to partial signature files.
78        #[arg(long, value_delimiter = ',')]
79        partials: Vec<PathBuf>,
80
81        /// Expected threshold (scalar `"2"` or fractions `"1/2,1/2,1/2"`).
82        #[arg(long)]
83        threshold: String,
84
85        /// Number of signer slots expected (device_count).
86        #[arg(long)]
87        key_count: usize,
88
89        /// Output path for the combined SignedEvent JSON.
90        #[arg(long)]
91        output: PathBuf,
92    },
93}
94
95impl ExecutableCommand for MultiSigCommand {
96    fn execute(&self, ctx: &CliConfig) -> Result<()> {
97        match &self.subcommand {
98            MultiSigSubcommand::Begin {
99                event,
100                signers,
101                output,
102            } => {
103                let raw = std::fs::read(event)
104                    .with_context(|| format!("reading event from {event:?}"))?;
105                let signed: SignedEvent = serde_json::from_slice(&raw)
106                    .with_context(|| format!("parsing SignedEvent from {event:?}"))?;
107                let aliases: Vec<KeyAlias> = signers
108                    .iter()
109                    .map(|s| KeyAlias::new_unchecked(s.to_string()))
110                    .collect();
111                let bundle = begin_multi_sig_event(&signed, &aliases, output)
112                    .with_context(|| "begin_multi_sig_event failed")?;
113                println!(
114                    "[OK] Unsigned bundle written to {:?} (SAID {}, {} signer slots)",
115                    output,
116                    bundle.said,
117                    aliases.len()
118                );
119                Ok(())
120            }
121
122            &MultiSigSubcommand::Sign {
123                ref unsigned,
124                ref key_alias,
125                index,
126                ref output,
127            } => {
128                let keychain: Arc<dyn auths_sdk::keychain::KeyStorage + Send + Sync> = Arc::from(
129                    get_platform_keychain_with_config(&ctx.env_config)
130                        .context("Failed to access keychain")?,
131                );
132                let alias = KeyAlias::new_unchecked(key_alias.clone());
133                let partial = sign_partial(
134                    unsigned,
135                    &alias,
136                    index,
137                    ctx.passphrase_provider.as_ref(),
138                    keychain.as_ref(),
139                )
140                .with_context(|| "sign_partial failed")?;
141                write_partial(&partial, output)
142                    .with_context(|| format!("writing partial to {output:?}"))?;
143                println!(
144                    "[OK] Partial signature written to {:?} (index {}, {} bytes)",
145                    output,
146                    partial.index,
147                    partial.sig.len()
148                );
149                Ok(())
150            }
151
152            &MultiSigSubcommand::Combine {
153                ref unsigned,
154                ref partials,
155                ref threshold,
156                key_count,
157                ref output,
158            } => {
159                let expected_kt: Threshold =
160                    crate::commands::init::parse_threshold_cli(threshold, key_count)?;
161                let loaded: Result<Vec<IndexedSignature>> = partials
162                    .iter()
163                    .map(|p| read_partial(p).map_err(|e| anyhow!("reading partial {:?}: {e}", p)))
164                    .collect();
165                let loaded = loaded?;
166                let signed =
167                    combine(unsigned, loaded, &expected_kt).with_context(|| "combine failed")?;
168                let body = serde_json::to_vec_pretty(&signed)
169                    .with_context(|| "serializing SignedEvent")?;
170                std::fs::write(output, &body)
171                    .with_context(|| format!("writing combined event to {output:?}"))?;
172                println!(
173                    "[OK] Combined SignedEvent written to {:?} ({} signatures)",
174                    output,
175                    signed.signatures.len()
176                );
177                Ok(())
178            }
179        }
180    }
181}