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// `deny`, not `forbid`: napi-derive 3 emits `#[allow(unsafe_code)]` on
18// its generated FFI glue, which a crate-level `forbid` would reject.
19// Hand-written unsafe in this crate remains a hard error.
20#![deny(unsafe_code)]
21#![allow(missing_docs)] // TODO: document before 1.0
22
23use napi::bindgen_prelude::Buffer;
24use napi::bindgen_prelude::Result as NapiResult;
25use napi_derive::napi;
26
27fn map_err<E: std::fmt::Display>(e: E) -> napi::Error {
28    napi::Error::new(napi::Status::GenericFailure, e.to_string())
29}
30
31// ===== CMP20 =====
32
33/// CMP20 threshold-ECDSA over P-256. Use for new threshold signing
34/// deployments; GG18 is provided for interop with existing systems.
35#[napi]
36pub struct Cmp20;
37
38#[napi]
39impl Cmp20 {
40    /// Run a non-interactive CMP20 DKG for `party_count` parties at
41    /// threshold `threshold`. Returns the per-party share blobs and
42    /// the joint SEC1-compressed public key.
43    #[napi]
44    pub fn keygen(threshold: u32, party_count: u32) -> NapiResult<Cmp20Keygen> {
45        let kg = confium_tc_cmp20::inprocess::keygen(threshold, party_count as usize)
46            .map_err(map_err)?;
47        Ok(Cmp20Keygen {
48            shares: kg.shares.into_iter().map(Into::into).collect(),
49            public_key: kg.public_key.into(),
50        })
51    }
52
53    /// Threshold-sign `message` using `shares` (each a share blob from
54    /// a previous `keygen` call). Returns the 64-byte `(r, s)` signature.
55    #[napi]
56    pub fn sign(shares: Vec<Buffer>, threshold: u32, message: Buffer) -> NapiResult<Buffer> {
57        let share_blobs: Vec<Vec<u8>> = shares.into_iter().map(|b| b.to_vec()).collect();
58        let msg = message.to_vec();
59        let sig =
60            confium_tc_cmp20::inprocess::sign(&share_blobs, threshold, &msg).map_err(map_err)?;
61        Ok(sig.into())
62    }
63
64    /// Sign N messages against the same joint key without re-running
65    /// DKG. See `sign_batch` in the Rust crate for performance notes.
66    #[napi]
67    pub fn sign_batch(
68        shares: Vec<Buffer>,
69        threshold: u32,
70        messages: Vec<Buffer>,
71    ) -> NapiResult<Vec<Buffer>> {
72        let share_blobs: Vec<Vec<u8>> = shares.into_iter().map(|b| b.to_vec()).collect();
73        let msg_refs: Vec<&[u8]> = messages.iter().map(|b| b.as_ref()).collect();
74        let sigs = confium_tc_cmp20::inprocess::sign_batch(&share_blobs, threshold, &msg_refs)
75            .map_err(map_err)?;
76        Ok(sigs.into_iter().map(Into::into).collect())
77    }
78}
79
80/// Outcome of a CMP20 / GG18 DKG.
81#[napi(object)]
82pub struct Cmp20Keygen {
83    /// Per-party share blobs, 71 bytes each. Distribute to N parties.
84    pub shares: Vec<Buffer>,
85    /// Joint P-256 public key (SEC1 compressed, 33 bytes).
86    #[napi(js_name = "publicKey")]
87    pub public_key: Buffer,
88}
89
90// ===== GG18 =====
91
92/// GG18 threshold-ECDSA over P-256. Prefer `Cmp20` for new deployments.
93#[napi]
94pub struct Gg18;
95
96#[napi]
97impl Gg18 {
98    /// Run a GG18 DKG. Returns the same shape as `Cmp20.keygen`.
99    #[napi]
100    pub fn keygen(threshold: u32, party_count: u32) -> NapiResult<Cmp20Keygen> {
101        let kg =
102            confium_tc_gg18::inprocess::keygen(threshold, party_count as usize).map_err(map_err)?;
103        Ok(Cmp20Keygen {
104            shares: kg.shares.into_iter().map(Into::into).collect(),
105            public_key: kg.public_key.into(),
106        })
107    }
108
109    /// Threshold-sign `message` with `shares`. Returns the 64-byte `(r, s)` signature.
110    #[napi]
111    pub fn sign(shares: Vec<Buffer>, threshold: u32, message: Buffer) -> NapiResult<Buffer> {
112        let share_blobs: Vec<Vec<u8>> = shares.into_iter().map(|b| b.to_vec()).collect();
113        let msg = message.to_vec();
114        let sig =
115            confium_tc_gg18::inprocess::sign(&share_blobs, threshold, &msg).map_err(map_err)?;
116        Ok(sig.into())
117    }
118}
119
120// ===== FROST-P256 (Shamir + single-party ECDSA) =====
121
122/// FROST-P256 Shamir primitives + single-party ECDSA-P256 sign.
123#[napi]
124pub struct FrostP256;
125
126#[napi]
127impl FrostP256 {
128    /// Generate a fresh P-256 keypair. Returns `{privateKey, publicKey}`
129    /// as 32-byte + 65-byte buffers respectively.
130    #[napi]
131    pub fn generate_keypair() -> NapiResult<FrostKeypair> {
132        let kp = confium_tc_frost_p256::generate_keypair();
133        let sk: [u8; 32] = kp.to_signing_key().to_bytes().into();
134        let pk = kp.to_verifying_key().to_sec1_bytes();
135        Ok(FrostKeypair {
136            private_key: sk.to_vec().into(),
137            public_key: pk.to_vec().into(),
138        })
139    }
140}
141
142/// Outcome of `FrostP256.generateKeypair`.
143#[napi(object)]
144pub struct FrostKeypair {
145    /// 32-byte secret scalar.
146    #[napi(js_name = "privateKey")]
147    pub private_key: Buffer,
148    /// 65-byte SEC1 uncompressed public key.
149    #[napi(js_name = "publicKey")]
150    pub public_key: Buffer,
151}
152
153// ===== Version =====
154
155/// Package version (mirrors the Cargo version).
156#[napi]
157pub fn version() -> String {
158    env!("CARGO_PKG_VERSION").to_string()
159}