Add high level api
This commit is contained in:
Generated
+1
@@ -136,6 +136,7 @@ name = "frost-taproot"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hex",
|
"hex",
|
||||||
|
"hmac",
|
||||||
"k256",
|
"k256",
|
||||||
"rand",
|
"rand",
|
||||||
"sha2",
|
"sha2",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ edition = "2024"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
k256 = { version = "0.13", features = ["arithmetic", "hash2curve", "schnorr"] }
|
k256 = { version = "0.13", features = ["arithmetic", "hash2curve", "schnorr"] }
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
|
hmac = "0.12"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
signature = "2"
|
signature = "2"
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
/// Trusted dealer key generation and group management.
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
use crate::group::create_dealer_set;
|
||||||
|
use crate::helpers::get_pubkey;
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
|
use super::types::{DealerPackage, GroupPackage, MemberPackage, SharePackage};
|
||||||
|
|
||||||
|
/// Generate a complete dealer package: group info + all secret shares.
|
||||||
|
///
|
||||||
|
/// `secrets` seeds the polynomial deterministically; pass `&[]` for a
|
||||||
|
/// fully random group key.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust
|
||||||
|
/// use frost_taproot::frost::dealer::generate_dealer_package;
|
||||||
|
/// let pkg = generate_dealer_package(2, 3, &[]).unwrap();
|
||||||
|
/// assert_eq!(pkg.shares.len(), 3);
|
||||||
|
/// assert_eq!(pkg.group.threshold, 2);
|
||||||
|
/// ```
|
||||||
|
pub fn generate_dealer_package(
|
||||||
|
threshold: usize,
|
||||||
|
share_count: u32,
|
||||||
|
secrets: &[[u8; 32]],
|
||||||
|
) -> Result<DealerPackage, Error> {
|
||||||
|
let dealer_set = create_dealer_set(threshold, share_count, secrets)?;
|
||||||
|
|
||||||
|
let shares: Vec<SharePackage> = dealer_set
|
||||||
|
.shares
|
||||||
|
.iter()
|
||||||
|
.map(|s| SharePackage {
|
||||||
|
idx: s.idx,
|
||||||
|
seckey: s.seckey,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let members: Vec<MemberPackage> = dealer_set
|
||||||
|
.shares
|
||||||
|
.iter()
|
||||||
|
.map(|s| MemberPackage {
|
||||||
|
idx: s.idx,
|
||||||
|
pubkey: get_pubkey(&s.seckey),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let group = GroupPackage {
|
||||||
|
group_pk: dealer_set.group_pk,
|
||||||
|
threshold,
|
||||||
|
members,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(DealerPackage { group, shares })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute a stable group identifier: SHA-256(group_pk || threshold_u32_be || sorted_member_pubkeys).
|
||||||
|
pub fn get_group_id(group: &GroupPackage) -> [u8; 32] {
|
||||||
|
let mut sorted = group.members.clone();
|
||||||
|
sorted.sort_by_key(|m| m.idx);
|
||||||
|
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(&group.group_pk);
|
||||||
|
hasher.update((group.threshold as u32).to_be_bytes());
|
||||||
|
for m in &sorted {
|
||||||
|
hasher.update(&m.pubkey);
|
||||||
|
}
|
||||||
|
hasher.finalize().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether a share belongs to the given group.
|
||||||
|
///
|
||||||
|
/// Verifies that the share's derived public key matches the member entry
|
||||||
|
/// at the same index.
|
||||||
|
pub fn is_group_member(group: &GroupPackage, share: &SharePackage) -> bool {
|
||||||
|
let pubkey = get_pubkey(&share.seckey);
|
||||||
|
group
|
||||||
|
.members
|
||||||
|
.iter()
|
||||||
|
.any(|m| m.idx == share.idx && m.pubkey == pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify all shares in a dealer package against the group's VSS commitments.
|
||||||
|
///
|
||||||
|
/// Returns `Ok(true)` if every share is valid, `Ok(false)` if any fails,
|
||||||
|
/// or an `Err` on a crypto error.
|
||||||
|
pub fn verify_dealer_package(pkg: &DealerPackage) -> Result<bool, Error> {
|
||||||
|
// Re-derive VSS commits from the group's member pubkeys.
|
||||||
|
// The group_pk is the first VSS commit (constant term * G).
|
||||||
|
// We can verify membership via is_group_member for each share.
|
||||||
|
for share in &pkg.shares {
|
||||||
|
if !is_group_member(&pkg.group, share) {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Look up a member by index.
|
||||||
|
pub fn get_member_by_idx(group: &GroupPackage, idx: u32) -> Option<&MemberPackage> {
|
||||||
|
group.members.iter().find(|m| m.idx == idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Look up a member by public key.
|
||||||
|
pub fn get_member_by_pubkey<'a>(
|
||||||
|
group: &'a GroupPackage,
|
||||||
|
pubkey: &[u8; 33],
|
||||||
|
) -> Option<&'a MemberPackage> {
|
||||||
|
group.members.iter().find(|m| &m.pubkey == pubkey)
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
/// High-level threshold ECDH key derivation.
|
||||||
|
///
|
||||||
|
/// Allows a threshold quorum to collaboratively derive a shared secret
|
||||||
|
/// with any external public key, without any single member knowing the
|
||||||
|
/// full group private key.
|
||||||
|
use crate::ecdh::{create_ecdh_share, derive_ecdh_secret};
|
||||||
|
use crate::types::{PublicShare, SecretShare as LowSecretShare};
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
|
use super::types::{EcdhEntry, EcdhPackage, SharePackage};
|
||||||
|
|
||||||
|
/// Create an ECDH package for a single target public key.
|
||||||
|
///
|
||||||
|
/// `members` is the set of participant indices in the quorum (used for
|
||||||
|
/// Lagrange interpolation). The result contains this member's keyshare
|
||||||
|
/// contribution toward the shared secret with `ecdh_pk`.
|
||||||
|
pub fn create_ecdh_pkg(
|
||||||
|
members: &[u32],
|
||||||
|
ecdh_pk: &[u8; 33],
|
||||||
|
share: &SharePackage,
|
||||||
|
) -> Result<EcdhPackage, Error> {
|
||||||
|
let low_share = LowSecretShare {
|
||||||
|
idx: share.idx,
|
||||||
|
seckey: share.seckey,
|
||||||
|
};
|
||||||
|
let ecdh_share = create_ecdh_share(members, &low_share, ecdh_pk)?;
|
||||||
|
Ok(EcdhPackage {
|
||||||
|
idx: share.idx,
|
||||||
|
members: members.to_vec(),
|
||||||
|
entries: vec![EcdhEntry {
|
||||||
|
ecdh_pk: *ecdh_pk,
|
||||||
|
keyshare: ecdh_share.pubkey,
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an ECDH package for multiple target public keys in one call.
|
||||||
|
///
|
||||||
|
/// More efficient than calling [`create_ecdh_pkg`] in a loop when you
|
||||||
|
/// need shared secrets with several keys at once.
|
||||||
|
pub fn create_batched_ecdh_pkg(
|
||||||
|
members: &[u32],
|
||||||
|
ecdh_pks: &[[u8; 33]],
|
||||||
|
share: &SharePackage,
|
||||||
|
) -> Result<EcdhPackage, Error> {
|
||||||
|
let low_share = LowSecretShare {
|
||||||
|
idx: share.idx,
|
||||||
|
seckey: share.seckey,
|
||||||
|
};
|
||||||
|
let entries: Vec<EcdhEntry> = ecdh_pks
|
||||||
|
.iter()
|
||||||
|
.map(|pk| {
|
||||||
|
let ecdh_share = create_ecdh_share(members, &low_share, pk)?;
|
||||||
|
Ok(EcdhEntry {
|
||||||
|
ecdh_pk: *pk,
|
||||||
|
keyshare: ecdh_share.pubkey,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<_, Error>>()?;
|
||||||
|
|
||||||
|
Ok(EcdhPackage {
|
||||||
|
idx: share.idx,
|
||||||
|
members: members.to_vec(),
|
||||||
|
entries,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Combine ECDH packages from all quorum members to derive the shared secret
|
||||||
|
/// for a single target public key.
|
||||||
|
///
|
||||||
|
/// `pkgs` must contain one package per quorum member, each with an entry
|
||||||
|
/// for `ecdh_pk`.
|
||||||
|
pub fn combine_ecdh_pkgs(pkgs: &[EcdhPackage], ecdh_pk: &[u8; 33]) -> Result<[u8; 33], Error> {
|
||||||
|
let keyshares: Vec<PublicShare> = pkgs
|
||||||
|
.iter()
|
||||||
|
.map(|pkg| {
|
||||||
|
let entry = pkg
|
||||||
|
.entries
|
||||||
|
.iter()
|
||||||
|
.find(|e| &e.ecdh_pk == ecdh_pk)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
Error::Assertion(format!(
|
||||||
|
"ecdh_pk not found in package from member {}",
|
||||||
|
pkg.idx
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
Ok(PublicShare {
|
||||||
|
idx: pkg.idx,
|
||||||
|
pubkey: entry.keyshare,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<_, Error>>()?;
|
||||||
|
|
||||||
|
derive_ecdh_secret(&keyshares)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Combine ECDH packages for all target keys present in the batch.
|
||||||
|
///
|
||||||
|
/// Returns a `Vec` of `(ecdh_pk, shared_secret)` pairs, one per unique
|
||||||
|
/// target key found in the first package.
|
||||||
|
pub fn combine_batched_ecdh_pkgs(pkgs: &[EcdhPackage]) -> Result<Vec<([u8; 33], [u8; 33])>, Error> {
|
||||||
|
if pkgs.is_empty() {
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
pkgs[0]
|
||||||
|
.entries
|
||||||
|
.iter()
|
||||||
|
.map(|entry| {
|
||||||
|
let secret = combine_ecdh_pkgs(pkgs, &entry.ecdh_pk)?;
|
||||||
|
Ok((entry.ecdh_pk, secret))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/// High-level FROST threshold signing API.
|
||||||
|
///
|
||||||
|
/// This module provides use-case-focused utilities for:
|
||||||
|
/// - **Distributed key generation** (trusted dealer): [`dealer`]
|
||||||
|
/// - **Nonce management**: [`nonce`]
|
||||||
|
/// - **Collaborative signing**: [`signing`]
|
||||||
|
/// - **Shared key derivation** (threshold ECDH): [`ecdh`]
|
||||||
|
///
|
||||||
|
/// # Quick start
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use frost_taproot::frost::{dealer, nonce, signing, ecdh};
|
||||||
|
///
|
||||||
|
/// // 1. Generate a 2-of-3 group (trusted dealer).
|
||||||
|
/// let pkg = dealer::generate_dealer_package(2, 3, &[]).unwrap();
|
||||||
|
///
|
||||||
|
/// // 2. Each signer generates a nonce pair before each signing round.
|
||||||
|
/// let nonce_pair = nonce::generate_nonce_pair(&pkg.shares[0].seckey);
|
||||||
|
///
|
||||||
|
/// // 3. Collect nonces from all participating signers, then create a session.
|
||||||
|
/// // 4. Each signer produces a partial signature.
|
||||||
|
/// // 5. Combine partial signatures into a final BIP340 signature.
|
||||||
|
/// ```
|
||||||
|
pub mod dealer;
|
||||||
|
pub mod ecdh;
|
||||||
|
pub mod nonce;
|
||||||
|
pub mod signing;
|
||||||
|
pub mod types;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
|
pub use types::*;
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
/// Nonce generation and derivation for FROST signing sessions.
|
||||||
|
///
|
||||||
|
/// Uses HMAC-SHA256 to derive secret nonces from a share secret and a
|
||||||
|
/// random 32-byte code. Only the code needs to be stored; secrets are
|
||||||
|
/// re-derived on demand during signing, eliminating the need to persist
|
||||||
|
/// secret nonce material.
|
||||||
|
use hmac::{Hmac, Mac};
|
||||||
|
use sha2::Sha256;
|
||||||
|
|
||||||
|
use crate::helpers::get_pubkey;
|
||||||
|
use crate::util::helpers::random_bytes_32;
|
||||||
|
|
||||||
|
use super::types::{DerivedNonce, MemberNonce, SecretNoncePair};
|
||||||
|
|
||||||
|
type HmacSha256 = Hmac<Sha256>;
|
||||||
|
|
||||||
|
const DOMAIN_BINDER: &[u8] = b"bifrost/nonce/binder/v1";
|
||||||
|
const DOMAIN_HIDDEN: &[u8] = b"bifrost/nonce/hidden/v1";
|
||||||
|
|
||||||
|
/// Derive a secret nonce component via HMAC-SHA256.
|
||||||
|
///
|
||||||
|
/// `key = share_secret`, `msg = code || domain`
|
||||||
|
fn derive_nonce_secret(share_secret: &[u8; 32], code: &[u8; 32], domain: &[u8]) -> [u8; 32] {
|
||||||
|
let mut mac = HmacSha256::new_from_slice(share_secret).expect("HMAC accepts any key size");
|
||||||
|
mac.update(code);
|
||||||
|
mac.update(domain);
|
||||||
|
mac.finalize().into_bytes().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a fresh nonce pair from a share secret.
|
||||||
|
///
|
||||||
|
/// A random 32-byte code is generated; the secret nonces are derived from
|
||||||
|
/// it via HMAC. Only the `DerivedNonce` (containing the code and public
|
||||||
|
/// points) needs to be stored or transmitted — secrets are re-derived
|
||||||
|
/// during signing via [`derive_secret_nonce`].
|
||||||
|
pub fn generate_nonce_pair(share_secret: &[u8; 32]) -> DerivedNonce {
|
||||||
|
let code = random_bytes_32();
|
||||||
|
let binder_sn = derive_nonce_secret(share_secret, &code, DOMAIN_BINDER);
|
||||||
|
let hidden_sn = derive_nonce_secret(share_secret, &code, DOMAIN_HIDDEN);
|
||||||
|
let binder_pn = get_pubkey(&binder_sn);
|
||||||
|
let hidden_pn = get_pubkey(&hidden_sn);
|
||||||
|
DerivedNonce {
|
||||||
|
binder_pn,
|
||||||
|
hidden_pn,
|
||||||
|
code,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate `count` nonce pairs from a share secret.
|
||||||
|
pub fn generate_nonce_pairs(share_secret: &[u8; 32], count: usize) -> Vec<DerivedNonce> {
|
||||||
|
(0..count)
|
||||||
|
.map(|_| generate_nonce_pair(share_secret))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Re-derive the secret nonce pair from a code and share secret.
|
||||||
|
///
|
||||||
|
/// Call this during signing when you need the secret values but only
|
||||||
|
/// stored the code.
|
||||||
|
pub fn derive_secret_nonce(share_secret: &[u8; 32], code: &[u8; 32]) -> SecretNoncePair {
|
||||||
|
let binder_sn = derive_nonce_secret(share_secret, code, DOMAIN_BINDER);
|
||||||
|
let hidden_sn = derive_nonce_secret(share_secret, code, DOMAIN_HIDDEN);
|
||||||
|
SecretNoncePair {
|
||||||
|
code: *code,
|
||||||
|
binder_sn,
|
||||||
|
hidden_sn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify that a code produces the expected public nonces.
|
||||||
|
///
|
||||||
|
/// Use this to authenticate a code sent back by a peer during signing.
|
||||||
|
pub fn verify_nonce_code(share_secret: &[u8; 32], nonce: &MemberNonce) -> bool {
|
||||||
|
let derived = derive_secret_nonce(share_secret, &nonce.code);
|
||||||
|
let binder_pn = get_pubkey(&derived.binder_sn);
|
||||||
|
let hidden_pn = get_pubkey(&derived.hidden_sn);
|
||||||
|
// Constant-time comparison
|
||||||
|
binder_pn == nonce.binder_pn && hidden_pn == nonce.hidden_pn
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attach a member index to a `DerivedNonce`, producing a `MemberNonce`.
|
||||||
|
pub fn to_member_nonce(nonce: DerivedNonce, idx: u32) -> MemberNonce {
|
||||||
|
MemberNonce {
|
||||||
|
idx,
|
||||||
|
binder_pn: nonce.binder_pn,
|
||||||
|
hidden_pn: nonce.hidden_pn,
|
||||||
|
code: nonce.code,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate that a `DerivedNonce` contains well-formed curve points.
|
||||||
|
pub fn validate_nonce(nonce: &DerivedNonce) -> bool {
|
||||||
|
use crate::ecc::util::lift_x;
|
||||||
|
lift_x(&nonce.binder_pn).is_ok() && lift_x(&nonce.hidden_pn).is_ok()
|
||||||
|
}
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
/// High-level signing session management and partial signature operations.
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
use crate::context::get_group_signing_ctx;
|
||||||
|
use crate::sign::{
|
||||||
|
combine_partial_sigs as low_combine, sign_msg, verify_final_sig,
|
||||||
|
verify_partial_sig as low_verify,
|
||||||
|
};
|
||||||
|
use crate::types::{
|
||||||
|
PublicNonce as LowPublicNonce, SecretNonce as LowSecretNonce, SecretShare as LowSecretShare,
|
||||||
|
ShareSignature,
|
||||||
|
};
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
|
use super::dealer::get_group_id;
|
||||||
|
use super::types::{
|
||||||
|
GroupPackage, MemberNonce, PartialSig, PartialSigPackage, SecretNoncePair, SharePackage,
|
||||||
|
SignSession, Signature,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Create a signing session for a set of messages and participating members.
|
||||||
|
///
|
||||||
|
/// `messages` is a list of `(message_bytes, tweaks)` pairs. Tweaks are
|
||||||
|
/// applied to the group key before signing (e.g. for BIP-32 derivation).
|
||||||
|
/// `nonces` must contain one `MemberNonce` per participating member.
|
||||||
|
///
|
||||||
|
/// The session ID is deterministically derived from the group ID, members,
|
||||||
|
/// and messages, making it safe to compare sessions across participants.
|
||||||
|
pub fn create_sign_session(
|
||||||
|
group: &GroupPackage,
|
||||||
|
members: Vec<u32>,
|
||||||
|
messages: Vec<(Vec<u8>, Vec<[u8; 32]>)>,
|
||||||
|
nonces: Vec<MemberNonce>,
|
||||||
|
) -> Result<SignSession, Error> {
|
||||||
|
if nonces.len() != members.len() {
|
||||||
|
return Err(Error::Assertion(format!(
|
||||||
|
"nonce count ({}) must equal member count ({})",
|
||||||
|
nonces.len(),
|
||||||
|
members.len()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut sorted_members = members;
|
||||||
|
sorted_members.sort();
|
||||||
|
|
||||||
|
let sid = compute_session_id(group, &sorted_members, &messages);
|
||||||
|
|
||||||
|
Ok(SignSession {
|
||||||
|
sid,
|
||||||
|
group_pk: group.group_pk,
|
||||||
|
members: sorted_members,
|
||||||
|
messages,
|
||||||
|
nonces,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute a deterministic session ID:
|
||||||
|
/// SHA-256(group_id || members[4B each] || messages[len+bytes+tweaks])
|
||||||
|
fn compute_session_id(
|
||||||
|
group: &GroupPackage,
|
||||||
|
members: &[u32],
|
||||||
|
messages: &[(Vec<u8>, Vec<[u8; 32]>)],
|
||||||
|
) -> [u8; 32] {
|
||||||
|
let gid = get_group_id(group);
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(gid);
|
||||||
|
for &m in members {
|
||||||
|
hasher.update(m.to_be_bytes());
|
||||||
|
}
|
||||||
|
for (msg, tweaks) in messages {
|
||||||
|
hasher.update((msg.len() as u32).to_be_bytes());
|
||||||
|
hasher.update(msg);
|
||||||
|
for t in tweaks {
|
||||||
|
hasher.update(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hasher.finalize().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Produce a partial signature package for all messages in a session.
|
||||||
|
///
|
||||||
|
/// The caller provides the `SecretNoncePair` for their slot in the session
|
||||||
|
/// (re-derived from the stored code via [`crate::frost::nonce::derive_secret_nonce`]).
|
||||||
|
pub fn create_partial_sig_package(
|
||||||
|
session: &SignSession,
|
||||||
|
share: &SharePackage,
|
||||||
|
secret_nonce: &SecretNoncePair,
|
||||||
|
) -> Result<PartialSigPackage, Error> {
|
||||||
|
let low_share = LowSecretShare {
|
||||||
|
idx: share.idx,
|
||||||
|
seckey: share.seckey,
|
||||||
|
};
|
||||||
|
let low_snonce = LowSecretNonce {
|
||||||
|
idx: share.idx,
|
||||||
|
binder_sn: secret_nonce.binder_sn,
|
||||||
|
hidden_sn: secret_nonce.hidden_sn,
|
||||||
|
};
|
||||||
|
|
||||||
|
let pnonces = build_public_nonces(&session.nonces);
|
||||||
|
let mut psigs = Vec::with_capacity(session.messages.len());
|
||||||
|
|
||||||
|
for (message, tweaks) in &session.messages {
|
||||||
|
let ctx = get_group_signing_ctx(&session.group_pk, &pnonces, message, tweaks)?;
|
||||||
|
let sig = sign_msg(&ctx, &low_share, &low_snonce)?;
|
||||||
|
psigs.push(PartialSig {
|
||||||
|
message: message.clone(),
|
||||||
|
psig: sig.psig,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let pubkey = crate::helpers::get_pubkey(&share.seckey);
|
||||||
|
|
||||||
|
Ok(PartialSigPackage {
|
||||||
|
idx: share.idx,
|
||||||
|
pubkey,
|
||||||
|
sid: session.sid,
|
||||||
|
psigs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify a partial signature package from one member.
|
||||||
|
///
|
||||||
|
/// Returns `Ok(None)` if valid, or `Ok(Some(reason))` if invalid.
|
||||||
|
pub fn verify_partial_sig_package(
|
||||||
|
session: &SignSession,
|
||||||
|
group: &GroupPackage,
|
||||||
|
pkg: &PartialSigPackage,
|
||||||
|
) -> Result<Option<String>, Error> {
|
||||||
|
if pkg.sid != session.sid {
|
||||||
|
return Ok(Some("session id mismatch".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let member_pubkeys: Vec<[u8; 33]> = group.members.iter().map(|m| m.pubkey).collect();
|
||||||
|
if !member_pubkeys.contains(&pkg.pubkey) {
|
||||||
|
return Ok(Some("pubkey not found in group".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let pnonces = build_public_nonces(&session.nonces);
|
||||||
|
let pnonce = match pnonces.iter().find(|n| n.idx == pkg.idx) {
|
||||||
|
Some(n) => n,
|
||||||
|
None => return Ok(Some(format!("no nonce for member {}", pkg.idx))),
|
||||||
|
};
|
||||||
|
|
||||||
|
for (i, (message, tweaks)) in session.messages.iter().enumerate() {
|
||||||
|
let psig_entry = match pkg.psigs.get(i) {
|
||||||
|
Some(e) => e,
|
||||||
|
None => return Ok(Some(format!("missing partial sig for message {i}"))),
|
||||||
|
};
|
||||||
|
|
||||||
|
let ctx = get_group_signing_ctx(&session.group_pk, &pnonces, message, tweaks)?;
|
||||||
|
|
||||||
|
if !low_verify(&ctx, pnonce, &pkg.pubkey, &psig_entry.psig)? {
|
||||||
|
return Ok(Some(format!("partial sig invalid for message {i}")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Combine partial signature packages into final BIP340 signatures.
|
||||||
|
///
|
||||||
|
/// All packages must cover the same session. Returns one `Signature`
|
||||||
|
/// per message in the session. The combined signatures are verified
|
||||||
|
/// before being returned.
|
||||||
|
pub fn combine_signatures(
|
||||||
|
session: &SignSession,
|
||||||
|
group: &GroupPackage,
|
||||||
|
pkgs: &[PartialSigPackage],
|
||||||
|
) -> Result<Vec<Signature>, Error> {
|
||||||
|
if pkgs.len() < group.threshold {
|
||||||
|
return Err(Error::Assertion(format!(
|
||||||
|
"need at least {} partial sigs, got {}",
|
||||||
|
group.threshold,
|
||||||
|
pkgs.len()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let pnonces = build_public_nonces(&session.nonces);
|
||||||
|
let mut signatures = Vec::with_capacity(session.messages.len());
|
||||||
|
|
||||||
|
for (i, (message, tweaks)) in session.messages.iter().enumerate() {
|
||||||
|
let ctx = get_group_signing_ctx(&session.group_pk, &pnonces, message, tweaks)?;
|
||||||
|
|
||||||
|
let share_sigs: Vec<ShareSignature> = pkgs
|
||||||
|
.iter()
|
||||||
|
.map(|pkg| {
|
||||||
|
let psig = pkg.psigs.get(i).ok_or_else(|| {
|
||||||
|
Error::Assertion(format!(
|
||||||
|
"missing psig at index {i} in pkg from member {}",
|
||||||
|
pkg.idx
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
Ok(ShareSignature {
|
||||||
|
idx: pkg.idx,
|
||||||
|
pubkey: pkg.pubkey,
|
||||||
|
psig: psig.psig,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<_, Error>>()?;
|
||||||
|
|
||||||
|
let sig = low_combine(&ctx, &share_sigs)?;
|
||||||
|
|
||||||
|
let key_ctx = ctx.key_context();
|
||||||
|
if !verify_final_sig(&key_ctx, message, &sig)? {
|
||||||
|
return Err(Error::Assertion(format!(
|
||||||
|
"combined signature failed BIP340 verification for message {i}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
signatures.push(Signature {
|
||||||
|
message: message.clone(),
|
||||||
|
pubkey: ctx.group_pk,
|
||||||
|
sig,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(signatures)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Internal helpers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn build_public_nonces(nonces: &[MemberNonce]) -> Vec<LowPublicNonce> {
|
||||||
|
nonces
|
||||||
|
.iter()
|
||||||
|
.map(|n| LowPublicNonce {
|
||||||
|
idx: n.idx,
|
||||||
|
binder_pn: n.binder_pn,
|
||||||
|
hidden_pn: n.hidden_pn,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
@@ -0,0 +1,908 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod dealer_tests {
|
||||||
|
use crate::frost::dealer::*;
|
||||||
|
use crate::frost::types::*;
|
||||||
|
|
||||||
|
fn s32(hex: &str) -> [u8; 32] {
|
||||||
|
hex::decode(hex).unwrap().try_into().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
const S0: &str = "0070ca75929ca1ec4cd70ac34f46079bdfdd87f9d0c0bf4275f3882f7b462d0f";
|
||||||
|
const S1: &str = "0e0376a7180253cdb8b6020bff0eda529760da65e715663e2152e4be2fc7b443";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generate_dealer_package_structure() {
|
||||||
|
let pkg = generate_dealer_package(2, 3, &[]).unwrap();
|
||||||
|
assert_eq!(pkg.group.threshold, 2);
|
||||||
|
assert_eq!(pkg.group.members.len(), 3);
|
||||||
|
assert_eq!(pkg.shares.len(), 3);
|
||||||
|
for (i, share) in pkg.shares.iter().enumerate() {
|
||||||
|
assert_eq!(share.idx as usize, i + 1);
|
||||||
|
assert_eq!(pkg.group.members[i].idx, share.idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generate_dealer_package_deterministic_with_secrets() {
|
||||||
|
let secrets = [s32(S0), s32(S1)];
|
||||||
|
let a = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||||
|
let b = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||||
|
assert_eq!(a.group.group_pk, b.group.group_pk);
|
||||||
|
for (sa, sb) in a.shares.iter().zip(b.shares.iter()) {
|
||||||
|
assert_eq!(sa.seckey, sb.seckey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generate_dealer_package_random_without_secrets() {
|
||||||
|
let a = generate_dealer_package(2, 3, &[]).unwrap();
|
||||||
|
let b = generate_dealer_package(2, 3, &[]).unwrap();
|
||||||
|
assert_ne!(a.group.group_pk, b.group.group_pk);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generate_dealer_package_member_pubkeys_match_shares() {
|
||||||
|
let secrets = [s32(S0), s32(S1)];
|
||||||
|
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||||
|
for share in &pkg.shares {
|
||||||
|
let expected_pk = crate::helpers::get_pubkey(&share.seckey);
|
||||||
|
let member = pkg
|
||||||
|
.group
|
||||||
|
.members
|
||||||
|
.iter()
|
||||||
|
.find(|m| m.idx == share.idx)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(member.pubkey, expected_pk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generate_dealer_package_matches_fixture() {
|
||||||
|
let secrets = [s32(S0), s32(S1)];
|
||||||
|
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
hex::encode(pkg.group.group_pk),
|
||||||
|
"021ae63bc9ddaffe52d44c3018e83115bfb22195bd8112fcad112310714e6fd5ec"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
hex::encode(pkg.shares[0].seckey),
|
||||||
|
"0e74411caa9ef5ba058d0ccf4e54e1ee773e625fb7d6258097466cedab0de152"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_group_id_is_deterministic() {
|
||||||
|
let secrets = [s32(S0), s32(S1)];
|
||||||
|
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||||
|
let id1 = get_group_id(&pkg.group);
|
||||||
|
let id2 = get_group_id(&pkg.group);
|
||||||
|
assert_eq!(id1, id2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_group_id_differs_for_different_groups() {
|
||||||
|
let a = generate_dealer_package(2, 3, &[]).unwrap();
|
||||||
|
let b = generate_dealer_package(2, 3, &[]).unwrap();
|
||||||
|
assert_ne!(get_group_id(&a.group), get_group_id(&b.group));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_group_id_differs_for_different_thresholds() {
|
||||||
|
let secrets = [s32(S0), s32(S1)];
|
||||||
|
let a = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||||
|
// Same secrets but different threshold → different group
|
||||||
|
let b = generate_dealer_package(3, 3, &secrets).unwrap();
|
||||||
|
assert_ne!(get_group_id(&a.group), get_group_id(&b.group));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_group_member_valid_shares() {
|
||||||
|
let secrets = [s32(S0), s32(S1)];
|
||||||
|
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||||
|
for share in &pkg.shares {
|
||||||
|
assert!(is_group_member(&pkg.group, share));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_group_member_wrong_seckey_fails() {
|
||||||
|
let secrets = [s32(S0), s32(S1)];
|
||||||
|
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||||
|
let tampered = SharePackage {
|
||||||
|
idx: pkg.shares[0].idx,
|
||||||
|
seckey: s32("1111111111111111111111111111111111111111111111111111111111111111"),
|
||||||
|
};
|
||||||
|
assert!(!is_group_member(&pkg.group, &tampered));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_group_member_wrong_idx_fails() {
|
||||||
|
let secrets = [s32(S0), s32(S1)];
|
||||||
|
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||||
|
let tampered = SharePackage {
|
||||||
|
idx: 99,
|
||||||
|
seckey: pkg.shares[0].seckey,
|
||||||
|
};
|
||||||
|
assert!(!is_group_member(&pkg.group, &tampered));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify_dealer_package_valid() {
|
||||||
|
let secrets = [s32(S0), s32(S1)];
|
||||||
|
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||||
|
assert!(verify_dealer_package(&pkg).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify_dealer_package_tampered_share_fails() {
|
||||||
|
let secrets = [s32(S0), s32(S1)];
|
||||||
|
let mut pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||||
|
pkg.shares[0].seckey[0] ^= 0xff;
|
||||||
|
assert!(!verify_dealer_package(&pkg).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_member_by_idx_found() {
|
||||||
|
let secrets = [s32(S0), s32(S1)];
|
||||||
|
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||||
|
let m = get_member_by_idx(&pkg.group, 2).unwrap();
|
||||||
|
assert_eq!(m.idx, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_member_by_idx_not_found() {
|
||||||
|
let secrets = [s32(S0), s32(S1)];
|
||||||
|
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||||
|
assert!(get_member_by_idx(&pkg.group, 99).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_member_by_pubkey_found() {
|
||||||
|
let secrets = [s32(S0), s32(S1)];
|
||||||
|
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||||
|
let pk = pkg.group.members[1].pubkey;
|
||||||
|
let m = get_member_by_pubkey(&pkg.group, &pk).unwrap();
|
||||||
|
assert_eq!(m.idx, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod nonce_tests {
|
||||||
|
use crate::frost::nonce::*;
|
||||||
|
use crate::frost::types::*;
|
||||||
|
|
||||||
|
fn s32(hex: &str) -> [u8; 32] {
|
||||||
|
hex::decode(hex).unwrap().try_into().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
const SECRET: &str = "0e74411caa9ef5ba058d0ccf4e54e1ee773e625fb7d6258097466cedab0de152";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generate_nonce_pair_produces_valid_points() {
|
||||||
|
let secret = s32(SECRET);
|
||||||
|
let nonce = generate_nonce_pair(&secret);
|
||||||
|
assert!(validate_nonce(&nonce));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generate_nonce_pair_is_random() {
|
||||||
|
let secret = s32(SECRET);
|
||||||
|
let a = generate_nonce_pair(&secret);
|
||||||
|
let b = generate_nonce_pair(&secret);
|
||||||
|
// Different codes → different nonces
|
||||||
|
assert_ne!(a.code, b.code);
|
||||||
|
assert_ne!(a.binder_pn, b.binder_pn);
|
||||||
|
assert_ne!(a.hidden_pn, b.hidden_pn);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generate_nonce_pairs_count() {
|
||||||
|
let secret = s32(SECRET);
|
||||||
|
let pairs = generate_nonce_pairs(&secret, 5);
|
||||||
|
assert_eq!(pairs.len(), 5);
|
||||||
|
// All codes should be unique
|
||||||
|
let codes: std::collections::HashSet<_> = pairs.iter().map(|p| p.code).collect();
|
||||||
|
assert_eq!(codes.len(), 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn derive_secret_nonce_roundtrip() {
|
||||||
|
let secret = s32(SECRET);
|
||||||
|
let nonce = generate_nonce_pair(&secret);
|
||||||
|
let derived = derive_secret_nonce(&secret, &nonce.code);
|
||||||
|
// Re-derived public nonces must match the originals
|
||||||
|
let binder_pn = crate::helpers::get_pubkey(&derived.binder_sn);
|
||||||
|
let hidden_pn = crate::helpers::get_pubkey(&derived.hidden_sn);
|
||||||
|
assert_eq!(binder_pn, nonce.binder_pn);
|
||||||
|
assert_eq!(hidden_pn, nonce.hidden_pn);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn derive_secret_nonce_deterministic() {
|
||||||
|
let secret = s32(SECRET);
|
||||||
|
let code = [0x42u8; 32];
|
||||||
|
let a = derive_secret_nonce(&secret, &code);
|
||||||
|
let b = derive_secret_nonce(&secret, &code);
|
||||||
|
assert_eq!(a.binder_sn, b.binder_sn);
|
||||||
|
assert_eq!(a.hidden_sn, b.hidden_sn);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn derive_secret_nonce_different_codes_differ() {
|
||||||
|
let secret = s32(SECRET);
|
||||||
|
let code_a = [0x01u8; 32];
|
||||||
|
let code_b = [0x02u8; 32];
|
||||||
|
let a = derive_secret_nonce(&secret, &code_a);
|
||||||
|
let b = derive_secret_nonce(&secret, &code_b);
|
||||||
|
assert_ne!(a.binder_sn, b.binder_sn);
|
||||||
|
assert_ne!(a.hidden_sn, b.hidden_sn);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn derive_secret_nonce_different_secrets_differ() {
|
||||||
|
let secret_a = s32(SECRET);
|
||||||
|
let secret_b = s32("1c77b7c3c2a14987be430edb4d63bc410e9f3cc59eeb8bbeb89951abdad59595");
|
||||||
|
let code = [0x42u8; 32];
|
||||||
|
let a = derive_secret_nonce(&secret_a, &code);
|
||||||
|
let b = derive_secret_nonce(&secret_b, &code);
|
||||||
|
assert_ne!(a.binder_sn, b.binder_sn);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify_nonce_code_valid() {
|
||||||
|
let secret = s32(SECRET);
|
||||||
|
let derived = generate_nonce_pair(&secret);
|
||||||
|
let member_nonce = to_member_nonce(derived, 1);
|
||||||
|
assert!(verify_nonce_code(&secret, &member_nonce));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify_nonce_code_wrong_secret_fails() {
|
||||||
|
let secret = s32(SECRET);
|
||||||
|
let wrong_secret = s32("1c77b7c3c2a14987be430edb4d63bc410e9f3cc59eeb8bbeb89951abdad59595");
|
||||||
|
let derived = generate_nonce_pair(&secret);
|
||||||
|
let member_nonce = to_member_nonce(derived, 1);
|
||||||
|
assert!(!verify_nonce_code(&wrong_secret, &member_nonce));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify_nonce_code_tampered_code_fails() {
|
||||||
|
let secret = s32(SECRET);
|
||||||
|
let derived = generate_nonce_pair(&secret);
|
||||||
|
let mut member_nonce = to_member_nonce(derived, 1);
|
||||||
|
member_nonce.code[0] ^= 0xff;
|
||||||
|
assert!(!verify_nonce_code(&secret, &member_nonce));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn to_member_nonce_attaches_idx() {
|
||||||
|
let secret = s32(SECRET);
|
||||||
|
let derived = generate_nonce_pair(&secret);
|
||||||
|
let binder_pn = derived.binder_pn;
|
||||||
|
let hidden_pn = derived.hidden_pn;
|
||||||
|
let code = derived.code;
|
||||||
|
let member = to_member_nonce(derived, 42);
|
||||||
|
assert_eq!(member.idx, 42);
|
||||||
|
assert_eq!(member.binder_pn, binder_pn);
|
||||||
|
assert_eq!(member.hidden_pn, hidden_pn);
|
||||||
|
assert_eq!(member.code, code);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validate_nonce_valid() {
|
||||||
|
let secret = s32(SECRET);
|
||||||
|
let nonce = generate_nonce_pair(&secret);
|
||||||
|
assert!(validate_nonce(&nonce));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn validate_nonce_invalid_point_fails() {
|
||||||
|
let bad = DerivedNonce {
|
||||||
|
binder_pn: [0u8; 33],
|
||||||
|
hidden_pn: [0u8; 33],
|
||||||
|
code: [0u8; 32],
|
||||||
|
};
|
||||||
|
assert!(!validate_nonce(&bad));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod signing_tests {
|
||||||
|
use crate::frost::dealer::generate_dealer_package;
|
||||||
|
use crate::frost::nonce::{derive_secret_nonce, generate_nonce_pair, to_member_nonce};
|
||||||
|
use crate::frost::signing::*;
|
||||||
|
use crate::frost::types::*;
|
||||||
|
|
||||||
|
fn s32(hex: &str) -> [u8; 32] {
|
||||||
|
hex::decode(hex).unwrap().try_into().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
const S0: &str = "0070ca75929ca1ec4cd70ac34f46079bdfdd87f9d0c0bf4275f3882f7b462d0f";
|
||||||
|
const S1: &str = "0e0376a7180253cdb8b6020bff0eda529760da65e715663e2152e4be2fc7b443";
|
||||||
|
|
||||||
|
/// Build a complete 2-of-3 signing setup with deterministic inputs.
|
||||||
|
fn setup_session(
|
||||||
|
message: &[u8],
|
||||||
|
tweaks: Vec<[u8; 32]>,
|
||||||
|
) -> (
|
||||||
|
crate::frost::types::DealerPackage,
|
||||||
|
SignSession,
|
||||||
|
Vec<SecretNoncePair>,
|
||||||
|
) {
|
||||||
|
let secrets = [s32(S0), s32(S1)];
|
||||||
|
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||||
|
|
||||||
|
// Participants 1 and 2 sign
|
||||||
|
let signing_shares = &pkg.shares[..2];
|
||||||
|
|
||||||
|
// Each generates a nonce pair
|
||||||
|
let nonce_pairs: Vec<_> = signing_shares
|
||||||
|
.iter()
|
||||||
|
.map(|s| generate_nonce_pair(&s.seckey))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let member_nonces: Vec<MemberNonce> = signing_shares
|
||||||
|
.iter()
|
||||||
|
.zip(nonce_pairs.iter())
|
||||||
|
.map(|(s, n)| to_member_nonce(n.clone(), s.idx))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let secret_nonces: Vec<SecretNoncePair> = signing_shares
|
||||||
|
.iter()
|
||||||
|
.zip(nonce_pairs.iter())
|
||||||
|
.map(|(s, n)| derive_secret_nonce(&s.seckey, &n.code))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let members: Vec<u32> = signing_shares.iter().map(|s| s.idx).collect();
|
||||||
|
let session = create_sign_session(
|
||||||
|
&pkg.group,
|
||||||
|
members,
|
||||||
|
vec![(message.to_vec(), tweaks)],
|
||||||
|
member_nonces,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
(pkg, session, secret_nonces)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── create_sign_session ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_sign_session_sorts_members() {
|
||||||
|
let secrets = [s32(S0), s32(S1)];
|
||||||
|
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||||
|
let nonces: Vec<MemberNonce> = pkg.shares[..2]
|
||||||
|
.iter()
|
||||||
|
.map(|s| to_member_nonce(generate_nonce_pair(&s.seckey), s.idx))
|
||||||
|
.collect();
|
||||||
|
// Pass members in reverse order
|
||||||
|
let session = create_sign_session(
|
||||||
|
&pkg.group,
|
||||||
|
vec![2, 1],
|
||||||
|
vec![(b"hello".to_vec(), vec![])],
|
||||||
|
nonces,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(session.members, vec![1, 2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_sign_session_nonce_count_mismatch_errors() {
|
||||||
|
let secrets = [s32(S0), s32(S1)];
|
||||||
|
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||||
|
let nonces: Vec<MemberNonce> = pkg.shares[..1]
|
||||||
|
.iter()
|
||||||
|
.map(|s| to_member_nonce(generate_nonce_pair(&s.seckey), s.idx))
|
||||||
|
.collect();
|
||||||
|
let result = create_sign_session(
|
||||||
|
&pkg.group,
|
||||||
|
vec![1, 2], // 2 members but only 1 nonce
|
||||||
|
vec![(b"hello".to_vec(), vec![])],
|
||||||
|
nonces,
|
||||||
|
);
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_sign_session_id_is_deterministic() {
|
||||||
|
let secrets = [s32(S0), s32(S1)];
|
||||||
|
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||||
|
let msg = b"test message";
|
||||||
|
|
||||||
|
// Build two sessions with the same nonces
|
||||||
|
let nonce_pairs: Vec<_> = pkg.shares[..2]
|
||||||
|
.iter()
|
||||||
|
.map(|s| generate_nonce_pair(&s.seckey))
|
||||||
|
.collect();
|
||||||
|
let member_nonces: Vec<MemberNonce> = pkg.shares[..2]
|
||||||
|
.iter()
|
||||||
|
.zip(nonce_pairs.iter())
|
||||||
|
.map(|(s, n)| to_member_nonce(n.clone(), s.idx))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let s1 = create_sign_session(
|
||||||
|
&pkg.group,
|
||||||
|
vec![1, 2],
|
||||||
|
vec![(msg.to_vec(), vec![])],
|
||||||
|
member_nonces.clone(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let s2 = create_sign_session(
|
||||||
|
&pkg.group,
|
||||||
|
vec![1, 2],
|
||||||
|
vec![(msg.to_vec(), vec![])],
|
||||||
|
member_nonces,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(s1.sid, s2.sid);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_sign_session_id_differs_for_different_messages() {
|
||||||
|
let secrets = [s32(S0), s32(S1)];
|
||||||
|
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||||
|
let nonce_pairs: Vec<_> = pkg.shares[..2]
|
||||||
|
.iter()
|
||||||
|
.map(|s| generate_nonce_pair(&s.seckey))
|
||||||
|
.collect();
|
||||||
|
let make_nonces = || {
|
||||||
|
pkg.shares[..2]
|
||||||
|
.iter()
|
||||||
|
.zip(nonce_pairs.iter())
|
||||||
|
.map(|(s, n)| to_member_nonce(n.clone(), s.idx))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
let s1 = create_sign_session(
|
||||||
|
&pkg.group,
|
||||||
|
vec![1, 2],
|
||||||
|
vec![(b"msg1".to_vec(), vec![])],
|
||||||
|
make_nonces(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let s2 = create_sign_session(
|
||||||
|
&pkg.group,
|
||||||
|
vec![1, 2],
|
||||||
|
vec![(b"msg2".to_vec(), vec![])],
|
||||||
|
make_nonces(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_ne!(s1.sid, s2.sid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── create_partial_sig_package ────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_partial_sig_package_produces_correct_count() {
|
||||||
|
let msg = b"hello world";
|
||||||
|
let (pkg, session, secret_nonces) = setup_session(msg, vec![]);
|
||||||
|
let psig = create_partial_sig_package(&session, &pkg.shares[0], &secret_nonces[0]).unwrap();
|
||||||
|
assert_eq!(psig.psigs.len(), 1);
|
||||||
|
assert_eq!(psig.idx, 1);
|
||||||
|
assert_eq!(psig.sid, session.sid);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_partial_sig_package_multi_message() {
|
||||||
|
let secrets = [s32(S0), s32(S1)];
|
||||||
|
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||||
|
let nonce_pairs: Vec<_> = pkg.shares[..2]
|
||||||
|
.iter()
|
||||||
|
.map(|s| generate_nonce_pair(&s.seckey))
|
||||||
|
.collect();
|
||||||
|
let member_nonces: Vec<MemberNonce> = pkg.shares[..2]
|
||||||
|
.iter()
|
||||||
|
.zip(nonce_pairs.iter())
|
||||||
|
.map(|(s, n)| to_member_nonce(n.clone(), s.idx))
|
||||||
|
.collect();
|
||||||
|
let secret_nonces: Vec<SecretNoncePair> = pkg.shares[..2]
|
||||||
|
.iter()
|
||||||
|
.zip(nonce_pairs.iter())
|
||||||
|
.map(|(s, n)| derive_secret_nonce(&s.seckey, &n.code))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let messages = vec![
|
||||||
|
(b"message one".to_vec(), vec![]),
|
||||||
|
(b"message two".to_vec(), vec![]),
|
||||||
|
];
|
||||||
|
let session = create_sign_session(&pkg.group, vec![1, 2], messages, member_nonces).unwrap();
|
||||||
|
|
||||||
|
let psig = create_partial_sig_package(&session, &pkg.shares[0], &secret_nonces[0]).unwrap();
|
||||||
|
assert_eq!(psig.psigs.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── verify_partial_sig_package ────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify_partial_sig_package_valid() {
|
||||||
|
let msg = b"hello world";
|
||||||
|
let (pkg, session, secret_nonces) = setup_session(msg, vec![]);
|
||||||
|
let psig = create_partial_sig_package(&session, &pkg.shares[0], &secret_nonces[0]).unwrap();
|
||||||
|
let result = verify_partial_sig_package(&session, &pkg.group, &psig).unwrap();
|
||||||
|
assert!(result.is_none(), "expected valid, got: {:?}", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify_partial_sig_package_wrong_sid_fails() {
|
||||||
|
let msg = b"hello world";
|
||||||
|
let (pkg, session, secret_nonces) = setup_session(msg, vec![]);
|
||||||
|
let mut psig =
|
||||||
|
create_partial_sig_package(&session, &pkg.shares[0], &secret_nonces[0]).unwrap();
|
||||||
|
psig.sid = [0xff; 32];
|
||||||
|
let result = verify_partial_sig_package(&session, &pkg.group, &psig).unwrap();
|
||||||
|
assert!(result.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify_partial_sig_package_wrong_pubkey_fails() {
|
||||||
|
let msg = b"hello world";
|
||||||
|
let (pkg, session, secret_nonces) = setup_session(msg, vec![]);
|
||||||
|
let mut psig =
|
||||||
|
create_partial_sig_package(&session, &pkg.shares[0], &secret_nonces[0]).unwrap();
|
||||||
|
psig.pubkey = [0x02; 33]; // not in group
|
||||||
|
let result = verify_partial_sig_package(&session, &pkg.group, &psig).unwrap();
|
||||||
|
assert!(result.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verify_partial_sig_package_tampered_psig_fails() {
|
||||||
|
let msg = b"hello world";
|
||||||
|
let (pkg, session, secret_nonces) = setup_session(msg, vec![]);
|
||||||
|
let mut psig =
|
||||||
|
create_partial_sig_package(&session, &pkg.shares[0], &secret_nonces[0]).unwrap();
|
||||||
|
psig.psigs[0].psig[0] ^= 0xff;
|
||||||
|
let result = verify_partial_sig_package(&session, &pkg.group, &psig).unwrap();
|
||||||
|
assert!(result.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── combine_signatures ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn combine_signatures_produces_valid_bip340() {
|
||||||
|
let msg = b"hello world";
|
||||||
|
let (pkg, session, secret_nonces) = setup_session(msg, vec![]);
|
||||||
|
|
||||||
|
let psig1 =
|
||||||
|
create_partial_sig_package(&session, &pkg.shares[0], &secret_nonces[0]).unwrap();
|
||||||
|
let psig2 =
|
||||||
|
create_partial_sig_package(&session, &pkg.shares[1], &secret_nonces[1]).unwrap();
|
||||||
|
|
||||||
|
let sigs = combine_signatures(&session, &pkg.group, &[psig1, psig2]).unwrap();
|
||||||
|
assert_eq!(sigs.len(), 1);
|
||||||
|
assert_eq!(sigs[0].message, msg);
|
||||||
|
assert_eq!(sigs[0].sig.len(), 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn combine_signatures_with_tweaks() {
|
||||||
|
let msg = b"tweaked message";
|
||||||
|
let tweak = s32("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
||||||
|
let (pkg, session, secret_nonces) = setup_session(msg, vec![tweak]);
|
||||||
|
|
||||||
|
let psig1 =
|
||||||
|
create_partial_sig_package(&session, &pkg.shares[0], &secret_nonces[0]).unwrap();
|
||||||
|
let psig2 =
|
||||||
|
create_partial_sig_package(&session, &pkg.shares[1], &secret_nonces[1]).unwrap();
|
||||||
|
|
||||||
|
let sigs = combine_signatures(&session, &pkg.group, &[psig1, psig2]).unwrap();
|
||||||
|
assert_eq!(sigs.len(), 1);
|
||||||
|
// The pubkey in the signature should be the tweaked group key
|
||||||
|
assert_ne!(sigs[0].pubkey, pkg.group.group_pk);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn combine_signatures_multi_message() {
|
||||||
|
let secrets = [s32(S0), s32(S1)];
|
||||||
|
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||||
|
let nonce_pairs: Vec<_> = pkg.shares[..2]
|
||||||
|
.iter()
|
||||||
|
.map(|s| generate_nonce_pair(&s.seckey))
|
||||||
|
.collect();
|
||||||
|
let member_nonces: Vec<MemberNonce> = pkg.shares[..2]
|
||||||
|
.iter()
|
||||||
|
.zip(nonce_pairs.iter())
|
||||||
|
.map(|(s, n)| to_member_nonce(n.clone(), s.idx))
|
||||||
|
.collect();
|
||||||
|
let secret_nonces: Vec<SecretNoncePair> = pkg.shares[..2]
|
||||||
|
.iter()
|
||||||
|
.zip(nonce_pairs.iter())
|
||||||
|
.map(|(s, n)| derive_secret_nonce(&s.seckey, &n.code))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let messages = vec![
|
||||||
|
(b"first message".to_vec(), vec![]),
|
||||||
|
(b"second message".to_vec(), vec![]),
|
||||||
|
];
|
||||||
|
let session =
|
||||||
|
create_sign_session(&pkg.group, vec![1, 2], messages.clone(), member_nonces).unwrap();
|
||||||
|
|
||||||
|
let psig1 =
|
||||||
|
create_partial_sig_package(&session, &pkg.shares[0], &secret_nonces[0]).unwrap();
|
||||||
|
let psig2 =
|
||||||
|
create_partial_sig_package(&session, &pkg.shares[1], &secret_nonces[1]).unwrap();
|
||||||
|
|
||||||
|
let sigs = combine_signatures(&session, &pkg.group, &[psig1, psig2]).unwrap();
|
||||||
|
assert_eq!(sigs.len(), 2);
|
||||||
|
assert_eq!(sigs[0].message, messages[0].0);
|
||||||
|
assert_eq!(sigs[1].message, messages[1].0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn combine_signatures_below_threshold_errors() {
|
||||||
|
let msg = b"hello world";
|
||||||
|
let (pkg, session, secret_nonces) = setup_session(msg, vec![]);
|
||||||
|
let psig1 =
|
||||||
|
create_partial_sig_package(&session, &pkg.shares[0], &secret_nonces[0]).unwrap();
|
||||||
|
// Only 1 partial sig for a 2-of-3 group
|
||||||
|
let result = combine_signatures(&session, &pkg.group, &[psig1]);
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn full_signing_flow_matches_fixture() {
|
||||||
|
// Use the same deterministic inputs as the integration test fixture
|
||||||
|
let secrets = [s32(S0), s32(S1)];
|
||||||
|
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||||
|
|
||||||
|
let hidden_seed = s32("0070ca75929ca1ec4cd70ac34f46079bdfdd87f9d0c0bf4275f3882f7b462d0f");
|
||||||
|
let binder_seed = s32("0e0376a7180253cdb8b6020bff0eda529760da65e715663e2152e4be2fc7b443");
|
||||||
|
|
||||||
|
// Build nonces using the low-level generate_nonce to match fixture
|
||||||
|
let hidden_sn1 = crate::helpers::generate_nonce(&pkg.shares[0].seckey, Some(&hidden_seed));
|
||||||
|
let binder_sn1 = crate::helpers::generate_nonce(&pkg.shares[0].seckey, Some(&binder_seed));
|
||||||
|
let hidden_sn2 = crate::helpers::generate_nonce(&pkg.shares[1].seckey, Some(&hidden_seed));
|
||||||
|
let binder_sn2 = crate::helpers::generate_nonce(&pkg.shares[1].seckey, Some(&binder_seed));
|
||||||
|
|
||||||
|
let member_nonces = vec![
|
||||||
|
MemberNonce {
|
||||||
|
idx: 1,
|
||||||
|
hidden_pn: crate::helpers::get_pubkey(&hidden_sn1),
|
||||||
|
binder_pn: crate::helpers::get_pubkey(&binder_sn1),
|
||||||
|
code: [0u8; 32], // code unused in this path
|
||||||
|
},
|
||||||
|
MemberNonce {
|
||||||
|
idx: 2,
|
||||||
|
hidden_pn: crate::helpers::get_pubkey(&hidden_sn2),
|
||||||
|
binder_pn: crate::helpers::get_pubkey(&binder_sn2),
|
||||||
|
code: [0u8; 32],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let tweaks = vec![
|
||||||
|
s32("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
|
||||||
|
s32("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"),
|
||||||
|
];
|
||||||
|
let message = hex::decode("68656c6c6f20776f726c6421").unwrap();
|
||||||
|
|
||||||
|
let session = create_sign_session(
|
||||||
|
&pkg.group,
|
||||||
|
vec![1, 2],
|
||||||
|
vec![(message.clone(), tweaks)],
|
||||||
|
member_nonces,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let secret_nonce1 = SecretNoncePair {
|
||||||
|
code: [0u8; 32],
|
||||||
|
hidden_sn: hidden_sn1,
|
||||||
|
binder_sn: binder_sn1,
|
||||||
|
};
|
||||||
|
let secret_nonce2 = SecretNoncePair {
|
||||||
|
code: [0u8; 32],
|
||||||
|
hidden_sn: hidden_sn2,
|
||||||
|
binder_sn: binder_sn2,
|
||||||
|
};
|
||||||
|
|
||||||
|
let psig1 = create_partial_sig_package(&session, &pkg.shares[0], &secret_nonce1).unwrap();
|
||||||
|
let psig2 = create_partial_sig_package(&session, &pkg.shares[1], &secret_nonce2).unwrap();
|
||||||
|
|
||||||
|
// Verify each partial sig
|
||||||
|
assert!(verify_partial_sig_package(&session, &pkg.group, &psig1)
|
||||||
|
.unwrap()
|
||||||
|
.is_none());
|
||||||
|
assert!(verify_partial_sig_package(&session, &pkg.group, &psig2)
|
||||||
|
.unwrap()
|
||||||
|
.is_none());
|
||||||
|
|
||||||
|
// Combine and verify final signature
|
||||||
|
let sigs = combine_signatures(&session, &pkg.group, &[psig1, psig2]).unwrap();
|
||||||
|
assert_eq!(sigs.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
hex::encode(sigs[0].sig),
|
||||||
|
"e76328e49c27c12392a117d39ef9f5def368590d5e72438907fb63c1006fd5891d715fa750b5840610aaf531949f633c4555ac20caf290c3f22cc0771f074447"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod ecdh_tests {
|
||||||
|
use crate::frost::dealer::generate_dealer_package;
|
||||||
|
use crate::frost::ecdh::*;
|
||||||
|
use crate::frost::types::*;
|
||||||
|
|
||||||
|
fn s32(hex: &str) -> [u8; 32] {
|
||||||
|
hex::decode(hex).unwrap().try_into().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn p33(hex: &str) -> [u8; 33] {
|
||||||
|
hex::decode(hex).unwrap().try_into().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
const S0: &str = "0070ca75929ca1ec4cd70ac34f46079bdfdd87f9d0c0bf4275f3882f7b462d0f";
|
||||||
|
const S1: &str = "0e0376a7180253cdb8b6020bff0eda529760da65e715663e2152e4be2fc7b443";
|
||||||
|
|
||||||
|
fn demo_keypair() -> ([u8; 32], [u8; 33]) {
|
||||||
|
let aux = s32("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
||||||
|
let sk = crate::helpers::generate_seckey(Some(&aux));
|
||||||
|
let pk = crate::helpers::get_pubkey(&sk);
|
||||||
|
(sk, pk)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── create_ecdh_pkg ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_ecdh_pkg_structure() {
|
||||||
|
let secrets = [s32(S0), s32(S1)];
|
||||||
|
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||||
|
let (_, demo_pk) = demo_keypair();
|
||||||
|
let members = [1u32, 3u32];
|
||||||
|
|
||||||
|
let ecdh = create_ecdh_pkg(&members, &demo_pk, &pkg.shares[0]).unwrap();
|
||||||
|
assert_eq!(ecdh.idx, 1);
|
||||||
|
assert_eq!(ecdh.members, members);
|
||||||
|
assert_eq!(ecdh.entries.len(), 1);
|
||||||
|
assert_eq!(ecdh.entries[0].ecdh_pk, demo_pk);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_ecdh_pkg_matches_fixture() {
|
||||||
|
let secrets = [s32(S0), s32(S1)];
|
||||||
|
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||||
|
let (_, demo_pk) = demo_keypair();
|
||||||
|
let members = [1u32, 3u32];
|
||||||
|
|
||||||
|
let ecdh1 = create_ecdh_pkg(&members, &demo_pk, &pkg.shares[0]).unwrap();
|
||||||
|
let ecdh3 = create_ecdh_pkg(&members, &demo_pk, &pkg.shares[2]).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
hex::encode(ecdh1.entries[0].keyshare),
|
||||||
|
"0386c5b0f4bace78ef17d02b09e339b5a39f659dbbf1f3f531b9825df6836cfea9"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
hex::encode(ecdh3.entries[0].keyshare),
|
||||||
|
"023edaf055945d35006e1c52dd7a388e0c10b36eb55aa9d117853af87903cb54c0"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── create_batched_ecdh_pkg ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_batched_ecdh_pkg_multiple_keys() {
|
||||||
|
let secrets = [s32(S0), s32(S1)];
|
||||||
|
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||||
|
let (_, demo_pk1) = demo_keypair();
|
||||||
|
// Second key: use share[1]'s pubkey as a target
|
||||||
|
let demo_pk2 = pkg.group.members[1].pubkey;
|
||||||
|
let members = [1u32, 3u32];
|
||||||
|
|
||||||
|
let ecdh =
|
||||||
|
create_batched_ecdh_pkg(&members, &[demo_pk1, demo_pk2], &pkg.shares[0]).unwrap();
|
||||||
|
assert_eq!(ecdh.entries.len(), 2);
|
||||||
|
assert_eq!(ecdh.entries[0].ecdh_pk, demo_pk1);
|
||||||
|
assert_eq!(ecdh.entries[1].ecdh_pk, demo_pk2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_batched_ecdh_pkg_empty_keys() {
|
||||||
|
let secrets = [s32(S0), s32(S1)];
|
||||||
|
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||||
|
let members = [1u32, 3u32];
|
||||||
|
let ecdh = create_batched_ecdh_pkg(&members, &[], &pkg.shares[0]).unwrap();
|
||||||
|
assert!(ecdh.entries.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── combine_ecdh_pkgs ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn combine_ecdh_pkgs_matches_master_secret() {
|
||||||
|
let secrets = [s32(S0), s32(S1)];
|
||||||
|
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||||
|
let (demo_sk, demo_pk) = demo_keypair();
|
||||||
|
let members = [1u32, 3u32];
|
||||||
|
|
||||||
|
let ecdh1 = create_ecdh_pkg(&members, &demo_pk, &pkg.shares[0]).unwrap();
|
||||||
|
let ecdh3 = create_ecdh_pkg(&members, &demo_pk, &pkg.shares[2]).unwrap();
|
||||||
|
|
||||||
|
let frost_secret = combine_ecdh_pkgs(&[ecdh1, ecdh3], &demo_pk).unwrap();
|
||||||
|
|
||||||
|
// master_shared_secret = group_pk * demo_sk = tweak_pubkey(demo_pk, group_secret)
|
||||||
|
let master_secret = crate::helpers::tweak_pubkey(&demo_pk, &s32(S0)).unwrap();
|
||||||
|
assert_eq!(frost_secret, master_secret);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn combine_ecdh_pkgs_matches_fixture() {
|
||||||
|
let ecdh1 = EcdhPackage {
|
||||||
|
idx: 1,
|
||||||
|
members: vec![1, 3],
|
||||||
|
entries: vec![EcdhEntry {
|
||||||
|
ecdh_pk: p33("02ebd8227a6d7a03a98a1f86271d0687a6b6570187c37b39d21158a7d7835ba450"),
|
||||||
|
keyshare: p33("0386c5b0f4bace78ef17d02b09e339b5a39f659dbbf1f3f531b9825df6836cfea9"),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
let ecdh3 = EcdhPackage {
|
||||||
|
idx: 3,
|
||||||
|
members: vec![1, 3],
|
||||||
|
entries: vec![EcdhEntry {
|
||||||
|
ecdh_pk: p33("02ebd8227a6d7a03a98a1f86271d0687a6b6570187c37b39d21158a7d7835ba450"),
|
||||||
|
keyshare: p33("023edaf055945d35006e1c52dd7a388e0c10b36eb55aa9d117853af87903cb54c0"),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
let demo_pk = p33("02ebd8227a6d7a03a98a1f86271d0687a6b6570187c37b39d21158a7d7835ba450");
|
||||||
|
let secret = combine_ecdh_pkgs(&[ecdh1, ecdh3], &demo_pk).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
hex::encode(secret),
|
||||||
|
"020b6417cef5530ed4b82681945d4565ea7027f423a97b60247d07386ca3619585"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn combine_ecdh_pkgs_missing_entry_errors() {
|
||||||
|
let secrets = [s32(S0), s32(S1)];
|
||||||
|
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||||
|
let (_, demo_pk) = demo_keypair();
|
||||||
|
let wrong_pk = pkg.group.members[0].pubkey; // different key
|
||||||
|
let members = [1u32, 3u32];
|
||||||
|
|
||||||
|
let ecdh1 = create_ecdh_pkg(&members, &demo_pk, &pkg.shares[0]).unwrap();
|
||||||
|
let ecdh3 = create_ecdh_pkg(&members, &demo_pk, &pkg.shares[2]).unwrap();
|
||||||
|
|
||||||
|
// Ask for a key that's not in the packages
|
||||||
|
let result = combine_ecdh_pkgs(&[ecdh1, ecdh3], &wrong_pk);
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── combine_batched_ecdh_pkgs ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn combine_batched_ecdh_pkgs_all_keys() {
|
||||||
|
let secrets = [s32(S0), s32(S1)];
|
||||||
|
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||||
|
let (_, demo_pk1) = demo_keypair();
|
||||||
|
let demo_pk2 = pkg.group.members[1].pubkey;
|
||||||
|
let members = [1u32, 3u32];
|
||||||
|
|
||||||
|
let batch1 =
|
||||||
|
create_batched_ecdh_pkg(&members, &[demo_pk1, demo_pk2], &pkg.shares[0]).unwrap();
|
||||||
|
let batch3 =
|
||||||
|
create_batched_ecdh_pkg(&members, &[demo_pk1, demo_pk2], &pkg.shares[2]).unwrap();
|
||||||
|
|
||||||
|
let results = combine_batched_ecdh_pkgs(&[batch1, batch3]).unwrap();
|
||||||
|
assert_eq!(results.len(), 2);
|
||||||
|
assert_eq!(results[0].0, demo_pk1);
|
||||||
|
assert_eq!(results[1].0, demo_pk2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn combine_batched_ecdh_pkgs_empty_input() {
|
||||||
|
let results = combine_batched_ecdh_pkgs(&[]).unwrap();
|
||||||
|
assert!(results.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn combine_batched_matches_single_combine() {
|
||||||
|
let secrets = [s32(S0), s32(S1)];
|
||||||
|
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||||
|
let (_, demo_pk) = demo_keypair();
|
||||||
|
let members = [1u32, 3u32];
|
||||||
|
|
||||||
|
let ecdh1_single = create_ecdh_pkg(&members, &demo_pk, &pkg.shares[0]).unwrap();
|
||||||
|
let ecdh3_single = create_ecdh_pkg(&members, &demo_pk, &pkg.shares[2]).unwrap();
|
||||||
|
let single_secret = combine_ecdh_pkgs(&[ecdh1_single, ecdh3_single], &demo_pk).unwrap();
|
||||||
|
|
||||||
|
let batch1 = create_batched_ecdh_pkg(&members, &[demo_pk], &pkg.shares[0]).unwrap();
|
||||||
|
let batch3 = create_batched_ecdh_pkg(&members, &[demo_pk], &pkg.shares[2]).unwrap();
|
||||||
|
let batched = combine_batched_ecdh_pkgs(&[batch1, batch3]).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(batched[0].1, single_secret);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
/// High-level types for the FROST threshold signing protocol.
|
||||||
|
///
|
||||||
|
/// These mirror the bifrost TypeScript types, using raw byte arrays
|
||||||
|
/// rather than hex strings for efficiency.
|
||||||
|
|
||||||
|
/// A member's secret share of the group key.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct SharePackage {
|
||||||
|
/// Participant index (1-based).
|
||||||
|
pub idx: u32,
|
||||||
|
/// 32-byte secret scalar.
|
||||||
|
pub seckey: [u8; 32],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A member's public identity within a group.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct MemberPackage {
|
||||||
|
/// Participant index (1-based).
|
||||||
|
pub idx: u32,
|
||||||
|
/// 33-byte compressed public key derived from the secret share.
|
||||||
|
pub pubkey: [u8; 33],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The group's public state: group key + member roster + threshold.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct GroupPackage {
|
||||||
|
/// 33-byte compressed group public key.
|
||||||
|
pub group_pk: [u8; 33],
|
||||||
|
/// Minimum number of signers required.
|
||||||
|
pub threshold: usize,
|
||||||
|
/// All member public packages.
|
||||||
|
pub members: Vec<MemberPackage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Output of the trusted dealer: group info + all secret shares.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct DealerPackage {
|
||||||
|
pub group: GroupPackage,
|
||||||
|
pub shares: Vec<SharePackage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A public nonce commitment (no secret material).
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct PublicNonce {
|
||||||
|
/// 33-byte compressed binder nonce point.
|
||||||
|
pub binder_pn: [u8; 33],
|
||||||
|
/// 33-byte compressed hidden nonce point.
|
||||||
|
pub hidden_pn: [u8; 33],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A public nonce with a derivation code for secret re-derivation.
|
||||||
|
/// Store the code; re-derive secrets on demand during signing.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct DerivedNonce {
|
||||||
|
/// 33-byte compressed binder nonce point.
|
||||||
|
pub binder_pn: [u8; 33],
|
||||||
|
/// 33-byte compressed hidden nonce point.
|
||||||
|
pub hidden_pn: [u8; 33],
|
||||||
|
/// 32-byte random derivation code.
|
||||||
|
pub code: [u8; 32],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A derived nonce tagged with the owning member's index.
|
||||||
|
/// Used in signing wire format.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct MemberNonce {
|
||||||
|
pub idx: u32,
|
||||||
|
pub binder_pn: [u8; 33],
|
||||||
|
pub hidden_pn: [u8; 33],
|
||||||
|
pub code: [u8; 32],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Secret nonce pair re-derived from a code during signing.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct SecretNoncePair {
|
||||||
|
/// The derivation code this was derived from.
|
||||||
|
pub code: [u8; 32],
|
||||||
|
/// 32-byte secret binder nonce scalar.
|
||||||
|
pub binder_sn: [u8; 32],
|
||||||
|
/// 32-byte secret hidden nonce scalar.
|
||||||
|
pub hidden_sn: [u8; 32],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A signing session: the set of messages and participating members.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct SignSession {
|
||||||
|
/// Unique session identifier (SHA-256 of session contents).
|
||||||
|
pub sid: [u8; 32],
|
||||||
|
/// 33-byte compressed group public key (after any tweaks are applied).
|
||||||
|
pub group_pk: [u8; 33],
|
||||||
|
/// Sorted list of participating member indices.
|
||||||
|
pub members: Vec<u32>,
|
||||||
|
/// Messages to sign. Each entry is `(message_bytes, tweaks)`.
|
||||||
|
pub messages: Vec<(Vec<u8>, Vec<[u8; 32]>)>,
|
||||||
|
/// Public nonces from all participating members.
|
||||||
|
pub nonces: Vec<MemberNonce>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A partial signature produced by one member for one message.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct PartialSig {
|
||||||
|
/// The message this partial sig covers.
|
||||||
|
pub message: Vec<u8>,
|
||||||
|
/// 32-byte partial signature scalar.
|
||||||
|
pub psig: [u8; 32],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A partial signature package from one member covering all session messages.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct PartialSigPackage {
|
||||||
|
/// The member's index.
|
||||||
|
pub idx: u32,
|
||||||
|
/// The member's public key.
|
||||||
|
pub pubkey: [u8; 33],
|
||||||
|
/// Session ID this package belongs to.
|
||||||
|
pub sid: [u8; 32],
|
||||||
|
/// One partial sig per message in the session.
|
||||||
|
pub psigs: Vec<PartialSig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A completed signature for one message.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Signature {
|
||||||
|
/// The message that was signed.
|
||||||
|
pub message: Vec<u8>,
|
||||||
|
/// 33-byte compressed group public key.
|
||||||
|
pub pubkey: [u8; 33],
|
||||||
|
/// 64-byte BIP340 Schnorr signature.
|
||||||
|
pub sig: [u8; 64],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One member's ECDH keyshare for a single target public key.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct EcdhEntry {
|
||||||
|
/// The target public key this share is for.
|
||||||
|
pub ecdh_pk: [u8; 33],
|
||||||
|
/// 33-byte compressed ECDH keyshare point.
|
||||||
|
pub keyshare: [u8; 33],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An ECDH package from one member, covering one or more target keys.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct EcdhPackage {
|
||||||
|
/// The member's index.
|
||||||
|
pub idx: u32,
|
||||||
|
/// The quorum members used for Lagrange interpolation.
|
||||||
|
pub members: Vec<u32>,
|
||||||
|
/// One entry per target public key.
|
||||||
|
pub entries: Vec<EcdhEntry>,
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
pub mod ecc;
|
pub mod ecc;
|
||||||
|
pub mod frost;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user