Add frost-taproot implementation
This commit is contained in:
@@ -1 +1,2 @@
|
||||
ref
|
||||
target
|
||||
|
||||
@@ -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.
|
||||
|
||||
Generated
+420
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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};
|
||||
@@ -0,0 +1,89 @@
|
||||
// Mirrors ref/frost/src/ecc/hash.ts
|
||||
// FROST-secp256k1-SHA256-v1 hash functions H1–H5.
|
||||
// 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()
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod group;
|
||||
pub mod hash;
|
||||
pub mod state;
|
||||
pub mod util;
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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],
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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::*;
|
||||
@@ -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]>,
|
||||
}
|
||||
@@ -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],
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod assert;
|
||||
pub mod helpers;
|
||||
|
||||
pub use helpers::*;
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user