tbs/
lib.rs

1//! # Threshold Blind Signatures
2//!
3//! This library implements an ad-hoc threshold blind signature scheme based on
4//! BLS signatures using the (unrelated) BLS12-381 curve.
5
6use std::collections::BTreeMap;
7
8use bls12_381::{pairing, G1Affine, G1Projective, G2Affine, G2Projective, Scalar};
9use fedimint_core::bls12_381_serde;
10use fedimint_core::encoding::{Decodable, Encodable};
11use group::ff::Field;
12use group::{Curve, Group};
13use hex::encode;
14use rand::rngs::OsRng;
15use rand::SeedableRng;
16use rand_chacha::ChaChaRng;
17use serde::{Deserialize, Serialize};
18use sha3::Digest;
19
20const HASH_TAG: &[u8] = b"TBS_BLS12-381_";
21const FINGERPRINT_TAG: &[u8] = b"TBS_KFP24_";
22
23fn hash_bytes_to_g1(data: &[u8]) -> G1Projective {
24    let mut hash_engine = sha3::Sha3_256::new();
25
26    hash_engine.update(HASH_TAG);
27    hash_engine.update(data);
28
29    let mut prng = ChaChaRng::from_seed(hash_engine.finalize().into());
30
31    G1Projective::random(&mut prng)
32}
33
34#[derive(Copy, Clone, Debug, Eq, PartialEq, Encodable, Decodable, Serialize, Deserialize)]
35pub struct SecretKeyShare(#[serde(with = "bls12_381_serde::scalar")] pub Scalar);
36
37#[derive(Copy, Clone, Debug, Eq, PartialEq, Encodable, Decodable, Serialize, Deserialize)]
38pub struct PublicKeyShare(#[serde(with = "bls12_381_serde::g2")] pub G2Affine);
39
40#[derive(Copy, Clone, Debug, Eq, PartialEq, Encodable, Decodable, Serialize, Deserialize)]
41pub struct AggregatePublicKey(#[serde(with = "bls12_381_serde::g2")] pub G2Affine);
42
43#[derive(Copy, Clone, Debug, Eq, PartialEq, Encodable, Decodable, Serialize, Deserialize)]
44pub struct Message(#[serde(with = "bls12_381_serde::g1")] pub G1Affine);
45
46#[derive(Copy, Clone, Eq, PartialEq, Encodable, Decodable, Serialize, Deserialize)]
47pub struct BlindingKey(#[serde(with = "bls12_381_serde::scalar")] pub Scalar);
48
49#[derive(Copy, Clone, Debug, Eq, PartialEq, Encodable, Decodable, Serialize, Deserialize)]
50pub struct BlindedMessage(#[serde(with = "bls12_381_serde::g1")] pub G1Affine);
51
52#[derive(Copy, Clone, Debug, Eq, PartialEq, Encodable, Decodable, Serialize, Deserialize)]
53pub struct BlindedSignatureShare(#[serde(with = "bls12_381_serde::g1")] pub G1Affine);
54
55#[derive(Copy, Clone, Debug, Eq, PartialEq, Encodable, Decodable, Serialize, Deserialize)]
56pub struct BlindedSignature(#[serde(with = "bls12_381_serde::g1")] pub G1Affine);
57
58#[derive(Copy, Clone, Debug, Eq, PartialEq, Encodable, Decodable, Serialize, Deserialize)]
59pub struct Signature(#[serde(with = "bls12_381_serde::g1")] pub G1Affine);
60
61macro_rules! point_hash_impl {
62    ($type:ty) => {
63        impl std::hash::Hash for $type {
64            fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
65                self.0.to_compressed().hash(state);
66            }
67        }
68    };
69}
70
71point_hash_impl!(PublicKeyShare);
72point_hash_impl!(AggregatePublicKey);
73point_hash_impl!(Message);
74point_hash_impl!(BlindedMessage);
75point_hash_impl!(BlindedSignatureShare);
76point_hash_impl!(BlindedSignature);
77point_hash_impl!(Signature);
78
79pub fn derive_pk_share(sk: &SecretKeyShare) -> PublicKeyShare {
80    PublicKeyShare((G2Projective::generator() * sk.0).to_affine())
81}
82
83impl BlindingKey {
84    pub fn random() -> BlindingKey {
85        // TODO: fix rand incompatibities
86        BlindingKey(Scalar::random(OsRng))
87    }
88
89    fn fingerprint(&self) -> [u8; 32] {
90        let mut hash_engine = sha3::Sha3_256::new();
91        hash_engine.update(FINGERPRINT_TAG);
92        hash_engine.update(self.0.to_bytes());
93        let result = hash_engine.finalize();
94        result.into()
95    }
96}
97
98impl ::core::fmt::Debug for BlindingKey {
99    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
100        let fingerprint = self.fingerprint();
101        let fingerprint_hex = encode(&fingerprint[..]);
102        write!(f, "BlindingKey({fingerprint_hex})")
103    }
104}
105
106impl ::core::fmt::Display for BlindingKey {
107    fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
108        let fingerprint = self.fingerprint();
109        let fingerprint_hex = encode(&fingerprint[..]);
110        write!(f, "{fingerprint_hex}")
111    }
112}
113
114impl Message {
115    pub fn from_bytes(msg: &[u8]) -> Message {
116        Message(hash_bytes_to_g1(msg).to_affine())
117    }
118}
119
120pub fn blind_message(msg: Message, blinding_key: BlindingKey) -> BlindedMessage {
121    let blinded_msg = msg.0 * blinding_key.0;
122
123    BlindedMessage(blinded_msg.to_affine())
124}
125
126pub fn sign_message(msg: BlindedMessage, sks: SecretKeyShare) -> BlindedSignatureShare {
127    let sig = msg.0 * sks.0;
128    BlindedSignatureShare(sig.to_affine())
129}
130
131pub fn verify_signature_share(
132    msg: BlindedMessage,
133    sig: BlindedSignatureShare,
134    pk: PublicKeyShare,
135) -> bool {
136    pairing(&msg.0, &pk.0) == pairing(&sig.0, &G2Affine::generator())
137}
138
139/// Combines the exact threshold of valid blinded signature shares to a blinded
140/// signature. The responsibility of verifying the shares and supplying
141/// exactly the necessary threshold of shares lies with the caller.
142/// # Panics
143/// If shares is empty
144pub fn aggregate_signature_shares(
145    shares: &BTreeMap<u64, BlindedSignatureShare>,
146) -> BlindedSignature {
147    // this is a special case for one-of-one federations
148    if shares.len() == 1 {
149        return BlindedSignature(
150            shares
151                .values()
152                .next()
153                .expect("We have at least one value")
154                .0,
155        );
156    }
157
158    BlindedSignature(
159        lagrange_multipliers(
160            shares
161                .keys()
162                .cloned()
163                .map(|peer| Scalar::from(peer + 1))
164                .collect(),
165        )
166        .into_iter()
167        .zip(shares.values())
168        .map(|(lagrange_multiplier, share)| lagrange_multiplier * share.0)
169        .reduce(|a, b| a + b)
170        .expect("We have at least one share")
171        .to_affine(),
172    )
173}
174
175// TODO: aggregating public key shares is hacky since we can obtain the
176// aggregated public by evaluating the dkg polynomial at zero - this function
177// should be removed, however it is currently needed in the mint module to
178// until we add the aggregated public key to the mint config.
179pub fn aggregate_public_key_shares(shares: &BTreeMap<u64, PublicKeyShare>) -> AggregatePublicKey {
180    // this is a special case for one-of-one federations
181    if shares.len() == 1 {
182        return AggregatePublicKey(
183            shares
184                .values()
185                .next()
186                .expect("We have at least one value")
187                .0,
188        );
189    }
190
191    AggregatePublicKey(
192        lagrange_multipliers(
193            shares
194                .keys()
195                .cloned()
196                .map(|peer| Scalar::from(peer + 1))
197                .collect(),
198        )
199        .into_iter()
200        .zip(shares.values())
201        .map(|(lagrange_multiplier, share)| lagrange_multiplier * share.0)
202        .reduce(|a, b| a + b)
203        .expect("We have at least one share")
204        .to_affine(),
205    )
206}
207
208fn lagrange_multipliers(scalars: Vec<Scalar>) -> Vec<Scalar> {
209    scalars
210        .iter()
211        .map(|i| {
212            scalars
213                .iter()
214                .filter(|j| *j != i)
215                .map(|j| j * (j - i).invert().expect("We filtered the case j == i"))
216                .reduce(|a, b| a * b)
217                .expect("We have at least one share")
218        })
219        .collect()
220}
221
222pub fn verify_blinded_signature(
223    msg: BlindedMessage,
224    sig: BlindedSignature,
225    pk: AggregatePublicKey,
226) -> bool {
227    pairing(&msg.0, &pk.0) == pairing(&sig.0, &G2Affine::generator())
228}
229
230pub fn unblind_signature(blinding_key: BlindingKey, blinded_sig: BlindedSignature) -> Signature {
231    let sig = blinded_sig.0 * blinding_key.0.invert().unwrap();
232    Signature(sig.to_affine())
233}
234
235pub fn verify(msg: Message, sig: Signature, pk: AggregatePublicKey) -> bool {
236    pairing(&msg.0, &pk.0) == pairing(&sig.0, &G2Affine::generator())
237}
238
239#[cfg(test)]
240mod tests {
241    use std::collections::BTreeMap;
242
243    use bls12_381::{G2Projective, Scalar};
244    use fedimint_core::bitcoin::hashes::sha256;
245    use fedimint_core::BitcoinHash;
246    use group::ff::Field;
247    use group::Curve;
248    use rand::SeedableRng;
249    use rand_chacha::ChaChaRng;
250
251    use crate::{
252        aggregate_signature_shares, blind_message, derive_pk_share, sign_message,
253        unblind_signature, verify, verify_signature_share, AggregatePublicKey,
254        BlindedSignatureShare, BlindingKey, Message, PublicKeyShare, SecretKeyShare,
255    };
256
257    fn dealer_agg_pk() -> AggregatePublicKey {
258        AggregatePublicKey((G2Projective::generator() * coefficient(0)).to_affine())
259    }
260
261    fn dealer_pk(threshold: u64, peer: u64) -> PublicKeyShare {
262        derive_pk_share(&dealer_sk(threshold, peer))
263    }
264
265    fn dealer_sk(threshold: u64, peer: u64) -> SecretKeyShare {
266        let x = Scalar::from(peer + 1);
267
268        // We evaluate the scalar polynomial of degree threshold - 1 at the point x
269        // using the Horner schema.
270
271        let y = (0..threshold)
272            .map(coefficient)
273            .rev()
274            .reduce(|accumulator, c| accumulator * x + c)
275            .expect("We have at least one coefficient");
276
277        SecretKeyShare(y)
278    }
279
280    fn coefficient(index: u64) -> Scalar {
281        Scalar::random(&mut ChaChaRng::from_seed(
282            *sha256::Hash::hash(&index.to_be_bytes()).as_byte_array(),
283        ))
284    }
285
286    #[test]
287    fn test_roundtrip() {
288        const PEERS: u64 = 4;
289        const THRESHOLD: u64 = 3;
290
291        let message = Message::from_bytes(b"Hello World!");
292        let blinding_key = BlindingKey::random();
293
294        let b_message = blind_message(message, blinding_key);
295
296        for peer in 0..PEERS {
297            assert!(verify_signature_share(
298                b_message,
299                sign_message(b_message, dealer_sk(THRESHOLD, peer)),
300                dealer_pk(THRESHOLD, peer)
301            ));
302        }
303
304        let signature_shares = (0..THRESHOLD)
305            .map(|peer| (peer, sign_message(b_message, dealer_sk(THRESHOLD, peer))))
306            .collect::<BTreeMap<u64, BlindedSignatureShare>>();
307
308        let signature = aggregate_signature_shares(&signature_shares);
309
310        let signature = unblind_signature(blinding_key, signature);
311
312        assert!(verify(message, signature, dealer_agg_pk()));
313    }
314
315    #[test]
316    fn test_blindingkey_fingerprint_multiple_calls_same_result() {
317        let bkey = BlindingKey::random();
318        assert_eq!(bkey.fingerprint(), bkey.fingerprint());
319    }
320
321    #[test]
322    fn test_blindingkey_fingerprint_ne_scalar() {
323        let bkey = BlindingKey::random();
324        assert_ne!(bkey.fingerprint(), bkey.0.to_bytes());
325    }
326}