Skip to main content

confium_node/
lib.rs

1//! Confium Node.js bindings — threshold cryptography for server-side JS.
2//!
3//! Wraps the in-process DKG + sign drivers from
4//! [`confium_tc_cmp20`]/[`confium_tc_gg18`], plus the FROST-P256
5//! Shamir primitives and ElGamal-P256 threshold encryption. Output
6//! shapes mirror the Ruby + Python bindings so cross-binding parity
7//! tests work uniformly.
8//!
9//! ## Why Node.js when WASM exists?
10//!
11//! The [`@confium/confium-wasm`](https://www.npmjs.com/package/@confium/confium-wasm)
12//! package is **verifier-only by design** — browsers verify,
13//! servers sign. Node.js is server-side. This binding exposes the
14//! *signing* surface for Node consumers: CI release pipelines,
15//! signing microservices, scheduled-ceremony workers.
16
17#![forbid(unsafe_code)]
18#![allow(missing_docs)] // TODO: document before 1.0
19
20use napi::bindgen_prelude::Buffer;
21use napi::bindgen_prelude::Result as NapiResult;
22use napi_derive::napi;
23
24fn map_err<E: std::fmt::Display>(e: E) -> napi::Error {
25    napi::Error::new(napi::Status::GenericFailure, e.to_string())
26}
27
28// ===== CMP20 =====
29
30/// CMP20 threshold-ECDSA over P-256. Use for new threshold signing
31/// deployments; GG18 is provided for interop with existing systems.
32#[napi]
33pub struct Cmp20;
34
35#[napi]
36impl Cmp20 {
37    /// Run a non-interactive CMP20 DKG for `party_count` parties at
38    /// threshold `threshold`. Returns the per-party share blobs and
39    /// the joint SEC1-compressed public key.
40    #[napi]
41    pub fn keygen(threshold: u32, party_count: u32) -> NapiResult<Cmp20Keygen> {
42        let kg = confium_tc_cmp20::inprocess::keygen(threshold, party_count as usize)
43            .map_err(map_err)?;
44        Ok(Cmp20Keygen {
45            shares: kg.shares.into_iter().map(Into::into).collect(),
46            public_key: kg.public_key.into(),
47        })
48    }
49
50    /// Threshold-sign `message` using `shares` (each a share blob from
51    /// a previous `keygen` call). Returns the 64-byte `(r, s)` signature.
52    #[napi]
53    pub fn sign(shares: Vec<Buffer>, threshold: u32, message: Buffer) -> NapiResult<Buffer> {
54        let share_blobs: Vec<Vec<u8>> = shares.into_iter().map(|b| b.to_vec()).collect();
55        let msg = message.to_vec();
56        let sig =
57            confium_tc_cmp20::inprocess::sign(&share_blobs, threshold, &msg).map_err(map_err)?;
58        Ok(sig.into())
59    }
60
61    /// Sign N messages against the same joint key without re-running
62    /// DKG. See `sign_batch` in the Rust crate for performance notes.
63    #[napi]
64    pub fn sign_batch(
65        shares: Vec<Buffer>,
66        threshold: u32,
67        messages: Vec<Buffer>,
68    ) -> NapiResult<Vec<Buffer>> {
69        let share_blobs: Vec<Vec<u8>> = shares.into_iter().map(|b| b.to_vec()).collect();
70        let msg_refs: Vec<&[u8]> = messages.iter().map(|b| b.as_ref()).collect();
71        let sigs = confium_tc_cmp20::inprocess::sign_batch(&share_blobs, threshold, &msg_refs)
72            .map_err(map_err)?;
73        Ok(sigs.into_iter().map(Into::into).collect())
74    }
75}
76
77/// Outcome of a CMP20 / GG18 DKG.
78#[napi(object)]
79pub struct Cmp20Keygen {
80    /// Per-party share blobs, 71 bytes each. Distribute to N parties.
81    pub shares: Vec<Buffer>,
82    /// Joint P-256 public key (SEC1 compressed, 33 bytes).
83    #[napi(js_name = "publicKey")]
84    pub public_key: Buffer,
85}
86
87// ===== GG18 =====
88
89/// GG18 threshold-ECDSA over P-256. Prefer `Cmp20` for new deployments.
90#[napi]
91pub struct Gg18;
92
93#[napi]
94impl Gg18 {
95    /// Run a GG18 DKG. Returns the same shape as `Cmp20.keygen`.
96    #[napi]
97    pub fn keygen(threshold: u32, party_count: u32) -> NapiResult<Cmp20Keygen> {
98        let kg =
99            confium_tc_gg18::inprocess::keygen(threshold, party_count as usize).map_err(map_err)?;
100        Ok(Cmp20Keygen {
101            shares: kg.shares.into_iter().map(Into::into).collect(),
102            public_key: kg.public_key.into(),
103        })
104    }
105
106    /// Threshold-sign `message` with `shares`. Returns the 64-byte `(r, s)` signature.
107    #[napi]
108    pub fn sign(shares: Vec<Buffer>, threshold: u32, message: Buffer) -> NapiResult<Buffer> {
109        let share_blobs: Vec<Vec<u8>> = shares.into_iter().map(|b| b.to_vec()).collect();
110        let msg = message.to_vec();
111        let sig =
112            confium_tc_gg18::inprocess::sign(&share_blobs, threshold, &msg).map_err(map_err)?;
113        Ok(sig.into())
114    }
115}
116
117// ===== FROST-P256 (Shamir + single-party ECDSA) =====
118
119/// FROST-P256 Shamir primitives + single-party ECDSA-P256 sign.
120#[napi]
121pub struct FrostP256;
122
123#[napi]
124impl FrostP256 {
125    /// Generate a fresh P-256 keypair. Returns `{privateKey, publicKey}`
126    /// as 32-byte + 65-byte buffers respectively.
127    #[napi]
128    pub fn generate_keypair() -> NapiResult<FrostKeypair> {
129        let kp = confium_tc_frost_p256::generate_keypair();
130        let sk: [u8; 32] = kp.to_signing_key().to_bytes().into();
131        let pk = kp.to_verifying_key().to_sec1_bytes();
132        Ok(FrostKeypair {
133            private_key: sk.to_vec().into(),
134            public_key: pk.to_vec().into(),
135        })
136    }
137}
138
139/// Outcome of `FrostP256.generateKeypair`.
140#[napi(object)]
141pub struct FrostKeypair {
142    /// 32-byte secret scalar.
143    #[napi(js_name = "privateKey")]
144    pub private_key: Buffer,
145    /// 65-byte SEC1 uncompressed public key.
146    #[napi(js_name = "publicKey")]
147    pub public_key: Buffer,
148}
149
150// ===== Version =====
151
152/// Package version (mirrors the Cargo version).
153#[napi]
154pub fn version() -> String {
155    env!("CARGO_PKG_VERSION").to_string()
156}