1use std::fs;
18use std::path::{Path, PathBuf};
19
20use auths_core::crypto::signer::decrypt_keypair;
21use auths_core::signing::PassphraseProvider;
22use auths_core::storage::keychain::{KeyAlias, KeyStorage};
23use auths_keri::{
24 Event, IndexedSignature, SignedEvent, Threshold, serialize_for_signing, validate_signed_event,
25};
26use serde::{Deserialize, Serialize};
27
28#[derive(Debug, thiserror::Error)]
30#[non_exhaustive]
31pub enum MultiSigError {
32 #[error("I/O error: {0}")]
34 Io(#[from] std::io::Error),
35
36 #[error("Serialization error: {0}")]
38 Serialization(String),
39
40 #[error("Keychain error: {0}")]
42 Keychain(String),
43
44 #[error("Signing failed: {0}")]
46 Signing(String),
47
48 #[error(
50 "Threshold not met: verified indices {verified:?} do not satisfy expected threshold (key_count={key_count})"
51 )]
52 ThresholdNotMet {
53 verified: Vec<u32>,
55 key_count: usize,
57 },
58
59 #[error("Signer index {index} out of range (key_count={key_count})")]
61 IndexOutOfRange {
62 index: u32,
64 key_count: usize,
66 },
67
68 #[error("Validation error: {0}")]
70 Validation(String),
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct UnsignedEventBundle {
77 pub event: Event,
79 pub signer_aliases: Vec<String>,
81 #[serde(with = "hex::serde")]
85 pub canonical_bytes: Vec<u8>,
86 pub said: String,
88}
89
90pub fn begin_multi_sig_event(
93 event: &SignedEvent,
94 signers: &[KeyAlias],
95 output_path: &Path,
96) -> Result<UnsignedEventBundle, MultiSigError> {
97 let canonical = serialize_for_signing(&event.event)
98 .map_err(|e| MultiSigError::Serialization(e.to_string()))?;
99
100 let bundle = UnsignedEventBundle {
101 event: event.event.clone(),
102 signer_aliases: signers.iter().map(|a| a.to_string()).collect(),
103 canonical_bytes: canonical,
104 said: event.event.said().as_str().to_string(),
105 };
106
107 let json = serde_json::to_vec_pretty(&bundle)
108 .map_err(|e| MultiSigError::Serialization(e.to_string()))?;
109 fs::write(output_path, &json)?;
110 Ok(bundle)
111}
112
113pub fn sign_partial(
119 unsigned_bundle_path: &Path,
120 key_alias: &KeyAlias,
121 signer_index: u32,
122 passphrase_provider: &dyn PassphraseProvider,
123 keychain: &(dyn KeyStorage + Send + Sync),
124) -> Result<IndexedSignature, MultiSigError> {
125 let raw = fs::read(unsigned_bundle_path)?;
126 let bundle: UnsignedEventBundle =
127 serde_json::from_slice(&raw).map_err(|e| MultiSigError::Serialization(e.to_string()))?;
128
129 let keys = match &bundle.event {
130 Event::Icp(icp) => &icp.k,
131 Event::Rot(rot) => &rot.k,
132 Event::Dip(dip) => &dip.k,
133 Event::Drt(drt) => &drt.k,
134 Event::Ixn(_) => {
135 return Err(MultiSigError::Validation(
136 "ixn events do not carry their own key list; multi-sig on interaction events uses the controller KEL state".to_string(),
137 ));
138 }
139 };
140 if (signer_index as usize) >= keys.len() {
141 return Err(MultiSigError::IndexOutOfRange {
142 index: signer_index,
143 key_count: keys.len(),
144 });
145 }
146
147 let (_did, _role, encrypted) = keychain
148 .load_key(key_alias)
149 .map_err(|e| MultiSigError::Keychain(e.to_string()))?;
150 let passphrase = passphrase_provider
151 .get_passphrase(&format!(
152 "Enter passphrase for multi-sig key '{}':",
153 key_alias
154 ))
155 .map_err(|e| MultiSigError::Keychain(e.to_string()))?;
156 let decrypted = decrypt_keypair(&encrypted, &passphrase)
157 .map_err(|e| MultiSigError::Keychain(e.to_string()))?;
158
159 use ring::signature::{Ed25519KeyPair, KeyPair};
162 let keypair = Ed25519KeyPair::from_pkcs8(&decrypted)
163 .map_err(|e| MultiSigError::Signing(format!("Ed25519 load: {e}")))?;
164 let _pub_bytes = keypair.public_key().as_ref().to_vec();
165 let sig = keypair.sign(&bundle.canonical_bytes);
166
167 Ok(IndexedSignature {
168 index: signer_index,
169 prior_index: None,
170 sig: sig.as_ref().to_vec(),
171 })
172}
173
174pub fn combine(
178 unsigned_bundle_path: &Path,
179 partials: Vec<IndexedSignature>,
180 expected_kt: &Threshold,
181) -> Result<SignedEvent, MultiSigError> {
182 let raw = fs::read(unsigned_bundle_path)?;
183 let bundle: UnsignedEventBundle =
184 serde_json::from_slice(&raw).map_err(|e| MultiSigError::Serialization(e.to_string()))?;
185
186 let keys_len = match &bundle.event {
187 Event::Icp(icp) => icp.k.len(),
188 Event::Rot(rot) => rot.k.len(),
189 Event::Dip(dip) => dip.k.len(),
190 Event::Drt(drt) => drt.k.len(),
191 Event::Ixn(_) => 0,
192 };
193
194 let signed = SignedEvent::new(bundle.event.clone(), partials.clone());
198 validate_signed_event(&signed, None).map_err(|e| MultiSigError::Validation(e.to_string()))?;
199
200 let verified: Vec<u32> = partials.iter().map(|p| p.index).collect();
204 if !expected_kt.is_satisfied(&verified, keys_len) {
205 return Err(MultiSigError::ThresholdNotMet {
206 verified,
207 key_count: keys_len,
208 });
209 }
210
211 Ok(signed)
212}
213
214pub fn write_partial(
217 partial: &IndexedSignature,
218 output_path: &Path,
219) -> Result<PathBuf, MultiSigError> {
220 let json = serde_json::to_vec_pretty(partial)
221 .map_err(|e| MultiSigError::Serialization(e.to_string()))?;
222 fs::write(output_path, &json)?;
223 Ok(output_path.to_path_buf())
224}
225
226pub fn read_partial(path: &Path) -> Result<IndexedSignature, MultiSigError> {
228 let raw = fs::read(path)?;
229 serde_json::from_slice(&raw).map_err(|e| MultiSigError::Serialization(e.to_string()))
230}
231
232#[cfg(test)]
233#[allow(clippy::unwrap_used, clippy::expect_used)]
234mod tests {
235 use super::*;
236 use auths_keri::{
237 CesrKey, Fraction, IcpEvent, KeriSequence, Prefix, Said, VersionString, finalize_icp_event,
238 };
239
240 use ring::rand::SystemRandom;
241 use ring::signature::{Ed25519KeyPair, KeyPair};
242 use tempfile::tempdir;
243
244 fn gen_keypair() -> Ed25519KeyPair {
245 let rng = SystemRandom::new();
246 let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
247 Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap()
248 }
249
250 fn cesr_pub(kp: &Ed25519KeyPair) -> String {
251 auths_keri::KeriPublicKey::ed25519(kp.public_key().as_ref())
252 .unwrap()
253 .to_qb64()
254 .unwrap()
255 }
256
257 fn half() -> Fraction {
258 Fraction {
259 numerator: 1,
260 denominator: 2,
261 }
262 }
263
264 fn make_three_key_icp() -> (IcpEvent, [Ed25519KeyPair; 3]) {
265 let kps = [gen_keypair(), gen_keypair(), gen_keypair()];
266 let k = vec![
267 CesrKey::new_unchecked(cesr_pub(&kps[0])),
268 CesrKey::new_unchecked(cesr_pub(&kps[1])),
269 CesrKey::new_unchecked(cesr_pub(&kps[2])),
270 ];
271 let n = vec![
272 Said::new_unchecked("EFakeNext0000000000000000000000000000000000".to_string()),
273 Said::new_unchecked("EFakeNext0000000000000000000000000000000001".to_string()),
274 Said::new_unchecked("EFakeNext0000000000000000000000000000000002".to_string()),
275 ];
276 let kt = Threshold::Weighted(vec![vec![half(), half(), half()]]);
277
278 let icp = IcpEvent {
279 v: VersionString::placeholder(),
280 d: Said::default(),
281 i: Prefix::default(),
282 s: KeriSequence::new(0),
283 kt,
284 k,
285 nt: Threshold::Weighted(vec![vec![half(), half(), half()]]),
286 n,
287 bt: Threshold::Simple(0),
288 b: vec![],
289 c: vec![],
290 a: vec![],
291 };
292 (finalize_icp_event(icp).unwrap(), kps)
293 }
294
295 #[test]
296 fn combine_threshold_not_met_with_one_partial() {
297 let (icp, kps) = make_three_key_icp();
298 let event = Event::Icp(icp.clone());
299 let unsigned = SignedEvent::new(event.clone(), vec![]);
300
301 let dir = tempdir().unwrap();
302 let bundle_path = dir.path().join("unsigned.json");
303 begin_multi_sig_event(
304 &unsigned,
305 &[
306 KeyAlias::new_unchecked("dev-a"),
307 KeyAlias::new_unchecked("dev-b"),
308 KeyAlias::new_unchecked("dev-c"),
309 ],
310 &bundle_path,
311 )
312 .unwrap();
313
314 let canonical = serialize_for_signing(&event).unwrap();
315 let partial0 = IndexedSignature {
316 index: 0,
317 prior_index: None,
318 sig: kps[0].sign(&canonical).as_ref().to_vec(),
319 };
320
321 let kt = Threshold::Weighted(vec![vec![half(), half(), half()]]);
322 let err = combine(&bundle_path, vec![partial0], &kt).unwrap_err();
323 match err {
324 MultiSigError::ThresholdNotMet { .. } | MultiSigError::Validation(_) => {}
325 other => panic!("expected ThresholdNotMet/Validation, got {other:?}"),
326 }
327 }
328
329 #[test]
330 fn combine_two_of_three_weighted_accepts() {
331 let (icp, kps) = make_three_key_icp();
332 let event = Event::Icp(icp.clone());
333 let unsigned = SignedEvent::new(event.clone(), vec![]);
334
335 let dir = tempdir().unwrap();
336 let bundle_path = dir.path().join("unsigned.json");
337 begin_multi_sig_event(
338 &unsigned,
339 &[
340 KeyAlias::new_unchecked("dev-a"),
341 KeyAlias::new_unchecked("dev-b"),
342 KeyAlias::new_unchecked("dev-c"),
343 ],
344 &bundle_path,
345 )
346 .unwrap();
347
348 let canonical = serialize_for_signing(&event).unwrap();
349 let partials = vec![
350 IndexedSignature {
351 index: 0,
352 prior_index: None,
353 sig: kps[0].sign(&canonical).as_ref().to_vec(),
354 },
355 IndexedSignature {
356 index: 2,
357 prior_index: None,
358 sig: kps[2].sign(&canonical).as_ref().to_vec(),
359 },
360 ];
361
362 let kt = Threshold::Weighted(vec![vec![half(), half(), half()]]);
363 let signed = combine(&bundle_path, partials, &kt).unwrap();
364 assert_eq!(signed.signatures.len(), 2);
365 }
366
367 #[test]
368 fn partial_roundtrip_via_disk() {
369 let sig = IndexedSignature {
370 index: 1,
371 prior_index: None,
372 sig: vec![7u8; 64],
373 };
374 let dir = tempdir().unwrap();
375 let path = dir.path().join("partial.sig");
376 write_partial(&sig, &path).unwrap();
377 let back = read_partial(&path).unwrap();
378 assert_eq!(back.index, 1);
379 assert_eq!(back.sig.len(), 64);
380 assert_eq!(back.sig, sig.sig);
381 }
382}