confium_tc_core/registry.rs
1//! Link-time registry of in-process threshold schemes.
2//!
3//! Each TC scheme (FROST-ed25519, GG18-ECDSA-P256, a Pedersen DKG, …)
4//! registers itself with [`register_tc_scheme!`]. The session layer
5//! looks schemes up by name and dispatches lifecycle calls through the
6//! [`TcScheme`] trait.
7//!
8//! This registry is for **in-process** Rust schemes linked into the
9//! same binary as `confium-tc`. Out-of-process plugins (loaded via
10//! `libloading`) advertise the `"tc"` interface name through the core
11//! plugin loader and are dispatched separately — see
12//! `confium-core::ffi::registry`.
13//!
14//! The registry name advertised for plugins is `"tc"` and covers both
15//! `tc-signature` and `tc-kem` via the scheme name; max version 0.
16
17use std::fmt;
18
19use crate::Result;
20use crate::message::Message;
21use crate::session::SessionParams;
22
23/// Broad category of a threshold scheme. Drives which lifecycle entry
24/// points apply (e.g. DKG produces a share on output; signing consumes
25/// a share on input).
26#[derive(Debug, Copy, Clone, PartialEq, Eq)]
27pub enum TcSchemeKind {
28 /// Threshold signing — parties cooperatively produce a signature.
29 Signature,
30 /// Threshold key encapsulation — parties cooperatively derive a
31 /// shared secret.
32 Kem,
33 /// Distributed key generation — parties produce a fresh shared
34 /// secret and per-party shares.
35 Dkg,
36}
37
38/// A registered threshold scheme.
39///
40/// Implementations are `Send + Sync` so a scheme can be looked up from
41/// any thread. Each `create_session` call yields a fresh [`SessionImpl`]
42/// that owns the per-session mutable state.
43pub trait TcScheme: Send + Sync {
44 /// Canonical scheme name, e.g. `"FROST-ed25519"`, `"GG18-ECDSA-P256"`.
45 fn name(&self) -> &'static str;
46
47 /// What kind of artifact this scheme produces.
48 fn kind(&self) -> TcSchemeKind;
49
50 /// Build a new session for this scheme. The returned [`SessionImpl`]
51 /// owns all per-session state; the framework drives it via
52 /// [`SessionImpl::round`] / [`SessionImpl::result`] /
53 /// [`SessionImpl::destroy`].
54 fn create_session(&self, params: &SessionParams) -> Result<Box<dyn SessionImpl>>;
55}
56
57/// Per-session scheme state, driven round-by-round by the framework.
58///
59/// A `SessionImpl` is **not** `Sync` — it owns mutable protocol state
60/// for exactly one party's view of one session. The framework moves it
61/// between threads via the owning [`crate::session::Session`] handle.
62pub trait SessionImpl: Send {
63 /// Advance one round. Consumes the messages received since the last
64 /// round and returns the messages to send next, plus a `complete`
65 /// flag indicating the session has produced its output and no more
66 /// rounds are needed.
67 fn round(&mut self, incoming: &[Message]) -> Result<RoundResult>;
68
69 /// Extract the final cryptographic artifact (signature bytes, DKG
70 /// output, KEM shared secret, …). Only valid once a round returned
71 /// `complete == true`.
72 fn result(&self) -> Result<Vec<u8>>;
73
74 /// Release any scheme-owned resources. Called exactly once when the
75 /// framework drops the session. Implementations should zeroize
76 /// sensitive state.
77 fn destroy(&mut self);
78}
79
80/// Outcome of one [`SessionImpl::round`] call.
81#[derive(Debug, Clone, Default)]
82pub struct RoundResult {
83 /// Messages to send for the next round.
84 pub outgoing: Vec<Message>,
85 /// `true` once the protocol is finished and [`SessionImpl::result`]
86 /// is ready to read.
87 pub complete: bool,
88}
89
90impl RoundResult {
91 pub fn new(outgoing: Vec<Message>, complete: bool) -> Self {
92 RoundResult { outgoing, complete }
93 }
94
95 /// Convenience: an empty result that signals the session is done.
96 pub fn done() -> Self {
97 RoundResult {
98 outgoing: Vec::new(),
99 complete: true,
100 }
101 }
102}
103
104/// Wrapper so a `&'static dyn TcScheme` can be collected by `inventory`.
105pub struct RegisteredScheme {
106 pub scheme: &'static dyn TcScheme,
107}
108
109inventory::collect!(RegisteredScheme);
110
111/// Iterator over every scheme registered at link time.
112pub fn iter() -> impl Iterator<Item = &'static dyn TcScheme> {
113 inventory::iter::<RegisteredScheme>().map(|r| r.scheme)
114}
115
116/// Look up a scheme by canonical name. Returns the first match — scheme
117/// name collisions are a registration bug and surface here as
118/// [`crate::error::Error::SchemeNotFound`].
119pub fn find(name: &str) -> Option<&'static dyn TcScheme> {
120 iter().find(|s| s.name() == name)
121}
122
123/// Submit a scheme implementation to the link-time registry.
124///
125/// ```ignore
126/// use confium_tc::register_tc_scheme;
127/// use confium_tc::registry::{TcScheme, TcSchemeKind};
128/// use confium_tc::{SessionImpl, SessionParams};
129/// # use confium_tc::Error;
130/// # struct MyScheme;
131/// # impl TcScheme for MyScheme {
132/// # fn name(&self) -> &'static str { "demo" }
133/// # fn kind(&self) -> TcSchemeKind { TcSchemeKind::Signature }
134/// # fn create_session(&self, _: &SessionParams)
135/// # -> std::result::Result<Box<dyn SessionImpl>, Error> { unimplemented!() }
136/// # }
137/// register_tc_scheme!(MyScheme);
138/// ```
139#[macro_export]
140macro_rules! register_tc_scheme {
141 ($scheme:ident) => {
142 ::inventory::submit! {
143 $crate::registry::RegisteredScheme { scheme: &$scheme }
144 }
145 };
146}
147
148impl fmt::Debug for dyn TcScheme {
149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150 f.debug_struct("TcScheme")
151 .field("name", &self.name())
152 .field("kind", &self.kind())
153 .finish()
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use crate::error;
161 use crate::session::SessionParams;
162 use crate::share::Share;
163
164 /// A no-op scheme used to exercise the registry + session lifecycle
165 /// end-to-end. It "completes" on the first round and produces a
166 /// fixed result; no real cryptography happens here.
167 struct NoopScheme;
168
169 impl TcScheme for NoopScheme {
170 fn name(&self) -> &'static str {
171 "test-noop"
172 }
173 fn kind(&self) -> TcSchemeKind {
174 TcSchemeKind::Signature
175 }
176 fn create_session(&self, params: &SessionParams) -> Result<Box<dyn SessionImpl>> {
177 Ok(Box::new(NoopSession {
178 result: params
179 .message
180 .clone()
181 .unwrap_or_else(|| b"noop-result".to_vec()),
182 done: false,
183 }))
184 }
185 }
186
187 struct NoopSession {
188 result: Vec<u8>,
189 done: bool,
190 }
191
192 impl SessionImpl for NoopSession {
193 fn round(&mut self, _incoming: &[Message]) -> Result<RoundResult> {
194 self.done = true;
195 Ok(RoundResult::done())
196 }
197 fn result(&self) -> Result<Vec<u8>> {
198 if !self.done {
199 return Err(error::SessionNotCompleteSnafu {}.build());
200 }
201 Ok(self.result.clone())
202 }
203 fn destroy(&mut self) {
204 self.result.fill(0);
205 }
206 }
207
208 // Force the scheme into the link-time registry for these tests.
209 inventory::submit! {
210 RegisteredScheme { scheme: &NoopScheme as &dyn TcScheme }
211 }
212
213 fn make_params() -> SessionParams {
214 use crate::party::{Party, PartyList};
215 SessionParams {
216 scheme: "test-noop".to_string(),
217 parties: PartyList::from_parties(vec![
218 Party::inproc("a"),
219 Party::inproc("b"),
220 Party::inproc("c"),
221 ]),
222 threshold: 2,
223 this_party_idx: 0,
224 local_share: None,
225 message: None,
226 }
227 }
228
229 #[test]
230 fn registry_finds_registered_scheme() {
231 let scheme = find("test-noop").expect("noop scheme must be registered");
232 assert_eq!(scheme.name(), "test-noop");
233 assert_eq!(scheme.kind(), TcSchemeKind::Signature);
234 }
235
236 #[test]
237 fn registry_find_missing_returns_none() {
238 assert!(find("does-not-exist").is_none());
239 }
240
241 #[test]
242 fn scheme_create_session_runs_full_lifecycle() {
243 let scheme = find("test-noop").expect("registered");
244 let params = make_params();
245 let mut session = scheme.create_session(¶ms).expect("session created");
246
247 let rr = session.round(&[]).expect("round ok");
248 assert!(rr.complete);
249 assert!(rr.outgoing.is_empty());
250
251 let result = session.result().expect("result ok");
252 assert_eq!(result, b"noop-result");
253
254 session.destroy();
255 }
256
257 #[test]
258 fn scheme_create_session_propagates_message_param() {
259 let scheme = find("test-noop").expect("registered");
260 let mut params = make_params();
261 params.message = Some(vec![0x11, 0x22]);
262 let mut session = scheme.create_session(¶ms).expect("session created");
263 session.round(&[]).expect("round ok");
264 let result = session.result().expect("result ok");
265 assert_eq!(result, vec![0x11, 0x22]);
266 }
267
268 #[test]
269 fn result_before_complete_errors() {
270 let scheme = find("test-noop").expect("registered");
271 let params = make_params();
272 let session = scheme.create_session(¶ms).expect("session created");
273 let err = session.result().unwrap_err();
274 assert!(matches!(err, error::Error::SessionNotComplete { .. }));
275 }
276
277 #[test]
278 fn share_param_does_not_panic_when_absent() {
279 // Sanity: SessionParams.local_share is optional and unused by
280 // the noop scheme; ensure the field is constructible.
281 let params = make_params();
282 assert!(params.local_share.is_none());
283 let _ = Share::new("test-noop", vec![]);
284 }
285}