use super::Error;
use super::echo::{other_parties, strip};
use super::key::Key;
use super::secp::{self, ProjectivePoint, Scalar};
use super::signing::lagrange_coefficient;
use crate::frost::hashing::sha512_256i_tagged;
use crate::tss::b64::B64Bytes;
use crate::tss::expect::JsonExpect;
use crate::tss::keyimage_hash::{digest32, validate as validate_hash};
use crate::tss::{HashAlgorithm, JsonMessage, Parameters, PartyId, json_get, json_wrap};
use purecrypto::rng::{OsRng, RngCore};
use serde::{Deserialize, Serialize};
use std::sync::mpsc::{Receiver, Sender, channel};
use std::sync::{Arc, Mutex};
const TYPE_R1: &str = "dkls:keyimage:r1";
const POINT_DOMAIN: &[u8] = b"DKLS23-keyimage-point-v1";
const SECRET_DOMAIN: &[u8] = b"DKLS23-keyimage-secret-v1";
const DLEQ_DOMAIN: &[u8] = b"DKLS23-keyimage-dleq-v1";
pub struct KeyImageSecret {
pub secret: Scalar,
pub public_key: ProjectivePoint,
pub key_image: ProjectivePoint,
}
impl KeyImageSecret {
pub fn secret_bytes(&self) -> [u8; 32] {
self.secret.to_bytes_be()
}
pub fn public_key_sec1(&self) -> [u8; 33] {
secp::to_sec1_compressed(&self.public_key).expect("child key is not the identity")
}
}
#[derive(Serialize, Deserialize)]
struct KeyImageR1 {
#[serde(rename = "W")]
w: B64Bytes,
#[serde(rename = "A1")]
a1: B64Bytes,
#[serde(rename = "A2")]
a2: B64Bytes,
#[serde(rename = "Z")]
z: B64Bytes,
}
pub struct KeyImageParty {
result_rx: Receiver<Result<KeyImageSecret, Error>>,
_shared: Arc<Shared>,
}
struct Shared {
params: Parameters,
key: Key,
identifier: Vec<u8>,
subset: Vec<PartyId>,
other_subset: Vec<PartyId>,
hash: HashAlgorithm,
point: ProjectivePoint,
my_partial: ProjectivePoint,
result_tx: Mutex<Option<Sender<Result<KeyImageSecret, Error>>>>,
}
impl KeyImageParty {
pub fn new(
params: Parameters,
key: Key,
identifier: Vec<u8>,
subset: Vec<PartyId>,
hash: HashAlgorithm,
) -> Result<KeyImageParty, Error> {
key.validate_basic()?;
if subset.len() < key.t + 1 {
return Err(Error::Validation(format!(
"subset size {}, expected at least T+1={}",
subset.len(),
key.t + 1
)));
}
validate_sorted_subset(&subset)?;
let me = params.party_id().clone();
let my_pos = subset
.iter()
.position(|p| p.cmp_key(&me) == std::cmp::Ordering::Equal)
.ok_or_else(|| Error::Validation("self not in key-image subset".into()))?;
let point = hash_to_point(&identifier, &key.ecdsa_pub, hash)?;
let ids: Vec<Scalar> = subset
.iter()
.map(|p| secp::scalar_from_be_reduce(&p.key))
.collect();
let w = lagrange_coefficient(&ids, my_pos)?.mul(&key.xi);
let partial = point.mul(&w);
let statement = secp::mul_base(&w);
let session = dleq_session(&identifier, &key.ecdsa_pub, &subset, &me.key, hash);
let proof = Dleq::prove(&session, &point, &w, &statement, &partial, &mut OsRng);
let other_subset = other_parties(&subset, &me);
let (tx, rx) = channel();
let shared = Arc::new(Shared {
params,
key,
identifier,
subset,
other_subset,
hash,
point,
my_partial: partial,
result_tx: Mutex::new(Some(tx)),
});
shared.round1(&proof)?;
Ok(KeyImageParty {
result_rx: rx,
_shared: shared,
})
}
pub fn try_result(&self) -> Option<Result<KeyImageSecret, Error>> {
self.result_rx.try_recv().ok()
}
pub fn wait(&self) -> Result<KeyImageSecret, Error> {
match self.result_rx.recv() {
Ok(r) => r,
Err(_) => Err(Error::Validation(
"key-image ceremony dropped without result".into(),
)),
}
}
}
impl Shared {
fn deliver(&self, r: Result<KeyImageSecret, Error>) {
if let Some(tx) = self.result_tx.lock().unwrap().take() {
let _ = tx.send(r);
}
}
fn round1(self: &Arc<Self>, proof: &Dleq) -> Result<(), Error> {
let (Some(w), Some(a1), Some(a2)) = (
secp::to_sec1_compressed(&self.my_partial),
secp::to_sec1_compressed(&proof.a1),
secp::to_sec1_compressed(&proof.a2),
) else {
return Err(Error::Validation(
"partial key image or DLEQ commitment is the identity".into(),
));
};
let msg = KeyImageR1 {
w: B64Bytes(w.to_vec()),
a1: B64Bytes(a1.to_vec()),
a2: B64Bytes(a2.to_vec()),
z: B64Bytes(secp::scalar_to_be_min(&proof.z)),
};
for pid in &self.other_subset {
self.send_to(TYPE_R1, &msg, pid)?;
}
let me = Arc::clone(self);
let others = self.other_subset.clone();
let exp = JsonExpect::new(
TYPE_R1,
self.other_subset.clone(),
Box::new(move |msgs| me.finalize(&others, msgs)),
);
self.params.broker().connect(TYPE_R1, Arc::new(exp));
Ok(())
}
fn finalize(self: &Arc<Self>, others: &[PartyId], msgs: Vec<JsonMessage>) {
let ids: Vec<Scalar> = self
.subset
.iter()
.map(|p| secp::scalar_from_be_reduce(&p.key))
.collect();
let mut v = self.my_partial;
for (pid, jm) in others.iter().zip(msgs.iter()) {
let r1: KeyImageR1 = match json_get(jm) {
Ok(m) => m,
Err(e) => return self.deliver(Err(Error::Serde(e))),
};
let (Some(wj), Some(a1), Some(a2)) = (
secp::from_sec1(&r1.w.0),
secp::from_sec1(&r1.a1.0),
secp::from_sec1(&r1.a2.0),
) else {
return self.deliver(Err(Error::Validation(format!(
"party {pid} sent a malformed partial key image"
))));
};
if r1.z.0.len() > 32 {
return self.deliver(Err(Error::Validation(format!(
"party {pid} sent an oversized DLEQ response"
))));
}
let z = secp::scalar_from_be_reduce(&r1.z.0);
let Some(pos) = self
.subset
.iter()
.position(|p| p.cmp_key(pid) == std::cmp::Ordering::Equal)
else {
return self.deliver(Err(Error::Validation(format!("{pid} not in subset"))));
};
let Some(full_idx) = self
.key
.party_ids
.iter()
.position(|p| p.cmp_key(pid) == std::cmp::Ordering::Equal)
else {
return self.deliver(Err(Error::Validation(format!(
"missing public share for {pid}"
))));
};
let lambda_j = match lagrange_coefficient(&ids, pos) {
Ok(l) => l,
Err(e) => return self.deliver(Err(e)),
};
let statement = self.key.big_xj[full_idx].mul(&lambda_j);
let session = dleq_session(
&self.identifier,
&self.key.ecdsa_pub,
&self.subset,
&pid.key,
self.hash,
);
let proof = Dleq { a1, a2, z };
if !proof.verify(&session, &self.point, &statement, &wj) {
return self.deliver(Err(Error::Validation(format!(
"partial key image from {pid} failed its DLEQ proof"
))));
}
v = v.add(&wj);
}
if bool::from(v.is_identity()) {
return self.deliver(Err(Error::Validation(
"key image is the identity point".into(),
)));
}
let secret =
match secret_from_key_image(&self.identifier, &self.key.ecdsa_pub, &v, self.hash) {
Ok(s) => s,
Err(e) => return self.deliver(Err(e)),
};
let public_key = self.key.ecdsa_pub.add(&secp::mul_base(&secret));
if bool::from(public_key.is_identity()) {
return self.deliver(Err(Error::Validation(
"derived child key is the identity point".into(),
)));
}
self.deliver(Ok(KeyImageSecret {
secret,
public_key,
key_image: v,
}));
}
fn send_to<T: Serialize>(&self, typ: &str, body: &T, to: &PartyId) -> Result<(), Error> {
let msg = json_wrap(
typ,
body,
Some(self.params.party_id().clone()),
Some(to.clone()),
)?;
self.params
.broker()
.receive(&msg)
.map_err(|e| Error::Validation(format!("broker delivery failed: {e}")))
}
}
struct Dleq {
a1: ProjectivePoint,
a2: ProjectivePoint,
z: Scalar,
}
impl Dleq {
fn prove(
session: &[u8],
p: &ProjectivePoint,
w: &Scalar,
y: &ProjectivePoint,
w_pt: &ProjectivePoint,
rng: &mut impl RngCore,
) -> Dleq {
let r = secp::random_scalar(rng);
let a1 = secp::mul_base(&r);
let a2 = p.mul(&r);
let c = dleq_challenge(session, p, y, w_pt, &a1, &a2);
let z = r.add(&c.mul(w));
Dleq { a1, a2, z }
}
fn verify(
&self,
session: &[u8],
p: &ProjectivePoint,
y: &ProjectivePoint,
w_pt: &ProjectivePoint,
) -> bool {
let c = dleq_challenge(session, p, y, w_pt, &self.a1, &self.a2);
secp::point_eq(&secp::mul_base(&self.z), &self.a1.add(&y.mul(&c)))
&& secp::point_eq(&p.mul(&self.z), &self.a2.add(&w_pt.mul(&c)))
}
}
fn dleq_challenge(
session: &[u8],
p: &ProjectivePoint,
y: &ProjectivePoint,
w_pt: &ProjectivePoint,
a1: &ProjectivePoint,
a2: &ProjectivePoint,
) -> Scalar {
let coords: Vec<Vec<u8>> = [p, y, w_pt, a1, a2]
.iter()
.flat_map(|pt| {
let (x, y) = secp::affine_be(pt);
[x, y]
})
.collect();
let operands: Vec<&[u8]> = coords.iter().map(|v| v.as_slice()).collect();
Scalar::from_bytes_be_reduce(&sha512_256i_tagged(session, &operands))
}
fn dleq_session(
identifier: &[u8],
ecdsa_pub: &ProjectivePoint,
subset: &[PartyId],
prover: &[u8],
hash: HashAlgorithm,
) -> Vec<u8> {
let (px, py) = secp::affine_be(ecdsa_pub);
let mut buf = Vec::with_capacity(96 + identifier.len() + 8 * subset.len());
buf.extend_from_slice(DLEQ_DOMAIN);
push_field(&mut buf, hash.name().as_bytes());
push_field(&mut buf, &px);
push_field(&mut buf, &py);
push_field(&mut buf, identifier);
buf.extend_from_slice(&(subset.len() as u64).to_be_bytes());
for p in subset {
push_field(&mut buf, strip(&p.key));
}
push_field(&mut buf, strip(prover));
buf
}
pub fn hash_to_point(
identifier: &[u8],
ecdsa_pub: &ProjectivePoint,
hash: HashAlgorithm,
) -> Result<ProjectivePoint, Error> {
validate_hash(hash).map_err(Error::Validation)?;
let compressed = secp::to_sec1_compressed(ecdsa_pub)
.ok_or_else(|| Error::Validation("public key is the identity".into()))?;
let mut base = Vec::with_capacity(80 + identifier.len());
base.extend_from_slice(POINT_DOMAIN);
push_field(&mut base, hash.name().as_bytes());
base.extend_from_slice(&compressed);
push_field(&mut base, identifier);
let prefix_len = base.len();
base.extend_from_slice(&[0u8; 4]);
let mut candidate = [0u8; 33];
candidate[0] = 0x02; for counter in 0u32..=u32::MAX {
base[prefix_len..].copy_from_slice(&counter.to_be_bytes());
candidate[1..].copy_from_slice(&digest32(hash, &base));
if let Some(p) = secp::from_sec1(&candidate)
&& !bool::from(p.is_identity())
{
return Ok(p);
}
}
unreachable!("no valid curve point in 2^32 hash-to-point attempts")
}
fn secret_from_key_image(
identifier: &[u8],
ecdsa_pub: &ProjectivePoint,
key_image: &ProjectivePoint,
hash: HashAlgorithm,
) -> Result<Scalar, Error> {
let pub_c = secp::to_sec1_compressed(ecdsa_pub)
.ok_or_else(|| Error::Validation("public key is the identity".into()))?;
let v_c = secp::to_sec1_compressed(key_image)
.ok_or_else(|| Error::Validation("key image is the identity".into()))?;
let mut buf = Vec::with_capacity(96 + identifier.len());
buf.extend_from_slice(SECRET_DOMAIN);
push_field(&mut buf, hash.name().as_bytes());
buf.extend_from_slice(&pub_c);
push_field(&mut buf, identifier);
buf.extend_from_slice(&v_c);
let s = Scalar::from_bytes_be_reduce(&digest32(hash, &buf));
if bool::from(s.is_zero()) {
return Err(Error::Validation(
"derived secret is zero; use a different identifier".into(),
));
}
Ok(s)
}
fn push_field(buf: &mut Vec<u8>, field: &[u8]) {
buf.extend_from_slice(&(field.len() as u64).to_be_bytes());
buf.extend_from_slice(field);
}
fn validate_sorted_subset(subset: &[PartyId]) -> Result<(), Error> {
for w in subset.windows(2) {
if w[0].cmp_key(&w[1]) != std::cmp::Ordering::Less {
return Err(Error::Validation(
"key-image subset must be sorted and distinct by key".into(),
));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::super::keygen::keygen;
use super::super::signing::{ecdsa_verify, hash_to_scalar, sign_with_tweak};
use super::*;
use crate::tss::testhub::TestHub;
use purecrypto::hash::sha256;
const H: HashAlgorithm = HashAlgorithm::Sha256;
fn party_ids(n: usize) -> Vec<PartyId> {
PartyId::sort(
(1..=n)
.map(|i| PartyId::new(i.to_string(), format!("P{i}"), vec![i as u8]))
.collect(),
0,
)
}
fn run(
ids: &[PartyId],
keys: &[Key],
members: &[usize],
t: usize,
identifier: &[u8],
) -> Vec<KeyImageSecret> {
let committee = PartyId::sort(members.iter().map(|&i| ids[i].clone()).collect(), 0);
let hub = TestHub::new(&committee);
let parties: Vec<KeyImageParty> = members
.iter()
.enumerate()
.map(|(pos, &i)| {
let params =
Parameters::new(committee.clone(), &committee[pos], t, hub.broker(pos));
KeyImageParty::new(
params,
keys[i].clone(),
identifier.to_vec(),
committee.clone(),
H,
)
.unwrap()
})
.collect();
parties
.iter()
.map(|p| p.wait().expect("key-image ceremony succeeds"))
.collect()
}
#[test]
fn key_image_equals_x_times_p() {
let ids = party_ids(3);
let keys = keygen(3, 1, &ids, &mut OsRng).unwrap();
let out = run(&ids, &keys, &[0, 1], 1, b"customer/42");
let subset: Vec<Scalar> = [0usize, 1]
.iter()
.map(|&i| secp::scalar_from_be_reduce(&ids[i].key))
.collect();
let mut x = Scalar::ZERO;
for (pos, &i) in [0usize, 1].iter().enumerate() {
x = x.add(&lagrange_coefficient(&subset, pos).unwrap().mul(&keys[i].xi));
}
let p = hash_to_point(b"customer/42", &keys[0].ecdsa_pub, H).unwrap();
assert!(secp::point_eq(&out[0].key_image, &p.mul(&x)));
for o in &out[1..] {
assert_eq!(o.secret_bytes(), out[0].secret_bytes());
assert_eq!(o.public_key_sec1(), out[0].public_key_sec1());
}
let want_pub = keys[0].ecdsa_pub.add(&secp::mul_base(&out[0].secret));
assert!(secp::point_eq(&out[0].public_key, &want_pub));
}
#[test]
fn different_committees_derive_the_same_secret() {
let ids = party_ids(3);
let keys = keygen(3, 1, &ids, &mut OsRng).unwrap();
let a = run(&ids, &keys, &[0, 1], 1, b"same-id");
let b = run(&ids, &keys, &[1, 2], 1, b"same-id");
assert_eq!(a[0].secret_bytes(), b[0].secret_bytes());
assert_eq!(a[0].public_key_sec1(), b[1].public_key_sec1());
}
#[test]
fn different_identifiers_give_different_secrets() {
let ids = party_ids(3);
let keys = keygen(3, 1, &ids, &mut OsRng).unwrap();
let a = run(&ids, &keys, &[0, 1], 1, b"id-a");
let b = run(&ids, &keys, &[0, 1], 1, b"id-b");
assert_ne!(a[0].secret_bytes(), b[0].secret_bytes());
assert_ne!(a[0].public_key_sec1(), b[0].public_key_sec1());
}
#[test]
fn derived_secret_signs_as_child_key() {
let ids = party_ids(3);
let keys = keygen(3, 1, &ids, &mut OsRng).unwrap();
let derived = run(&ids, &keys, &[0, 1], 1, b"hardened/0");
let msg = sha256(b"signed under a key-image-derived child key");
let sig = sign_with_tweak(&keys, &[0, 1], &derived[0].secret, &msg, &mut OsRng).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(&derived[0].public_key, &e, &r, &s));
assert!(!ecdsa_verify(&keys[0].ecdsa_pub, &e, &r, &s));
}
#[test]
fn hash_to_point_is_deterministic_and_identifier_bound() {
let ids = party_ids(2);
let keys = keygen(2, 1, &ids, &mut OsRng).unwrap();
let a = hash_to_point(b"x", &keys[0].ecdsa_pub, H).unwrap();
assert!(secp::point_eq(
&a,
&hash_to_point(b"x", &keys[0].ecdsa_pub, H).unwrap()
));
assert!(!secp::point_eq(
&a,
&hash_to_point(b"y", &keys[0].ecdsa_pub, H).unwrap()
));
assert!(!bool::from(a.is_identity()));
}
#[test]
fn dleq_rejects_a_wrong_partial() {
let ids = party_ids(2);
let keys = keygen(2, 1, &ids, &mut OsRng).unwrap();
let p = hash_to_point(b"dleq", &keys[0].ecdsa_pub, H).unwrap();
let w = secp::random_scalar(&mut OsRng);
let y = secp::mul_base(&w);
let w_pt = p.mul(&w);
let proof = Dleq::prove(b"s", &p, &w, &y, &w_pt, &mut OsRng);
assert!(proof.verify(b"s", &p, &y, &w_pt));
let bogus = p.mul(&secp::random_scalar(&mut OsRng));
assert!(!proof.verify(b"s", &p, &y, &bogus));
assert!(!proof.verify(b"other", &p, &y, &w_pt));
}
#[test]
fn each_hash_gives_an_unrelated_secret() {
let ids = party_ids(3);
let keys = keygen(3, 1, &ids, &mut OsRng).unwrap();
let committee = PartyId::sort(ids[..2].to_vec(), 0);
let mut seen = std::collections::HashSet::new();
for alg in [
HashAlgorithm::Sha256,
HashAlgorithm::Sha512,
HashAlgorithm::Sha3_256,
HashAlgorithm::Keccak256,
HashAlgorithm::Blake3,
] {
let hub = TestHub::new(&committee);
let parties: Vec<KeyImageParty> = (0..2)
.map(|i| {
let params =
Parameters::new(committee.clone(), &committee[i], 1, hub.broker(i));
KeyImageParty::new(
params,
keys[i].clone(),
b"same".to_vec(),
committee.clone(),
alg,
)
.unwrap()
})
.collect();
let out: Vec<_> = parties.iter().map(|p| p.wait().unwrap()).collect();
assert_eq!(out[0].secret_bytes(), out[1].secret_bytes());
assert!(
seen.insert(out[0].secret_bytes()),
"{} collided",
alg.name()
);
}
}
#[test]
fn unusable_hashes_are_rejected() {
let ids = party_ids(2);
let keys = keygen(2, 1, &ids, &mut OsRng).unwrap();
let committee = PartyId::sort(ids.clone(), 0);
for alg in [
HashAlgorithm::Sha1,
HashAlgorithm::Md5,
HashAlgorithm::Sha224,
] {
let hub = TestHub::new(&committee);
let params = Parameters::new(committee.clone(), &committee[0], 1, hub.broker(0));
assert!(
KeyImageParty::new(
params,
keys[0].clone(),
b"id".to_vec(),
committee.clone(),
alg
)
.is_err(),
"{} accepted",
alg.name()
);
assert!(hash_to_point(b"id", &keys[0].ecdsa_pub, alg).is_err());
}
}
#[test]
fn subset_smaller_than_threshold_is_rejected() {
let ids = party_ids(3);
let keys = keygen(3, 2, &ids, &mut OsRng).unwrap();
let committee = PartyId::sort(ids[..2].to_vec(), 0);
let hub = TestHub::new(&committee);
let params = Parameters::new(committee.clone(), &committee[0], 2, hub.broker(0));
assert!(KeyImageParty::new(params, keys[0].clone(), b"id".to_vec(), committee, H).is_err());
}
}
#[cfg(test)]
mod vectors {
use super::*;
use crate::tss::keyimage_hash::validate as validate_hash;
use crate::tss::testhub::TestHub;
use purecrypto::hash::sha512;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
struct Case {
hash: String,
identifier: String,
point: String,
partials: Vec<String>,
key_image: String,
secret: String,
child_public_key: String,
dleq: DleqCase,
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
struct DleqCase {
session_digest: String,
nonce: String,
a1: String,
a2: String,
challenge: String,
z: String,
wire_json: String,
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
struct Vectors {
scheme: String,
point_domain: String,
secret_domain: String,
dleq_domain: String,
threshold: usize,
party_keys: Vec<String>,
shares: Vec<String>,
master_secret: String,
group_public_key: String,
committee: Vec<String>,
cases: Vec<Case>,
}
fn hex_scalar(s: &Scalar) -> String {
hex::encode(s.to_bytes_be())
}
fn hex_point(p: &ProjectivePoint) -> String {
hex::encode(secp::to_sec1_compressed(p).expect("non-identity point"))
}
fn scalar_from_hex(s: &str) -> Scalar {
let b: [u8; 32] = hex::decode(s).unwrap().try_into().unwrap();
Scalar::from_bytes_be(&b).expect("canonical scalar")
}
fn point_from_hex(s: &str) -> ProjectivePoint {
secp::from_sec1(&hex::decode(s).unwrap()).expect("valid point")
}
fn vector_keys() -> (Vec<PartyId>, Vec<Key>, Scalar) {
let coeffs: Vec<Scalar> = ["tsslib/keyimage/vector/a0", "tsslib/keyimage/vector/a1"]
.iter()
.map(|label| {
Scalar::from_bytes_be_reduce(&digest32(HashAlgorithm::Sha256, label.as_bytes()))
})
.collect();
let ids = PartyId::sort(
(1..=3u8)
.map(|i| PartyId::new(i.to_string(), format!("P{i}"), vec![i]))
.collect(),
0,
);
let xs: Vec<Scalar> = ids
.iter()
.map(|p| {
let x = secp::scalar_from_be_reduce(&p.key);
coeffs[1].mul(&x).add(&coeffs[0])
})
.collect();
let big_xj: Vec<ProjectivePoint> = xs.iter().map(secp::mul_base).collect();
let ecdsa_pub = secp::mul_base(&coeffs[0]);
let keys = (0..3)
.map(|i| Key {
n: 3,
t: 1,
idx: i,
party_ids: ids.clone(),
xi: xs[i].clone(),
big_xj: big_xj.clone(),
ecdsa_pub,
ot: vec![None, None, None],
chain_code: [0u8; 32],
})
.collect();
(ids, keys, coeffs[0].clone())
}
fn vector_identifiers() -> Vec<Vec<u8>> {
vec![
Vec::new(),
b"abc".to_vec(),
b"m/44'/60'/0'/0/0".to_vec(),
(0u8..=31).collect(),
]
}
fn vector_hashes() -> Vec<HashAlgorithm> {
HashAlgorithm::ALL
.iter()
.copied()
.filter(|&a| validate_hash(a).is_ok())
.collect()
}
fn build() -> Vectors {
let (ids, keys, master) = vector_keys();
let ecdsa_pub = keys[0].ecdsa_pub;
let committee: Vec<PartyId> = ids[..2].to_vec();
let lambda_ids: Vec<Scalar> = committee
.iter()
.map(|p| secp::scalar_from_be_reduce(&p.key))
.collect();
let mut cases = Vec::new();
for hash in vector_hashes() {
for identifier in vector_identifiers() {
let point = hash_to_point(&identifier, &ecdsa_pub, hash).unwrap();
let mut partials = Vec::new();
let mut v = ProjectivePoint::identity();
for (pos, key) in keys.iter().take(committee.len()).enumerate() {
let w = lagrange_coefficient(&lambda_ids, pos).unwrap().mul(&key.xi);
let partial = point.mul(&w);
v = v.add(&partial);
partials.push(hex_point(&partial));
}
let secret = secret_from_key_image(&identifier, &ecdsa_pub, &v, hash).unwrap();
let child = ecdsa_pub.add(&secp::mul_base(&secret));
let w = lagrange_coefficient(&lambda_ids, 0)
.unwrap()
.mul(&keys[0].xi);
let w_pt = point.mul(&w);
let statement = secp::mul_base(&w);
let session =
dleq_session(&identifier, &ecdsa_pub, &committee, &committee[0].key, hash);
let r = Scalar::from_bytes_be_reduce(&digest32(
HashAlgorithm::Sha256,
&[
b"tsslib/keyimage/vector/nonce".as_slice(),
hash.name().as_bytes(),
&identifier,
]
.concat(),
));
let a1 = secp::mul_base(&r);
let a2 = point.mul(&r);
let c = dleq_challenge(&session, &point, &statement, &w_pt, &a1, &a2);
let z = r.add(&c.mul(&w));
let wire = KeyImageR1 {
w: B64Bytes(secp::to_sec1_compressed(&w_pt).unwrap().to_vec()),
a1: B64Bytes(secp::to_sec1_compressed(&a1).unwrap().to_vec()),
a2: B64Bytes(secp::to_sec1_compressed(&a2).unwrap().to_vec()),
z: B64Bytes(secp::scalar_to_be_min(&z)),
};
cases.push(Case {
hash: hash.name().to_string(),
identifier: hex::encode(&identifier),
point: hex_point(&point),
partials,
key_image: hex_point(&v),
secret: hex_scalar(&secret),
child_public_key: hex_point(&child),
dleq: DleqCase {
session_digest: hex::encode(&sha512(&session)[..32]),
nonce: hex_scalar(&r),
a1: hex_point(&a1),
a2: hex_point(&a2),
challenge: hex_scalar(&c),
z: hex_scalar(&z),
wire_json: serde_json::to_string(&wire).unwrap(),
},
});
}
}
Vectors {
scheme: "dklstss/keyimage/v1".into(),
point_domain: String::from_utf8(POINT_DOMAIN.to_vec()).unwrap(),
secret_domain: String::from_utf8(SECRET_DOMAIN.to_vec()).unwrap(),
dleq_domain: String::from_utf8(DLEQ_DOMAIN.to_vec()).unwrap(),
threshold: 1,
party_keys: ids.iter().map(|p| hex::encode(&p.key)).collect(),
shares: keys.iter().map(|k| hex_scalar(&k.xi)).collect(),
master_secret: hex_scalar(&master),
group_public_key: hex_point(&ecdsa_pub),
committee: committee.iter().map(|p| hex::encode(&p.key)).collect(),
cases,
}
}
fn checked_in() -> Vectors {
serde_json::from_str(include_str!("testdata/keyimage_vectors.json"))
.expect("valid keyimage_vectors.json")
}
#[test]
#[ignore]
fn print() {
println!("{}", serde_json::to_string_pretty(&build()).unwrap());
}
#[test]
fn recomputation_matches_checked_in_file() {
let (got, want) = (build(), checked_in());
assert_eq!(
got.cases.len(),
want.cases.len(),
"case count changed (a new HashAlgorithm to cover?)"
);
for (g, w) in got.cases.iter().zip(want.cases.iter()) {
assert_eq!(g, w, "derivation changed for hash {}", w.hash);
}
assert_eq!(got, want, "key-image construction changed");
}
#[test]
fn checked_in_file_is_self_consistent() {
let f = checked_in();
let ecdsa_pub = point_from_hex(&f.group_public_key);
let master = scalar_from_hex(&f.master_secret);
assert!(secp::point_eq(&ecdsa_pub, &secp::mul_base(&master)));
let (ids, _, _) = vector_keys();
let committee: Vec<PartyId> = ids[..2].to_vec();
assert_eq!(
f.committee,
committee
.iter()
.map(|p| hex::encode(&p.key))
.collect::<Vec<_>>()
);
let lambda_ids: Vec<Scalar> = committee
.iter()
.map(|p| secp::scalar_from_be_reduce(&p.key))
.collect();
let statement = secp::mul_base(
&lagrange_coefficient(&lambda_ids, 0)
.unwrap()
.mul(&scalar_from_hex(&f.shares[0])),
);
for case in &f.cases {
let hash = HashAlgorithm::from_name(&case.hash).expect("known hash name");
let identifier = hex::decode(&case.identifier).unwrap();
let point = point_from_hex(&case.point);
assert!(secp::point_eq(
&point,
&hash_to_point(&identifier, &ecdsa_pub, hash).unwrap()
));
let mut v = ProjectivePoint::identity();
for p in &case.partials {
v = v.add(&point_from_hex(p));
}
assert!(secp::point_eq(&v, &point_from_hex(&case.key_image)));
assert!(secp::point_eq(&v, &point.mul(&master)));
let secret = scalar_from_hex(&case.secret);
assert!(bool::from(secret.ct_eq(
&secret_from_key_image(&identifier, &ecdsa_pub, &v, hash).unwrap()
)));
assert!(secp::point_eq(
&point_from_hex(&case.child_public_key),
&ecdsa_pub.add(&secp::mul_base(&secret))
));
let session =
dleq_session(&identifier, &ecdsa_pub, &committee, &committee[0].key, hash);
assert_eq!(
hex::encode(&sha512(&session)[..32]),
case.dleq.session_digest
);
let proof = Dleq {
a1: point_from_hex(&case.dleq.a1),
a2: point_from_hex(&case.dleq.a2),
z: scalar_from_hex(&case.dleq.z),
};
assert!(proof.verify(
&session,
&point,
&statement,
&point_from_hex(&case.partials[0])
));
let msg: KeyImageR1 = serde_json::from_str(&case.dleq.wire_json).unwrap();
assert_eq!(hex::encode(&msg.w.0), case.partials[0]);
assert_eq!(hex::encode(&msg.a1.0), case.dleq.a1);
assert_eq!(hex::encode(&msg.a2.0), case.dleq.a2);
}
}
#[test]
fn ceremony_reproduces_vectors() {
let f = checked_in();
let (ids, keys, _) = vector_keys();
let committee = PartyId::sort(ids[..2].to_vec(), 0);
for case in &f.cases {
let hash = HashAlgorithm::from_name(&case.hash).expect("known hash name");
let identifier = hex::decode(&case.identifier).unwrap();
let hub = TestHub::new(&committee);
let parties: Vec<KeyImageParty> = (0..2)
.map(|i| {
let params =
Parameters::new(committee.clone(), &committee[i], 1, hub.broker(i));
KeyImageParty::new(
params,
keys[i].clone(),
identifier.clone(),
committee.clone(),
hash,
)
.expect("ceremony starts")
})
.collect();
for p in &parties {
let out = p.wait().expect("ceremony succeeds");
assert_eq!(hex_point(&out.key_image), case.key_image, "{}", case.hash);
assert_eq!(hex::encode(out.secret_bytes()), case.secret);
assert_eq!(hex::encode(out.public_key_sec1()), case.child_public_key);
}
}
}
}