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