use super::Error;
use super::key::{Key, Signature};
use super::ole;
use super::secp::{self, ProjectivePoint, Scalar};
use super::signing::{
cmp_be, ecdsa_verify, hash_to_scalar, is_high_s, lagrange_coefficient, make_sid, pad32,
};
use crate::frost::hashing::sha512_256i_tagged;
use purecrypto::rng::RngCore;
use std::sync::atomic::{AtomicBool, Ordering};
struct PartyPresign {
rho: Scalar,
sigma: Scalar, k_rho_share: Scalar, }
pub struct PresignOutput {
pub_key: ProjectivePoint,
r_point: ProjectivePoint,
r: Scalar,
parties: Vec<PartyPresign>,
consumed: AtomicBool,
}
impl PresignOutput {
pub fn consumed(&self) -> bool {
self.consumed.load(Ordering::Acquire)
}
pub fn r_hash(&self) -> [u8; 32] {
let (rx, ry) = secp::affine_be(&self.r_point);
sha512_256i_tagged(b"DKLS23-presign-rhash-v1", &[&rx, &ry])
}
}
pub fn presign(
keys: &[Key],
signer_idx: &[usize],
rng: &mut impl RngCore,
) -> Result<PresignOutput, Error> {
if keys.is_empty() {
return Err(Error::Validation(
"Presign requires at least one key".into(),
));
}
let n = keys[0].n;
let t = keys[0].t;
let pub_key = keys[0].ecdsa_pub;
if signer_idx.len() != t + 1 {
return Err(Error::Validation(format!("requires T+1={} signers", t + 1)));
}
for (a, &idx) in signer_idx.iter().enumerate() {
if idx >= n {
return Err(Error::Validation("signer index out of range".into()));
}
if signer_idx[..a].contains(&idx) {
return Err(Error::Validation("duplicate signer index".into()));
}
}
let mut signers: Vec<&Key> = signer_idx.iter().map(|&i| &keys[i]).collect();
signers.sort_by(|a, b| cmp_be(&a.party_ids[a.idx].key, &b.party_ids[b.idx].key));
let sgn = signers.len();
let ids: Vec<Scalar> = signers
.iter()
.map(|k| secp::scalar_from_be_reduce(&k.party_ids[k.idx].key))
.collect();
let mut sx: Vec<Scalar> = Vec::with_capacity(sgn);
for (i, k) in signers.iter().enumerate() {
let lam = lagrange_coefficient(&ids, i)?;
sx.push(lam.mul(&k.xi));
}
let mut nonce = [0u8; 16];
rng.fill_bytes(&mut nonce);
let mut ssid = b"DKLS23-presign-v1-".to_vec();
ssid.extend_from_slice(&nonce);
let mut k: Vec<Scalar> = Vec::with_capacity(sgn);
let mut rho: Vec<Scalar> = Vec::with_capacity(sgn);
let mut big_k: Vec<ProjectivePoint> = Vec::with_capacity(sgn);
for _ in 0..sgn {
let ki = secp::random_scalar(rng);
big_k.push(secp::mul_base(&ki));
k.push(ki);
rho.push(secp::random_scalar(rng));
}
let mut r_point = big_k[0];
for kp in &big_k[1..] {
r_point = r_point.add(kp);
}
let (rx, _) = secp::affine_be(&r_point);
let r = secp::scalar_from_be_reduce(&rx);
if bool::from(r.is_zero()) {
return Err(Error::Validation("R.x is 0 mod q; retry".into()));
}
let mut k_rho: Vec<Scalar> = (0..sgn).map(|i| k[i].mul(&rho[i])).collect();
let mut x_rho: Vec<Scalar> = (0..sgn).map(|i| sx[i].mul(&rho[i])).collect();
for ai in 0..sgn {
for bj in 0..sgn {
if ai == bj {
continue;
}
let alice = signers[ai];
let bob = signers[bj];
let alice_pair = alice.ot[bob.idx]
.as_ref()
.ok_or_else(|| Error::Validation("missing OT state".into()))?;
let bob_pair = bob.ot[alice.idx]
.as_ref()
.ok_or_else(|| Error::Validation("missing OT state".into()))?;
let sid_k = make_sid(&ssid, "presign-kxrho", alice.idx, bob.idx);
let (a_msg, a_state) = ole::alice_step1(&sid_k, &alice_pair.as_alice, &k[ai])?;
let (b_msg, u_b) = ole::bob_step1(&sid_k, &bob_pair.as_bob, &rho[bj], &a_msg)?;
let u_a = ole::alice_step2(&a_state, &b_msg)?;
k_rho[ai] = k_rho[ai].add(&u_a);
k_rho[bj] = k_rho[bj].add(&u_b);
let sid_x = make_sid(&ssid, "presign-xxrho", alice.idx, bob.idx);
let (a_msg, a_state) = ole::alice_step1(&sid_x, &alice_pair.as_alice, &sx[ai])?;
let (b_msg, u_b) = ole::bob_step1(&sid_x, &bob_pair.as_bob, &rho[bj], &a_msg)?;
let u_a = ole::alice_step2(&a_state, &b_msg)?;
x_rho[ai] = x_rho[ai].add(&u_a);
x_rho[bj] = x_rho[bj].add(&u_b);
}
}
let parties = (0..sgn)
.map(|i| PartyPresign {
rho: rho[i].clone(),
sigma: x_rho[i].clone(),
k_rho_share: k_rho[i].clone(),
})
.collect();
Ok(PresignOutput {
pub_key,
r_point,
r,
parties,
consumed: AtomicBool::new(false),
})
}
pub fn sign_with_presign(
presign: &PresignOutput,
hash: &[u8],
tweak: Option<&Scalar>,
) -> Result<Signature, Error> {
if hash.is_empty() {
return Err(Error::Validation("hash must be non-empty".into()));
}
if presign
.consumed
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_err()
{
return Err(Error::Validation(
"presign output already consumed — reuse equals nonce reuse equals key extraction"
.into(),
));
}
let mut phi = Scalar::ZERO;
for p in &presign.parties {
phi = phi.add(&p.k_rho_share);
}
if bool::from(phi.is_zero()) {
return Err(Error::Validation("φ is 0; presign was malformed".into()));
}
let e = hash_to_scalar(hash);
let mut sigma_sum = Scalar::ZERO;
for p in &presign.parties {
let mut sigma = p.sigma.clone();
if let Some(tw) = tweak {
sigma = sigma.add(&tw.mul(&p.rho));
}
let shati = p.rho.mul(&e).add(&presign.r.mul(&sigma));
sigma_sum = sigma_sum.add(&shati);
}
let mut s = sigma_sum.mul(&phi.invert());
if bool::from(s.is_zero()) {
return Err(Error::Validation("produced s = 0".into()));
}
let (_, ry) = secp::affine_be(&presign.r_point);
let mut v = ry.last().copied().unwrap_or(0) & 1;
if is_high_s(&s) {
s = s.negate();
v ^= 1;
}
let verify_pub = match tweak {
Some(tw) => presign.pub_key.add(&secp::mul_base(tw)),
None => presign.pub_key,
};
if !ecdsa_verify(&verify_pub, &e, &presign.r, &s) {
return Err(Error::Validation(
"presign finalize failed ECDSA verification".into(),
));
}
Ok(Signature {
r: pad32(&secp::scalar_to_be_min(&presign.r)),
s: pad32(&secp::scalar_to_be_min(&s)),
v,
})
}
pub trait UsedPresignStore {
fn check_and_record(&self, r_hash: &[u8; 32]) -> Result<bool, Error>;
}
#[derive(Default)]
pub struct InMemoryPresignStore {
seen: std::sync::Mutex<std::collections::HashSet<[u8; 32]>>,
}
impl InMemoryPresignStore {
pub fn new() -> Self {
Self::default()
}
}
impl UsedPresignStore for InMemoryPresignStore {
fn check_and_record(&self, r_hash: &[u8; 32]) -> Result<bool, Error> {
Ok(self.seen.lock().unwrap().insert(*r_hash))
}
}
pub fn sign_with_presign_durable(
presign: &PresignOutput,
hash: &[u8],
tweak: Option<&Scalar>,
store: &dyn UsedPresignStore,
) -> Result<Signature, Error> {
if hash.is_empty() {
return Err(Error::Validation("hash must be non-empty".into()));
}
if !store.check_and_record(&presign.r_hash())? {
return Err(Error::Validation(
"presign output already consumed — reuse equals nonce reuse equals key extraction"
.into(),
));
}
sign_with_presign(presign, hash, tweak)
}
#[cfg(test)]
mod tests {
use super::super::keygen::keygen;
use super::*;
use purecrypto::hash::sha256;
use purecrypto::rng::OsRng;
fn party_ids(n: usize) -> Vec<crate::tss::PartyId> {
crate::tss::PartyId::sort(
(1..=n)
.map(|i| crate::tss::PartyId::new(i.to_string(), format!("P{i}"), vec![i as u8]))
.collect(),
0,
)
}
#[test]
fn presign_then_sign_verifies() {
let ids = party_ids(3);
let keys = keygen(3, 1, &ids, &mut OsRng).unwrap();
let po = presign(&keys, &[0, 2], &mut OsRng).unwrap();
let msg = sha256(b"presigned message");
let sig = sign_with_presign(&po, &msg, None).unwrap();
let e = hash_to_scalar(&msg);
let r = secp::scalar_from_be_reduce(&sig.r);
let s = secp::scalar_from_be_reduce(&sig.s);
assert!(ecdsa_verify(&keys[0].ecdsa_pub, &e, &r, &s));
assert!(!is_high_s(&s));
}
#[test]
fn presign_is_single_use() {
let ids = party_ids(3);
let keys = keygen(3, 1, &ids, &mut OsRng).unwrap();
let po = presign(&keys, &[0, 1], &mut OsRng).unwrap();
let msg = sha256(b"once");
assert!(!po.consumed());
sign_with_presign(&po, &msg, None).unwrap();
assert!(po.consumed());
assert!(sign_with_presign(&po, &msg, None).is_err());
}
#[test]
fn durable_store_rejects_reuse() {
let ids = party_ids(3);
let keys = keygen(3, 1, &ids, &mut OsRng).unwrap();
let store = InMemoryPresignStore::new();
let msg = sha256(b"durable");
let po1 = presign(&keys, &[0, 1], &mut OsRng).unwrap();
sign_with_presign_durable(&po1, &msg, None, &store).unwrap();
assert!(!store.check_and_record(&po1.r_hash()).unwrap());
}
#[test]
fn presign_tweak_verifies_under_child_key() {
let ids = party_ids(3);
let keys = keygen(3, 1, &ids, &mut OsRng).unwrap();
let po = presign(&keys, &[1, 2], &mut OsRng).unwrap();
let tweak = secp::scalar_from_be_reduce(&[0x11, 0x22, 0x33]);
let msg = sha256(b"tweaked presign");
let sig = sign_with_presign(&po, &msg, Some(&tweak)).unwrap();
let child_pub = keys[0].ecdsa_pub.add(&secp::mul_base(&tweak));
let e = hash_to_scalar(&msg);
let r = secp::scalar_from_be_reduce(&sig.r);
let s = secp::scalar_from_be_reduce(&sig.s);
assert!(ecdsa_verify(&child_pub, &e, &r, &s));
}
}