Add frost-taproot implementation

This commit is contained in:
Jon Staab
2026-02-19 15:06:22 -08:00
parent 4e3c4aa8ce
commit 77bd576c0a
32 changed files with 2038 additions and 1 deletions
+1
View File
@@ -0,0 +1 @@
--ignore-dir=target
+1
View File
@@ -0,0 +1 @@
target
+1
View File
@@ -1 +1,2 @@
ref
target
+1 -1
View File
@@ -2,6 +2,6 @@ This repository is a monorepo with multiple narrowly-scoped packages intended to
# frost-taproot
A rust implementation of BIP-340 FROST signatures, including DKG. It is compatible with cmdruid/frost typescript implementation, contained in the `ref` directory.
Frost taproot is a rust implementation of BIP-340 FROST signatures, including DKG. It is compatible with cmdruid/frost typescript implementation, contained in the `ref` directory.
cmdruid/frost relies on noble/curves and noble/hashes, both of which are provided in the `ref` directory. However, the implementation for this project will rely on the k256 crate.
+420
View File
@@ -0,0 +1,420 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "base16ct"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
[[package]]
name = "base64ct"
version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crypto-bigint"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
dependencies = [
"generic-array",
"rand_core",
"subtle",
"zeroize",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "der"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [
"const-oid",
"zeroize",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"const-oid",
"crypto-common",
"subtle",
]
[[package]]
name = "ecdsa"
version = "0.16.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
dependencies = [
"der",
"digest",
"elliptic-curve",
"rfc6979",
"signature",
"spki",
]
[[package]]
name = "elliptic-curve"
version = "0.13.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
dependencies = [
"base16ct",
"crypto-bigint",
"digest",
"ff",
"generic-array",
"group",
"pkcs8",
"rand_core",
"sec1",
"subtle",
"zeroize",
]
[[package]]
name = "ff"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
dependencies = [
"rand_core",
"subtle",
]
[[package]]
name = "frost-taproot"
version = "0.1.0"
dependencies = [
"k256",
"rand",
"sha2",
"signature",
"thiserror",
]
[[package]]
name = "generic-array"
version = "0.14.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
dependencies = [
"typenum",
"version_check",
"zeroize",
]
[[package]]
name = "getrandom"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "group"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
dependencies = [
"ff",
"rand_core",
"subtle",
]
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]]
name = "k256"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b"
dependencies = [
"cfg-if",
"ecdsa",
"elliptic-curve",
"once_cell",
"sha2",
"signature",
]
[[package]]
name = "libc"
version = "0.2.182"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "pkcs8"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der",
"spki",
]
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "rfc6979"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
dependencies = [
"hmac",
"subtle",
]
[[package]]
name = "sec1"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
dependencies = [
"base16ct",
"der",
"generic-array",
"pkcs8",
"subtle",
"zeroize",
]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "signature"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"digest",
"rand_core",
]
[[package]]
name = "spki"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
dependencies = [
"base64ct",
"der",
]
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.116"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "typenum"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "zerocopy"
version = "0.8.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "zeroize"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+5
View File
@@ -4,3 +4,8 @@ version = "0.1.0"
edition = "2024"
[dependencies]
k256 = { version = "0.13", features = ["arithmetic", "hash2curve", "schnorr"] }
sha2 = "0.10"
rand = "0.8"
thiserror = "1"
signature = "2"
+129
View File
@@ -0,0 +1,129 @@
// Mirrors ref/frost/src/lib/commit.ts
use crate::ecc::group::{scalar_multi, serialize_element, serialize_scalar_u32};
use crate::ecc::hash::{h1, h4, h5};
use crate::ecc::util::lift_x;
use crate::helpers::{generate_nonce, get_pubkey};
use crate::types::{BindFactor, CommitmentPackage, PublicNonce, SecretShare};
use crate::Error;
/// Extract participant indices from a list of public nonces.
/// Mirrors `get_nonce_ids` in the TS implementation.
pub fn get_nonce_ids(pnonces: &[PublicNonce]) -> Vec<u32> {
pnonces.iter().map(|pn| pn.idx).collect()
}
/// Encode all public nonces into a sorted byte prefix.
/// Mirrors `get_commits_prefix` in the TS implementation.
pub fn get_commits_prefix(pnonces: &[PublicNonce]) -> Vec<u8> {
let mut sorted = pnonces.to_vec();
sorted.sort_by_key(|pn| pn.idx);
let mut out = Vec::new();
for pn in &sorted {
out.extend_from_slice(&serialize_scalar_u32(pn.idx));
out.extend_from_slice(&pn.hidden_pn);
out.extend_from_slice(&pn.binder_pn);
}
out
}
/// Build the group signing prefix: group_pk || H4(msg) || H5(commit_list).
/// Mirrors `get_group_prefix` in the TS implementation.
pub fn get_group_prefix(pnonces: &[PublicNonce], group_pk: &[u8; 33], message: &[u8]) -> Vec<u8> {
let msg_hash = h4(message);
let commit_list = get_commits_prefix(pnonces);
let commit_hash = h5(&commit_list);
let mut out = Vec::new();
out.extend_from_slice(group_pk);
out.extend_from_slice(&msg_hash);
out.extend_from_slice(&commit_hash);
out
}
/// Look up the binding factor for a given participant index.
/// Mirrors `get_bind_factor` in the TS implementation.
pub fn get_bind_factor(binders: &[BindFactor], idx: u32) -> Result<[u8; 32], Error> {
binders
.iter()
.find(|b| b.idx == idx)
.map(|b| b.factor)
.ok_or(Error::RecordNotFound(idx))
}
/// Compute per-participant binding factors from the group prefix.
/// Mirrors `get_group_binders` in the TS implementation.
pub fn get_group_binders(pnonces: &[PublicNonce], prefix: &[u8]) -> Vec<BindFactor> {
pnonces
.iter()
.map(|pn| {
let scalar_bytes = serialize_scalar_u32(pn.idx);
let mut rho_input = prefix.to_vec();
rho_input.extend_from_slice(&scalar_bytes);
let factor = h1(&rho_input);
BindFactor {
idx: pn.idx,
factor,
}
})
.collect()
}
/// Compute the group public nonce: sum of (hidden_pn + bind_factor * binder_pn) for each participant.
/// Mirrors `get_group_pubnonce` in the TS implementation.
pub fn get_group_pubnonce(
pnonces: &[PublicNonce],
binders: &[BindFactor],
) -> Result<[u8; 33], Error> {
use crate::ecc::group::element_add;
use crate::ecc::util::scalar_from_bytes;
let mut group_commit = None;
for pn in pnonces {
let hidden_elem = lift_x(&pn.hidden_pn)?;
let binding_elem = lift_x(&pn.binder_pn)?;
let bind_factor_bytes = get_bind_factor(binders, pn.idx)?;
let bind_factor = scalar_from_bytes(&bind_factor_bytes);
let factored_elem = scalar_multi(&binding_elem, &bind_factor);
group_commit = Some(element_add(group_commit, Some(hidden_elem))?);
group_commit = Some(element_add(group_commit, Some(factored_elem))?);
}
let pt = group_commit.ok_or(Error::BothPointsNull)?;
Ok(serialize_element(&pt))
}
/// Create a commitment package (secret + public nonces) for a signing session.
/// Mirrors `create_commit_pkg` in the TS implementation.
pub fn create_commit_pkg(
secret_share: &SecretShare,
hidden_seed: Option<&[u8; 32]>,
binder_seed: Option<&[u8; 32]>,
) -> CommitmentPackage {
let binder_sn = generate_nonce(&secret_share.seckey, binder_seed);
let hidden_sn = generate_nonce(&secret_share.seckey, hidden_seed);
let binder_pn = get_pubkey(&binder_sn);
let hidden_pn = get_pubkey(&hidden_sn);
CommitmentPackage {
idx: secret_share.idx,
binder_sn,
hidden_sn,
binder_pn,
hidden_pn,
}
}
/// Find a commitment package for a given share.
/// Mirrors `get_commit_pkg` in the TS implementation.
pub fn get_commit_pkg(
commits: &[CommitmentPackage],
share: &SecretShare,
) -> Result<CommitmentPackage, Error> {
commits
.iter()
.find(|c| c.idx == share.idx)
.cloned()
.ok_or(Error::RecordNotFound(share.idx))
}
+78
View File
@@ -0,0 +1,78 @@
// Mirrors ref/frost/src/lib/context.ts
use crate::commit::{get_group_binders, get_group_prefix, get_group_pubnonce, get_nonce_ids};
use crate::ecc::group::serialize_element;
use crate::ecc::state::get_point_state;
use crate::ecc::util::lift_x;
use crate::helpers::get_challenge;
use crate::types::{GroupCommitContext, GroupKeyContext, GroupSigningCtx, PublicNonce};
use crate::Error;
/// Build the group key context, applying optional tweaks.
/// Mirrors `get_group_key_context` in the TS implementation.
pub fn get_group_key_context(pubkey: &[u8], tweaks: &[[u8; 32]]) -> Result<GroupKeyContext, Error> {
let int_pt = lift_x(pubkey)?;
let int_pk_bytes = serialize_element(&int_pt);
let group_pt = get_point_state(int_pt, tweaks)?;
let group_pk = serialize_element(&group_pt.point);
Ok(GroupKeyContext {
group_pt,
group_pk,
int_pt: Some(int_pt),
int_pk: Some(int_pk_bytes),
tweak: None,
})
}
/// Build the commit context from the key context, nonces, and message.
/// Mirrors `get_group_commit_context` in the TS implementation.
pub fn get_group_commit_context(
key_ctx: &GroupKeyContext,
pnonces: &[PublicNonce],
message: &[u8],
) -> Result<GroupCommitContext, Error> {
let bind_prefix = get_group_prefix(pnonces, &key_ctx.group_pk, message);
let bind_factors = get_group_binders(pnonces, &bind_prefix);
let group_pn = get_group_pubnonce(pnonces, &bind_factors)?;
let indexes = get_nonce_ids(pnonces);
let challenge = get_challenge(&group_pn, &key_ctx.group_pk, message)?;
Ok(GroupCommitContext {
bind_factors,
bind_prefix,
challenge,
group_pn,
indexes,
message: message.to_vec(),
pnonces: pnonces.to_vec(),
})
}
/// Build the full signing context.
/// Mirrors `get_group_signing_ctx` in the TS implementation.
pub fn get_group_signing_ctx(
group_pk: &[u8],
pnonces: &[PublicNonce],
message: &[u8],
tweaks: &[[u8; 32]],
) -> Result<GroupSigningCtx, Error> {
let key_ctx = get_group_key_context(group_pk, tweaks)?;
let com_ctx = get_group_commit_context(&key_ctx, pnonces, message)?;
Ok(GroupSigningCtx {
group_pt: key_ctx.group_pt,
group_pk: key_ctx.group_pk,
int_pt: key_ctx.int_pt,
int_pk: key_ctx.int_pk,
tweak: key_ctx.tweak,
bind_factors: com_ctx.bind_factors,
bind_prefix: com_ctx.bind_prefix,
challenge: com_ctx.challenge,
group_pn: com_ctx.group_pn,
indexes: com_ctx.indexes,
message: com_ctx.message,
pnonces: com_ctx.pnonces,
})
}
+95
View File
@@ -0,0 +1,95 @@
// Mirrors ref/frost/src/ecc/group.ts
// Group operations on secp256k1 using k256.
use k256::{ProjectivePoint, Scalar};
use rand::rngs::OsRng;
use rand::RngCore;
use super::util::{deserialize_point, mod_n, scalar_from_bytes, scalar_to_bytes, serialize_point};
use crate::Error;
use k256::U256;
/// The group order N.
pub fn order() -> Scalar {
// N as a scalar is zero (it wraps), so we expose the U256 constant from util.
// Callers needing N for negation use `Scalar::ZERO - Scalar::ONE` + 1 = N-1 pattern,
// or use scalar_neg directly.
Scalar::ZERO
}
/// The identity (point at infinity).
pub fn identity() -> ProjectivePoint {
ProjectivePoint::IDENTITY
}
/// Generate a random scalar in [1, N-1].
pub fn random_scalar() -> Scalar {
let mut bytes = [0u8; 32];
OsRng.fill_bytes(&mut bytes);
mod_n(U256::from_be_slice(&bytes))
}
/// Add two points. Either may be the identity (None-equivalent in TS was null).
pub fn element_add(
a: Option<ProjectivePoint>,
b: Option<ProjectivePoint>,
) -> Result<ProjectivePoint, Error> {
match (a, b) {
(None, None) => Err(Error::BothPointsNull),
(None, Some(b)) => Ok(b),
(Some(a), None) => Ok(a),
(Some(a), Some(b)) => Ok(a + b),
}
}
/// Sum a slice of optional points.
pub fn element_add_many(elems: &[Option<ProjectivePoint>]) -> Result<ProjectivePoint, Error> {
if elems.is_empty() {
return Err(Error::BothPointsNull);
}
let mut acc = elems[0];
for &e in &elems[1..] {
acc = Some(element_add(acc, e)?);
}
acc.ok_or(Error::BothPointsNull)
}
/// Scalar multiplication: k * A.
pub fn scalar_multi(a: &ProjectivePoint, k: &Scalar) -> ProjectivePoint {
a * k
}
/// Scalar base multiplication: k * G.
pub fn scalar_base_multi(k: &Scalar) -> ProjectivePoint {
ProjectivePoint::GENERATOR * k
}
/// Serialize a point to 33 compressed bytes.
pub fn serialize_element(pt: &ProjectivePoint) -> [u8; 33] {
serialize_point(pt)
}
/// Deserialize a point from 33 compressed bytes.
pub fn deserialize_element(bytes: &[u8; 33]) -> Result<ProjectivePoint, Error> {
deserialize_point(bytes)
}
/// Serialize a scalar index (u32) as a 32-byte big-endian value.
/// Mirrors `G.SerializeScalar(idx)` where idx is a small integer.
pub fn serialize_scalar_u32(idx: u32) -> [u8; 32] {
let mut out = [0u8; 32];
out[28..].copy_from_slice(&idx.to_be_bytes());
out
}
/// Serialize a Scalar to 32 bytes.
pub fn serialize_scalar(s: &Scalar) -> [u8; 32] {
scalar_to_bytes(s)
}
/// Deserialize 32 bytes into a Scalar (reduced mod N).
pub fn deserialize_scalar(bytes: &[u8; 32]) -> Scalar {
scalar_from_bytes(bytes)
}
pub use super::util::{has_even_y, lift_x, negate_point, pow_n};
+89
View File
@@ -0,0 +1,89 @@
// Mirrors ref/frost/src/ecc/hash.ts
// FROST-secp256k1-SHA256-v1 hash functions H1H5.
// Spec: draft-irtf-cfrg-frost-15, section 6.5.
use k256::{
elliptic_curve::hash2curve::{ExpandMsg, ExpandMsgXmd, Expander},
Scalar,
};
use sha2::{Digest, Sha256};
use super::util::mod_n;
use k256::U256;
const DOMAIN: &str = "FROST-secp256k1-SHA256-v1";
/// Build the DST for a given sub-tag.
fn dst(sub: &str) -> Vec<u8> {
format!("{}{}", DOMAIN, sub).into_bytes()
}
/// hash_to_field using XMD with SHA-256, outputting one field element mod N.
/// Mirrors `hash_to_field(msg, 1, { m:1, p:N, k:128, expand:'xmd', hash:sha256, DST })`.
fn hash_to_field_n(msg: &[u8], dst_bytes: &[u8]) -> Scalar {
// expand_message_xmd with len_in_bytes = 48 (ceil((log2(N)+k)/8) = ceil((256+128)/8) = 48)
let mut uniform = [0u8; 48];
ExpandMsgXmd::<Sha256>::expand_message(&[msg], &[dst_bytes], 48)
.expect("expand_message failed")
.fill_bytes(&mut uniform);
// Interpret as a 384-bit integer mod N.
// We take the low 256 bits after reduction — use U256 from the 48-byte big-endian value.
// Noble does: interpret as big-endian integer, then mod N.
// We replicate by taking the full 48 bytes as a big integer mod N.
// k256 U256 is only 256 bits, so we do two-step reduction:
// high 16 bytes * 2^256 + low 32 bytes, all mod N.
let (hi, lo) = uniform.split_at(16);
let mut hi32 = [0u8; 32];
hi32[16..].copy_from_slice(hi);
let mut lo32 = [0u8; 32];
lo32.copy_from_slice(lo);
let hi_scalar = mod_n(U256::from_be_slice(&hi32));
let lo_scalar = mod_n(U256::from_be_slice(&lo32));
// 2^128 mod N as a scalar
let shift = {
let mut s = [0u8; 32];
s[16] = 1; // 2^128
mod_n(U256::from_be_slice(&s))
};
hi_scalar * shift + lo_scalar
}
/// H1: rho — binding factor hash.
pub fn h1(msg: &[u8]) -> [u8; 32] {
let s = hash_to_field_n(msg, &dst("rho"));
s.to_bytes().into()
}
/// H2: chal — challenge hash.
pub fn h2(msg: &[u8]) -> [u8; 32] {
let s = hash_to_field_n(msg, &dst("chal"));
s.to_bytes().into()
}
/// H3: nonce — nonce generation hash.
pub fn h3(msg: &[u8]) -> [u8; 32] {
let s = hash_to_field_n(msg, &dst("nonce"));
s.to_bytes().into()
}
/// H4: msg — plain SHA-256 with domain prefix.
pub fn h4(msg: &[u8]) -> [u8; 32] {
let prefix = dst("msg");
let mut hasher = Sha256::new();
hasher.update(&prefix);
hasher.update(msg);
hasher.finalize().into()
}
/// H5: com — plain SHA-256 with domain prefix.
pub fn h5(msg: &[u8]) -> [u8; 32] {
let prefix = dst("com");
let mut hasher = Sha256::new();
hasher.update(&prefix);
hasher.update(msg);
hasher.finalize().into()
}
+4
View File
@@ -0,0 +1,4 @@
pub mod group;
pub mod hash;
pub mod state;
pub mod util;
+46
View File
@@ -0,0 +1,46 @@
// Mirrors ref/frost/src/ecc/state.ts
use k256::{ProjectivePoint, Scalar};
use super::group::{element_add, scalar_base_multi};
use super::util::{has_even_y, mod_n, negate_point, scalar_from_bytes};
use crate::types::PointState;
use crate::Error;
use k256::U256;
/// Computes the accumulative parity state for a given point with optional key tweaks.
/// Mirrors `get_point_state` in the TS implementation.
pub fn get_point_state(element: ProjectivePoint, tweaks: &[[u8; 32]]) -> Result<PointState, Error> {
let pos = Scalar::ONE;
let neg = -pos; // N - 1 mod N
let mut point = element;
let mut parity;
let mut state = pos;
let mut tweak = Scalar::ZERO;
for t_bytes in tweaks {
let t = scalar_from_bytes(t_bytes);
let tg = scalar_base_multi(&t);
parity = if has_even_y(&point) { pos } else { neg };
if parity == neg {
point = negate_point(&point);
}
point = element_add(Some(point), Some(tg))?;
state = state * parity;
tweak = mod_n(U256::from_be_slice(&(t + parity * tweak).to_bytes()));
}
parity = if has_even_y(&point) { pos } else { neg };
Ok(PointState {
parity,
point,
state,
tweak,
})
}
+119
View File
@@ -0,0 +1,119 @@
// Mirrors ref/frost/src/ecc/util.ts
use std::ops::Neg;
use k256::{
elliptic_curve::{
ops::Reduce,
sec1::{FromEncodedPoint, ToEncodedPoint},
},
EncodedPoint, ProjectivePoint, Scalar, U256,
};
use crate::Error;
/// Secp256k1 field prime P.
pub const P: U256 =
U256::from_be_hex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f");
/// Secp256k1 group order N.
pub const N: U256 =
U256::from_be_hex("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141");
/// Reduce a 256-bit integer mod N into a Scalar.
pub fn mod_n(x: U256) -> Scalar {
<Scalar as Reduce<U256>>::reduce(x)
}
/// Scalar from raw bytes, reduced mod N.
pub fn scalar_from_bytes(bytes: &[u8; 32]) -> Scalar {
mod_n(U256::from_be_slice(bytes))
}
/// Scalar to big-endian bytes.
pub fn scalar_to_bytes(s: &Scalar) -> [u8; 32] {
s.to_bytes().into()
}
/// Add two scalars mod N.
pub fn scalar_add(a: Scalar, b: Scalar) -> Scalar {
a + b
}
/// Multiply two scalars mod N.
pub fn scalar_mul(a: Scalar, b: Scalar) -> Scalar {
a * b
}
/// Negate a scalar mod N.
pub fn scalar_neg(a: Scalar) -> Scalar {
-a
}
/// Modular inverse of a scalar (panics if zero).
pub fn scalar_invert(a: &Scalar) -> Result<Scalar, Error> {
Option::from(a.invert()).ok_or(Error::ScalarInversion)
}
/// Compute base^exp mod N using repeated squaring.
/// Mirrors `pow_n` in the TS implementation.
pub fn pow_n(base: u64, exp: u64) -> Scalar {
if exp == 0 {
return Scalar::ONE;
}
let mut result = Scalar::ONE;
let mut b = mod_n(U256::from(base));
let mut e = exp;
while e > 0 {
if e & 1 == 1 {
result = result * b;
}
b = b * b;
e >>= 1;
}
result
}
/// Lift an x-only (32-byte) or compressed (33-byte) pubkey to a ProjectivePoint.
/// Mirrors `lift_x` in the TS implementation — always returns the even-Y variant.
pub fn lift_x(bytes: &[u8]) -> Result<ProjectivePoint, Error> {
let encoded = match bytes.len() {
32 => {
// Prepend 0x02 (even parity) to get a compressed point.
let mut buf = [0u8; 33];
buf[0] = 0x02;
buf[1..].copy_from_slice(bytes);
EncodedPoint::from_bytes(&buf).map_err(|_| Error::InvalidPoint)?
}
33 => EncodedPoint::from_bytes(bytes).map_err(|_| Error::InvalidPoint)?,
_ => return Err(Error::InvalidPoint),
};
let pt = ProjectivePoint::from_encoded_point(&encoded);
Option::from(pt).ok_or(Error::InvalidPoint)
}
/// Serialize a ProjectivePoint to a 33-byte compressed encoding.
pub fn serialize_point(pt: &ProjectivePoint) -> [u8; 33] {
let encoded = pt.to_encoded_point(true);
let bytes = encoded.as_bytes();
let mut out = [0u8; 33];
out.copy_from_slice(bytes);
out
}
/// Deserialize a 33-byte compressed point.
pub fn deserialize_point(bytes: &[u8; 33]) -> Result<ProjectivePoint, Error> {
let encoded = EncodedPoint::from_bytes(bytes as &[u8]).map_err(|_| Error::InvalidPoint)?;
Option::from(ProjectivePoint::from_encoded_point(&encoded)).ok_or(Error::InvalidPoint)
}
/// Returns true if the point has an even Y coordinate.
pub fn has_even_y(pt: &ProjectivePoint) -> bool {
let encoded = pt.to_encoded_point(true);
encoded.as_bytes()[0] == 0x02
}
/// Negate a point (flip Y).
pub fn negate_point(pt: &ProjectivePoint) -> ProjectivePoint {
Neg::neg(*pt)
}
+48
View File
@@ -0,0 +1,48 @@
// Mirrors ref/frost/src/lib/ecdh.ts
use k256::Scalar;
use crate::ecc::group::{element_add, scalar_multi, serialize_element};
use crate::ecc::util::{lift_x, scalar_from_bytes};
use crate::poly::{calc_lagrange_coeff, index_to_scalar};
use crate::types::{PublicShare, SecretShare};
use crate::Error;
/// Compute an ECDH share: a participant's contribution to a shared secret.
/// Mirrors `create_ecdh_share` in the TS implementation.
pub fn create_ecdh_share(
members: &[u32],
share: &SecretShare,
pubkey: &[u8; 33],
) -> Result<PublicShare, Error> {
let mbrs: Vec<Scalar> = members
.iter()
.filter(|&&idx| idx != share.idx)
.map(|&idx| index_to_scalar(idx))
.collect();
let idx = index_to_scalar(share.idx);
let secret = scalar_from_bytes(&share.seckey);
let point = lift_x(pubkey)?;
let l_coeff = calc_lagrange_coeff(&mbrs, idx, Scalar::ZERO)?;
let p_coeff = l_coeff * secret;
let ecdh_pt = scalar_multi(&point, &p_coeff);
Ok(PublicShare {
idx: share.idx,
pubkey: serialize_element(&ecdh_pt),
})
}
/// Derive the shared ECDH secret by summing all participant ECDH shares.
/// Mirrors `derive_ecdh_secret` in the TS implementation.
pub fn derive_ecdh_secret(shares: &[PublicShare]) -> Result<[u8; 33], Error> {
let mut point = None;
for share in shares {
let pt = lift_x(&share.pubkey)?;
point = Some(element_add(point, Some(pt))?);
}
let pt = point.ok_or(Error::BothPointsNull)?;
Ok(serialize_element(&pt))
}
+40
View File
@@ -0,0 +1,40 @@
// Mirrors ref/frost/src/lib/group.ts
// High-level share set creation (trusted dealer).
use crate::shares::create_shares;
use crate::types::{DealerShareSet, SecretShareSet};
use crate::vss::{create_share_coeffs, get_share_commits};
use crate::Error;
/// Create a set of secret shares and VSS commitments.
/// Mirrors `create_share_set` in the TS implementation.
pub fn create_share_set(
threshold: usize,
share_max: u32,
secrets: &[[u8; 32]],
) -> Result<SecretShareSet, Error> {
let coeffs = create_share_coeffs(secrets, threshold);
let shares = create_shares(&coeffs, share_max)?;
let vss_commits = get_share_commits(&coeffs);
Ok(SecretShareSet {
shares,
vss_commits,
})
}
/// Create a dealer share set that also exposes the group public key.
/// The group public key is the first VSS commitment (the constant term's public key).
/// Mirrors `create_dealer_set` in the TS implementation.
pub fn create_dealer_set(
threshold: usize,
share_max: u32,
secrets: &[[u8; 32]],
) -> Result<DealerShareSet, Error> {
let share_set = create_share_set(threshold, share_max, secrets)?;
let group_pk = share_set.vss_commits[0];
Ok(DealerShareSet {
shares: share_set.shares,
vss_commits: share_set.vss_commits,
group_pk,
})
}
+103
View File
@@ -0,0 +1,103 @@
// Mirrors ref/frost/src/lib/helpers.ts
use k256::Scalar;
use crate::ecc::group::{scalar_base_multi, scalar_multi, serialize_element};
use crate::ecc::hash::h3;
use crate::ecc::util::{lift_x, mod_n, scalar_from_bytes, scalar_to_bytes};
use crate::util::assert;
use crate::util::helpers::{hash340, random_bytes_32};
use crate::Error;
use k256::U256;
/// Generate a secret key by hashing optional auxiliary bytes through H3.
/// Mirrors `generate_seckey` in the TS implementation.
pub fn generate_seckey(aux: Option<&[u8; 32]>) -> [u8; 32] {
let aux_bytes = match aux {
Some(a) => *a,
None => random_bytes_32(),
};
h3(&aux_bytes)
}
/// Generate a secret nonce by hashing (aux || secret) through H3.
/// Mirrors `generate_nonce` in the TS implementation.
pub fn generate_nonce(secret: &[u8; 32], aux_seed: Option<&[u8; 32]>) -> [u8; 32] {
let aux = match aux_seed {
Some(a) => *a,
None => random_bytes_32(),
};
let mut input = [0u8; 64];
input[..32].copy_from_slice(&aux);
input[32..].copy_from_slice(secret);
h3(&input)
}
/// Derive a compressed public key from a secret key.
/// Mirrors `get_pubkey` in the TS implementation.
pub fn get_pubkey(secret: &[u8; 32]) -> [u8; 33] {
let scalar = scalar_from_bytes(secret);
let point = scalar_base_multi(&scalar);
serialize_element(&point)
}
/// Tweak a secret key by multiplying by a tweak scalar.
/// Mirrors `tweak_seckey` in the TS implementation.
pub fn tweak_seckey(seckey: &[u8; 32], tweak: &[u8; 32]) -> [u8; 32] {
let coeff = scalar_from_bytes(tweak);
let secret = scalar_from_bytes(seckey);
scalar_to_bytes(&(secret * coeff))
}
/// Tweak a public key by multiplying by a tweak scalar.
/// Mirrors `tweak_pubkey` in the TS implementation.
pub fn tweak_pubkey(pubkey: &[u8], tweak: &[u8; 32]) -> Result<[u8; 33], Error> {
let coeff = scalar_from_bytes(tweak);
let point = lift_x(pubkey)?;
let tweaked = scalar_multi(&point, &coeff);
Ok(serialize_element(&tweaked))
}
/// Compute a BIP340-compatible challenge hash.
/// challenge = SHA256(SHA256("BIP0340/challenge") || SHA256("BIP0340/challenge") || R_x || P_x || msg)
/// Mirrors `get_challenge` in the TS implementation.
pub fn get_challenge(pnonce: &[u8], pubkey: &[u8], message: &[u8]) -> Result<Scalar, Error> {
let grp_pn = convert_pubkey_to_bip340(pnonce)?;
let grp_pk = convert_pubkey_to_bip340(pubkey)?;
assert::ok(
grp_pn.len() == 32,
"pnonce must be 32 bytes after conversion",
)?;
assert::ok(
grp_pk.len() == 32,
"pubkey must be 32 bytes after conversion",
)?;
let digest = hash340("BIP0340/challenge", &[&grp_pn, &grp_pk, message]);
Ok(mod_n(U256::from_be_slice(&digest)))
}
/// Convert a pubkey to BIP340 format (x-only, 32 bytes).
/// If 33 bytes (compressed), strip the prefix byte.
/// If 32 bytes, return as-is.
pub fn convert_pubkey_to_bip340(pubkey: &[u8]) -> Result<Vec<u8>, Error> {
match pubkey.len() {
33 => Ok(pubkey[1..].to_vec()),
32 => Ok(pubkey.to_vec()),
_ => Err(Error::InvalidPoint),
}
}
/// Convert a pubkey to ECDSA format (compressed, 33 bytes).
/// If 32 bytes (x-only), prepend 0x02.
/// If 33 bytes, return as-is.
pub fn convert_pubkey_to_ecdsa(pubkey: &[u8]) -> Result<Vec<u8>, Error> {
match pubkey.len() {
32 => {
let mut out = vec![0x02u8];
out.extend_from_slice(pubkey);
Ok(out)
}
33 => Ok(pubkey.to_vec()),
_ => Err(Error::InvalidPoint),
}
}
+35
View File
@@ -0,0 +1,35 @@
pub mod ecc;
pub mod types;
pub mod util;
pub mod commit;
pub mod context;
pub mod ecdh;
pub mod group;
pub mod helpers;
pub mod poly;
pub mod recover;
pub mod refresh;
pub mod shares;
pub mod sign;
pub mod vss;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
#[error("assertion failed: {0}")]
Assertion(String),
#[error("invalid point encoding")]
InvalidPoint,
#[error("both points are null")]
BothPointsNull,
#[error("scalar inversion failed (zero scalar)")]
ScalarInversion,
#[error("record not found for index: {0}")]
RecordNotFound(u32),
}
+88
View File
@@ -0,0 +1,88 @@
// Mirrors ref/frost/src/lib/poly.ts
// Polynomial evaluation and Lagrange interpolation over the secp256k1 scalar field.
use k256::Scalar;
use crate::ecc::util::{scalar_from_bytes, scalar_invert};
use crate::util::assert;
use crate::Error;
/// Evaluate a polynomial at x using Horner's method.
/// Coefficients are in ascending order: coeffs[0] + coeffs[1]*x + coeffs[2]*x^2 + ...
/// Mirrors `evaluate_x` in the TS implementation.
pub fn evaluate_x(coeffs: &[Scalar], x: Scalar) -> Result<Scalar, Error> {
if x == Scalar::ZERO {
return Err(Error::Assertion("x is zero".to_string()));
}
let mut value = Scalar::ZERO;
for coeff in coeffs.iter().rev() {
value = value * x + coeff;
}
Ok(value)
}
/// Interpolate a polynomial at x=0 (the root) using Lagrange interpolation.
/// Points are (x, y) pairs of Scalars.
/// Mirrors `interpolate_root` in the TS implementation.
pub fn interpolate_root(points: &[(Scalar, Scalar)]) -> Result<Scalar, Error> {
let xs: Vec<Scalar> = points.iter().map(|(x, _)| *x).collect();
let mut p = Scalar::ZERO;
for (x, y) in points {
let delta = interpolate_x(&xs, *x)?;
p = p + delta * y;
}
Ok(p)
}
/// Compute the Lagrange basis polynomial value at x=0 for the given x-coordinate,
/// relative to the set of x-coordinates L.
/// Mirrors `interpolate_x` in the TS implementation.
pub fn interpolate_x(l: &[Scalar], x: Scalar) -> Result<Scalar, Error> {
assert::is_included(l, &x)?;
assert::is_unique_set(l)?;
let mut numerator = Scalar::ONE;
let mut denominator = Scalar::ONE;
for &x_j in l {
if x_j == x {
continue;
}
numerator = numerator * x_j;
denominator = denominator * (x_j + (-x)); // x_j - x
}
let inv = scalar_invert(&denominator)?;
Ok(numerator * inv)
}
/// Compute the Lagrange coefficient for participant P relative to members L,
/// evaluated at point x.
/// Mirrors `calc_lagrange_coeff` in the TS implementation.
pub fn calc_lagrange_coeff(l: &[Scalar], p: Scalar, x: Scalar) -> Result<Scalar, Error> {
assert::is_unique_set(l)?;
let mut numerator = Scalar::ONE;
let mut denominator = Scalar::ONE;
for &x_j in l {
if x_j == p {
continue;
}
numerator = numerator * (x + (-x_j)); // x - x_j
denominator = denominator * (p + (-x_j)); // p - x_j
}
let inv = scalar_invert(&denominator)?;
Ok(numerator * inv)
}
/// Convert a u32 participant index to a Scalar.
pub fn index_to_scalar(idx: u32) -> Scalar {
scalar_from_bytes(&{
let mut b = [0u8; 32];
b[28..].copy_from_slice(&idx.to_be_bytes());
b
})
}
+86
View File
@@ -0,0 +1,86 @@
// Mirrors ref/frost/src/lib/recover.ts
use k256::Scalar;
use crate::ecc::util::{scalar_from_bytes, scalar_to_bytes};
use crate::poly::{calc_lagrange_coeff, index_to_scalar};
use crate::types::{SecretShare, SecretSharePackage};
use crate::util::assert;
use crate::vss::{create_share_coeffs, get_share_commits};
use crate::Error;
/// Generate recovery shares that allow a target participant to reconstruct their share.
/// Mirrors `gen_recovery_shares` in the TS implementation.
pub fn gen_recovery_shares(
members: &[u32],
share: &SecretShare,
target: u32,
threshold: usize,
secrets: &[[u8; 32]],
) -> Result<SecretSharePackage, Error> {
assert::ok(
members.len() >= threshold,
"not enough members to meet threshold",
)?;
let mut sorted_members = members.to_vec();
sorted_members.sort();
let share_idx = index_to_scalar(share.idx);
let target_idx = index_to_scalar(target);
// Members excluding the current share holder.
let mbrs: Vec<Scalar> = sorted_members
.iter()
.filter(|&&idx| idx != share.idx)
.map(|&idx| index_to_scalar(idx))
.collect();
let share_seckey = scalar_from_bytes(&share.seckey);
let lgrng_coeff = calc_lagrange_coeff(&mbrs, share_idx, target_idx)?;
assert::ok(
lgrng_coeff != Scalar::ZERO,
"lagrange coefficient must be greater than zero",
)?;
let rand_coeffs = create_share_coeffs(secrets, threshold - 1);
let coeff_sum = rand_coeffs.iter().fold(Scalar::ZERO, |p, n| p + n);
let repair_coeff = lgrng_coeff * share_seckey - coeff_sum;
let repair_shares: Vec<Scalar> = rand_coeffs
.iter()
.cloned()
.chain(std::iter::once(repair_coeff))
.collect();
let vss_commits = get_share_commits(&repair_shares);
let shares: Vec<SecretShare> = sorted_members
.iter()
.enumerate()
.map(|(i, &idx)| SecretShare {
idx,
seckey: scalar_to_bytes(&repair_shares[i]),
})
.collect();
Ok(SecretSharePackage {
idx: share.idx,
vss_commits,
shares,
})
}
/// Recover a participant's share by summing aggregated recovery shares.
/// Mirrors `recover_share` in the TS implementation.
pub fn recover_share(shares: &[SecretShare], idx: u32) -> SecretShare {
let summed = shares
.iter()
.map(|s| scalar_from_bytes(&s.seckey))
.fold(Scalar::ZERO, |p, n| p + n);
SecretShare {
idx,
seckey: scalar_to_bytes(&summed),
}
}
+47
View File
@@ -0,0 +1,47 @@
// Mirrors ref/frost/src/lib/refresh.ts
use k256::Scalar;
use crate::shares::{combine_set, create_shares};
use crate::types::{SecretShare, SecretSharePackage};
use crate::vss::{create_share_coeffs, get_share_commits};
use crate::Error;
/// Generate refresh shares for proactive secret sharing.
/// The polynomial has a zero constant term, so adding these shares to existing
/// shares does not change the underlying secret.
/// Mirrors `gen_refresh_shares` in the TS implementation.
pub fn gen_refresh_shares(
index: u32,
threshold: usize,
share_max: u32,
secrets: &[[u8; 32]],
) -> Result<SecretSharePackage, Error> {
// Auxiliary coefficients (threshold - 1 of them, no constant term).
let sub_coeffs = create_share_coeffs(secrets, threshold - 1);
// Prepend zero as the constant term so the polynomial evaluates to 0 at x=0.
let coeffs: Vec<Scalar> = std::iter::once(Scalar::ZERO)
.chain(sub_coeffs.iter().cloned())
.collect();
let shares = create_shares(&coeffs, share_max)?;
let vss_commits = get_share_commits(&sub_coeffs);
Ok(SecretSharePackage {
idx: index,
vss_commits,
shares,
})
}
/// Apply refresh shares to a current share by summing them.
/// Mirrors `refresh_share` in the TS implementation.
pub fn refresh_share(
refresh_shares: &[SecretShare],
current_share: &SecretShare,
) -> Result<SecretShare, Error> {
let all: Vec<SecretShare> = std::iter::once(current_share.clone())
.chain(refresh_shares.iter().cloned())
.collect();
combine_set(&all)
}
+103
View File
@@ -0,0 +1,103 @@
// Mirrors ref/frost/src/lib/shares.ts
use k256::{elliptic_curve::point::AffineCoordinates, Scalar};
use crate::ecc::group::{scalar_base_multi, scalar_multi};
use crate::ecc::util::{lift_x, pow_n, scalar_from_bytes, scalar_to_bytes};
use crate::poly::{evaluate_x, index_to_scalar, interpolate_root};
use crate::types::SecretShare;
use crate::util::assert;
use crate::Error;
/// Create secret shares by evaluating the polynomial at indices 1..=count.
/// Mirrors `create_shares` in the TS implementation.
pub fn create_shares(coeffs: &[Scalar], count: u32) -> Result<Vec<SecretShare>, Error> {
let mut shares = Vec::with_capacity(count as usize);
for i in 1..=count {
let x = index_to_scalar(i);
let scalar = evaluate_x(coeffs, x)?;
shares.push(SecretShare {
idx: i,
seckey: scalar_to_bytes(&scalar),
});
}
Ok(shares)
}
/// Sum a list of secret shares into a single scalar (used in DKG aggregation).
/// Mirrors `combine_shares` in the TS implementation.
pub fn combine_shares(shares: &[SecretShare]) -> [u8; 32] {
let secret = shares
.iter()
.map(|s| scalar_from_bytes(&s.seckey))
.fold(Scalar::ZERO, |acc, cur| acc + cur);
scalar_to_bytes(&secret)
}
/// Combine a set of shares that all have the same idx into one share.
/// Mirrors `combine_set` in the TS implementation.
pub fn combine_set(shares: &[SecretShare]) -> Result<SecretShare, Error> {
assert::is_equal_set(&shares.iter().map(|s| s.idx).collect::<Vec<_>>())?;
let idx = shares[0].idx;
let seckey = combine_shares(shares);
Ok(SecretShare { idx, seckey })
}
/// Merge two lists of shares by combining matching indices.
/// Mirrors `merge_shares` in the TS implementation.
pub fn merge_shares(
shares_a: &[SecretShare],
shares_b: &[SecretShare],
) -> Result<Vec<SecretShare>, Error> {
assert::equal_arr_size(shares_a, shares_b)?;
shares_a
.iter()
.map(|curr| {
let aux = shares_b
.iter()
.find(|s| s.idx == curr.idx)
.ok_or(Error::RecordNotFound(curr.idx))?;
combine_set(&[curr.clone(), aux.clone()])
})
.collect()
}
/// Verify a secret share against VSS commitments.
/// Mirrors `verify_share` in the TS implementation.
pub fn verify_share(
commits: &[[u8; 33]],
share: &SecretShare,
threshold: usize,
) -> Result<bool, Error> {
let scalar = scalar_from_bytes(&share.seckey);
let s_i = scalar_base_multi(&scalar);
let mut s_ip = None;
for j in 0..threshold {
let point = lift_x(&commits[j])?;
let exp = pow_n(share.idx as u64, j as u64);
let prod = scalar_multi(&point, &exp);
s_ip = Some(match s_ip {
None => prod,
Some(acc) => acc + prod,
});
}
let s_ip = s_ip.ok_or(Error::Assertion("no commits".to_string()))?;
// Compare x-coordinates (affine).
let s_i_affine = s_i.to_affine();
let s_ip_affine = s_ip.to_affine();
Ok(s_i_affine.x() == s_ip_affine.x())
}
/// Recover the group secret by Lagrange interpolation over a threshold of shares.
/// Mirrors `derive_shares_secret` in the TS implementation.
pub fn derive_shares_secret(shares: &[SecretShare]) -> Result<[u8; 32], Error> {
let points: Vec<(Scalar, Scalar)> = shares
.iter()
.map(|s| (index_to_scalar(s.idx), scalar_from_bytes(&s.seckey)))
.collect();
let secret = interpolate_root(&points)?;
Ok(scalar_to_bytes(&secret))
}
+141
View File
@@ -0,0 +1,141 @@
// Mirrors ref/frost/src/lib/sign.ts
use k256::{elliptic_curve::point::AffineCoordinates, Scalar};
use crate::commit::{get_bind_factor, get_group_binders, get_group_prefix, get_group_pubnonce};
use crate::ecc::group::{element_add, scalar_base_multi, scalar_multi};
use crate::ecc::util::{has_even_y, lift_x, scalar_from_bytes, scalar_to_bytes};
use crate::helpers::get_pubkey;
use crate::poly::{index_to_scalar, interpolate_x};
use crate::types::{
GroupKeyContext, GroupSigningCtx, PublicNonce, SecretNonce, SecretShare, ShareSignature,
};
use crate::Error;
/// Produce a partial signature for a signing session.
/// Mirrors `sign_msg` in the TS implementation.
pub fn sign_msg(
ctx: &GroupSigningCtx,
share: &SecretShare,
snonce: &SecretNonce,
) -> Result<ShareSignature, Error> {
if snonce.idx != share.idx {
return Err(Error::Assertion(format!(
"commit index does not match share index: {} !== {}",
snonce.idx, share.idx
)));
}
let bind_factor_bytes = get_bind_factor(&ctx.bind_factors, share.idx)?;
let bind_factor = scalar_from_bytes(&bind_factor_bytes);
let indexes: Vec<Scalar> = ctx.indexes.iter().map(|&i| index_to_scalar(i)).collect();
let coefficient = interpolate_x(&indexes, index_to_scalar(share.idx))?;
let mut snonce_h = scalar_from_bytes(&snonce.hidden_sn);
let mut snonce_b = scalar_from_bytes(&snonce.binder_sn);
let seckey = scalar_from_bytes(&share.seckey);
let r_elem = lift_x(&ctx.group_pn)?;
if !has_even_y(&r_elem) {
snonce_h = -snonce_h;
snonce_b = -snonce_b;
}
// sk = parity * state * seckey (mod N)
let sk = ctx.group_pt.parity * ctx.group_pt.state * seckey;
// nk = hidden_sn + binder_sn * bind_factor
let nk = snonce_h + snonce_b * bind_factor;
// ps = challenge * coefficient * sk + nk
let ps = ctx.challenge * coefficient * sk + nk;
Ok(ShareSignature {
idx: share.idx,
psig: scalar_to_bytes(&ps),
pubkey: get_pubkey(&share.seckey),
})
}
/// Aggregate partial signatures into a final BIP340 Schnorr signature (64 bytes).
/// Mirrors `combine_partial_sigs` in the TS implementation.
pub fn combine_partial_sigs(
ctx: &GroupSigningCtx,
psigs: &[ShareSignature],
) -> Result<[u8; 64], Error> {
let commit_prefix = get_group_prefix(&ctx.pnonces, &ctx.group_pk, &ctx.message);
let group_binders = get_group_binders(&ctx.pnonces, &commit_prefix);
let group_pnonce = get_group_pubnonce(&ctx.pnonces, &group_binders)?;
// Sum all partial signatures.
let ps = psigs
.iter()
.map(|s| scalar_from_bytes(&s.psig))
.fold(Scalar::ZERO, |acc, s| acc + s);
// twk = challenge * parity * tweak
let twk = ctx.challenge * ctx.group_pt.parity * ctx.group_pt.tweak;
let s = ps + twk;
// Signature = R_x (32 bytes) || s (32 bytes).
// group_pnonce is a 33-byte compressed point; x-only is bytes [1..33].
let mut sig = [0u8; 64];
sig[..32].copy_from_slice(&group_pnonce[1..]);
sig[32..].copy_from_slice(&scalar_to_bytes(&s));
Ok(sig)
}
/// Verify a partial signature from one participant.
/// Mirrors `verify_partial_sig` in the TS implementation.
pub fn verify_partial_sig(
ctx: &GroupSigningCtx,
pnonce: &PublicNonce,
share_pk: &[u8; 33],
share_psig: &[u8; 32],
) -> Result<bool, Error> {
let binder = scalar_from_bytes(&get_bind_factor(&ctx.bind_factors, pnonce.idx)?);
let mut hidden_elem = lift_x(&pnonce.hidden_pn)?;
let mut binder_elem = lift_x(&pnonce.binder_pn)?;
let public_elem = lift_x(share_pk)?;
let r_elem = lift_x(&ctx.group_pn)?;
if !has_even_y(&r_elem) {
hidden_elem = -hidden_elem;
binder_elem = -binder_elem;
}
let commit_elem = scalar_multi(&binder_elem, &binder);
let nonce_elem = element_add(Some(hidden_elem), Some(commit_elem))?;
let indexes: Vec<Scalar> = ctx.indexes.iter().map(|&i| index_to_scalar(i)).collect();
let lambda_i = interpolate_x(&indexes, index_to_scalar(pnonce.idx))?;
let state = ctx.group_pt.parity * ctx.group_pt.state;
let chal = ctx.challenge * lambda_i * state;
let sig = scalar_from_bytes(share_psig);
let sg = scalar_base_multi(&sig);
let pki = scalar_multi(&public_elem, &chal);
let r = element_add(Some(nonce_elem), Some(pki))?;
// Compare x-coordinates.
Ok(sg.to_affine().x() == r.to_affine().x())
}
/// Verify a final aggregated BIP340 Schnorr signature.
/// Mirrors `verify_final_sig` in the TS implementation.
pub fn verify_final_sig(
ctx: &GroupKeyContext,
message: &[u8],
signature: &[u8; 64],
) -> Result<bool, Error> {
use k256::schnorr::{Signature, VerifyingKey};
use signature::Verifier;
// group_pk is 33-byte compressed; BIP340 uses x-only (32 bytes).
let pk_bytes: [u8; 32] = ctx.group_pk[1..].try_into().unwrap();
let vk = VerifyingKey::from_bytes(&pk_bytes).map_err(|_| Error::InvalidPoint)?;
let sig = Signature::try_from(signature.as_slice()).map_err(|_| Error::InvalidPoint)?;
Ok(vk.verify(message, &sig).is_ok())
}
+53
View File
@@ -0,0 +1,53 @@
// Mirrors ref/frost/src/types/commit.ts
/// A participant's secret nonces for one signing round.
#[derive(Clone, Debug)]
pub struct SecretNonce {
pub idx: u32,
pub binder_sn: [u8; 32],
pub hidden_sn: [u8; 32],
}
/// A participant's public nonce commitments for one signing round.
#[derive(Clone, Debug)]
pub struct PublicNonce {
pub idx: u32,
pub binder_pn: [u8; 33],
pub hidden_pn: [u8; 33],
}
/// Combined secret + public nonce package for a participant.
/// Mirrors `CommitmentPackage = SecretNonce & PublicNonce`.
#[derive(Clone, Debug)]
pub struct CommitmentPackage {
pub idx: u32,
pub binder_sn: [u8; 32],
pub hidden_sn: [u8; 32],
pub binder_pn: [u8; 33],
pub hidden_pn: [u8; 33],
}
impl CommitmentPackage {
pub fn secret_nonce(&self) -> SecretNonce {
SecretNonce {
idx: self.idx,
binder_sn: self.binder_sn,
hidden_sn: self.hidden_sn,
}
}
pub fn public_nonce(&self) -> PublicNonce {
PublicNonce {
idx: self.idx,
binder_pn: self.binder_pn,
hidden_pn: self.hidden_pn,
}
}
}
/// Per-participant binding factor.
#[derive(Clone, Debug)]
pub struct BindFactor {
pub idx: u32,
pub factor: [u8; 32],
}
+58
View File
@@ -0,0 +1,58 @@
// Mirrors ref/frost/src/types/ctx.ts
use k256::Scalar;
use super::{BindFactor, PointState, PublicNonce};
/// Key context: the group key and its tweaked state.
#[derive(Clone, Debug)]
pub struct GroupKeyContext {
pub group_pt: PointState,
pub group_pk: [u8; 33],
pub int_pt: Option<k256::ProjectivePoint>,
pub int_pk: Option<[u8; 33]>,
pub tweak: Option<[u8; 32]>,
}
/// Commit context: everything derived from the nonces and message.
#[derive(Clone, Debug)]
pub struct GroupCommitContext {
pub bind_factors: Vec<BindFactor>,
pub bind_prefix: Vec<u8>,
pub challenge: Scalar,
pub group_pn: [u8; 33],
pub indexes: Vec<u32>,
pub message: Vec<u8>,
pub pnonces: Vec<PublicNonce>,
}
/// Full signing context = key context + commit context.
#[derive(Clone, Debug)]
pub struct GroupSigningCtx {
// Key context fields
pub group_pt: PointState,
pub group_pk: [u8; 33],
pub int_pt: Option<k256::ProjectivePoint>,
pub int_pk: Option<[u8; 33]>,
pub tweak: Option<[u8; 32]>,
// Commit context fields
pub bind_factors: Vec<BindFactor>,
pub bind_prefix: Vec<u8>,
pub challenge: Scalar,
pub group_pn: [u8; 33],
pub indexes: Vec<u32>,
pub message: Vec<u8>,
pub pnonces: Vec<PublicNonce>,
}
impl GroupSigningCtx {
pub fn key_context(&self) -> GroupKeyContext {
GroupKeyContext {
group_pt: self.group_pt.clone(),
group_pk: self.group_pk,
int_pt: self.int_pt,
int_pk: self.int_pk,
tweak: self.tweak,
}
}
}
+17
View File
@@ -0,0 +1,17 @@
// Mirrors ref/frost/src/types/ecc.ts
use k256::{ProjectivePoint, Scalar};
/// Accumulated parity/tweak state for a group key.
/// Mirrors `PointState` in the TS implementation.
#[derive(Clone, Debug)]
pub struct PointState {
/// Current parity factor (+1 or -1 as a Scalar).
pub parity: Scalar,
/// The (possibly tweaked) point.
pub point: ProjectivePoint,
/// Accumulated state (product of parities).
pub state: Scalar,
/// Accumulated tweak value.
pub tweak: Scalar,
}
+11
View File
@@ -0,0 +1,11 @@
mod commit;
mod ctx;
mod ecc;
mod share;
mod sign;
pub use commit::*;
pub use ctx::*;
pub use ecc::*;
pub use share::*;
pub use sign::*;
+39
View File
@@ -0,0 +1,39 @@
// Mirrors ref/frost/src/types/share.ts
/// A participant's secret share of the group key.
#[derive(Clone, Debug)]
pub struct SecretShare {
pub idx: u32,
pub seckey: [u8; 32],
}
/// A participant's public share (commitment).
#[derive(Clone, Debug)]
pub struct PublicShare {
pub idx: u32,
pub pubkey: [u8; 33],
}
/// A set of secret shares with VSS commitments, produced by one dealer/participant.
#[derive(Clone, Debug)]
pub struct SecretShareSet {
pub shares: Vec<SecretShare>,
pub vss_commits: Vec<[u8; 33]>,
}
/// A dealer-produced share set that also includes the group public key.
#[derive(Clone, Debug)]
pub struct DealerShareSet {
pub shares: Vec<SecretShare>,
pub vss_commits: Vec<[u8; 33]>,
pub group_pk: [u8; 33],
}
/// A secret share set tagged with the originating participant index.
/// Mirrors `SecretSharePackage` in the TS implementation.
#[derive(Clone, Debug)]
pub struct SecretSharePackage {
pub idx: u32,
pub shares: Vec<SecretShare>,
pub vss_commits: Vec<[u8; 33]>,
}
+9
View File
@@ -0,0 +1,9 @@
// Mirrors ref/frost/src/types/sign.ts
/// A partial signature produced by one participant.
#[derive(Clone, Debug)]
pub struct ShareSignature {
pub idx: u32,
pub pubkey: [u8; 33],
pub psig: [u8; 32],
}
+56
View File
@@ -0,0 +1,56 @@
// Mirrors ref/frost/src/util/assert.ts
use crate::Error;
pub fn ok(value: bool, message: &str) -> Result<(), Error> {
if !value {
Err(Error::Assertion(message.to_string()))
} else {
Ok(())
}
}
pub fn is_included<T: PartialEq>(array: &[T], item: &T) -> Result<(), Error> {
if !array.contains(item) {
Err(Error::Assertion(
"item is not included in array".to_string(),
))
} else {
Ok(())
}
}
pub fn is_unique_set<T: PartialEq + std::fmt::Debug>(array: &[T]) -> Result<(), Error> {
for x in array {
let count = array.iter().filter(|e| *e == x).count();
if count != 1 {
return Err(Error::Assertion(format!(
"item in set is not unique: {:?}",
x
)));
}
}
Ok(())
}
pub fn is_equal_set<T: PartialEq>(array: &[T]) -> Result<(), Error> {
if !array.windows(2).all(|w| w[0] == w[1]) {
Err(Error::Assertion(
"set does not have equal items".to_string(),
))
} else {
Ok(())
}
}
pub fn equal_arr_size<T, U>(a: &[T], b: &[U]) -> Result<(), Error> {
if a.len() != b.len() {
Err(Error::Assertion(format!(
"array lengths are unequal: {} !== {}",
a.len(),
b.len()
)))
} else {
Ok(())
}
}
+56
View File
@@ -0,0 +1,56 @@
// Mirrors ref/frost/src/util/helpers.ts
use rand::rngs::OsRng;
use rand::RngCore;
use sha2::{Digest, Sha256};
use crate::Error;
/// Generate `size` random bytes.
pub fn random_bytes(size: usize) -> Vec<u8> {
let mut buf = vec![0u8; size];
OsRng.fill_bytes(&mut buf);
buf
}
/// Generate 32 random bytes.
pub fn random_bytes_32() -> [u8; 32] {
let mut buf = [0u8; 32];
OsRng.fill_bytes(&mut buf);
buf
}
/// Find a record by idx in a slice of items that have an `idx` field.
/// Mirrors `get_record` in the TS implementation.
pub fn get_record<T: HasIdx + Clone>(records: &[T], idx: u32) -> Result<T, Error> {
records
.iter()
.find(|r| r.idx() == idx)
.cloned()
.ok_or_else(|| Error::RecordNotFound(idx))
}
/// Trait for types that carry a participant index.
pub trait HasIdx {
fn idx(&self) -> u32;
}
/// Compute a BIP340-style tagged hash prefix: SHA256(tag) || SHA256(tag).
pub fn taghash(tag: &str) -> [u8; 64] {
let hash: [u8; 32] = Sha256::digest(tag.as_bytes()).into();
let mut out = [0u8; 64];
out[..32].copy_from_slice(&hash);
out[32..].copy_from_slice(&hash);
out
}
/// BIP340 tagged hash: SHA256(SHA256(tag) || SHA256(tag) || data...).
pub fn hash340(tag: &str, data: &[&[u8]]) -> [u8; 32] {
let prefix = taghash(tag);
let mut hasher = Sha256::new();
hasher.update(&prefix);
for d in data {
hasher.update(d);
}
hasher.finalize().into()
}
+4
View File
@@ -0,0 +1,4 @@
pub mod assert;
pub mod helpers;
pub use helpers::*;
+55
View File
@@ -0,0 +1,55 @@
// Mirrors ref/frost/src/lib/vss.ts
// Verifiable Secret Sharing: coefficient generation and commitments.
use k256::Scalar;
use crate::ecc::group::{scalar_base_multi, serialize_element};
use crate::ecc::util::mod_n;
use crate::util::assert;
use crate::util::helpers::random_bytes_32;
use crate::Error;
use k256::U256;
/// Create polynomial coefficients for a Shamir secret sharing scheme.
/// If fewer secrets are provided than `threshold`, the remaining coefficients are random.
/// Mirrors `create_share_coeffs` in the TS implementation.
pub fn create_share_coeffs(secrets: &[[u8; 32]], threshold: usize) -> Vec<Scalar> {
let mut coeffs = Vec::with_capacity(threshold);
for i in 0..threshold {
let coeff = if let Some(s) = secrets.get(i) {
mod_n(U256::from_be_slice(s))
} else {
mod_n(U256::from_be_slice(&random_bytes_32()))
};
coeffs.push(coeff);
}
coeffs
}
/// Compute VSS commitments: one compressed public key per coefficient.
/// Mirrors `get_share_commits` in the TS implementation.
pub fn get_share_commits(coeffs: &[Scalar]) -> Vec<[u8; 33]> {
coeffs
.iter()
.map(|c| serialize_element(&scalar_base_multi(c)))
.collect()
}
/// Merge two sets of VSS commitments by adding corresponding points.
/// Mirrors `merge_share_commits` in the TS implementation.
pub fn merge_share_commits(
commits_a: &[[u8; 33]],
commits_b: &[[u8; 33]],
) -> Result<Vec<[u8; 33]>, Error> {
assert::equal_arr_size(commits_a, commits_b)?;
commits_a
.iter()
.zip(commits_b.iter())
.map(|(a, b)| {
let pa = crate::ecc::util::lift_x(a)?;
let pb = crate::ecc::util::lift_x(b)?;
let pc = pa + pb;
Ok(serialize_element(&pc))
})
.collect()
}