nash_protocol/protocol/
signer.rs

1#[cfg(feature = "k256")]
2use k256::ecdsa::signature::Signer as k256_Signer;
3#[cfg(feature = "k256")]
4use k256::ecdsa::{Signature, SigningKey};
5#[cfg(feature = "secp256k1")]
6use rust_bigint::traits::Converter;
7#[cfg(feature = "secp256k1")]
8use secp256k1::constants::{COMPACT_SIGNATURE_SIZE, MESSAGE_SIZE, SECRET_KEY_SIZE};
9#[cfg(feature = "secp256k1")]
10use secp256k1::{Message, SecretKey};
11
12use nash_mpc::client::APIchildkey;
13use nash_mpc::common::Curve;
14#[cfg(feature = "secp256k1")]
15use nash_mpc::curves::secp256_k1::get_context;
16#[cfg(feature = "k256")]
17use nash_mpc::curves::secp256_k1_rust::Secp256k1Scalar;
18#[cfg(feature = "k256")]
19use nash_mpc::curves::traits::ECScalar;
20use nash_mpc::paillier_common;
21use nash_mpc::rust_bigint::BigInt;
22
23use crate::errors::{ProtocolError, Result};
24use crate::protocol::RequestPayloadSignature;
25use crate::types::ApiKeys;
26use crate::types::Blockchain;
27use crate::types::PublicKey;
28#[cfg(feature = "secp256k1")]
29use crate::utils::{der_encode_sig, hash_message};
30use std::sync::atomic::{AtomicU32, Ordering};
31
32pub fn chain_path(chain: Blockchain) -> &'static str {
33    match chain {
34        Blockchain::NEO => "m/44'/888'/0'/0/0",
35        Blockchain::Ethereum => "m/44'/60'/0'/0/0",
36        Blockchain::Bitcoin => "m/44'/0'/0'/0/0",
37    }
38}
39
40#[derive(Debug)]
41pub struct Signer {
42    pub api_keys: ApiKeys,
43    k1_remaining: AtomicU32,
44    r1_remaining: AtomicU32,
45}
46
47impl Signer {
48    pub fn new(key_path: &str) -> Result<Self> {
49        Ok(Self {
50            api_keys: ApiKeys::new(key_path)?,
51            k1_remaining: AtomicU32::new(0),
52            r1_remaining: AtomicU32::new(0),
53        })
54    }
55
56    pub fn from_data(secret: &str, session: &str) -> Result<Self> {
57        Ok(Self {
58            api_keys: ApiKeys::from_data(secret, session)?,
59            k1_remaining: AtomicU32::new(0),
60            r1_remaining: AtomicU32::new(0),
61        })
62    }
63
64    /// Sign GraphQL payload request via payload signing key
65    /// The output is a hex string where signature has been DER encoded
66    /// Either implemented with k256 from rustcrypto (pure rust) or secp256k1 (better performance)
67    #[cfg(feature = "rustcrypto")]
68    pub fn sign_canonical_string(&self, request: &str) -> RequestPayloadSignature {
69        let signing_key: Secp256k1Scalar =
70            ECScalar::from(&self.api_keys.keys.payload_signing_key).expect("Invalid key");
71        let key = SigningKey::from_bytes(&signing_key.to_vec()).expect("invalid secret key");
72        let sig_pre: Signature = key.try_sign(request.as_bytes()).expect("signing failed");
73        let sig = sig_pre.to_der();
74        RequestPayloadSignature {
75            signed_digest: hex::encode(sig),
76            public_key: self.request_payload_public_key(),
77        }
78    }
79    #[cfg(feature = "secp256k1")]
80    pub fn sign_canonical_string(&self, request: &str) -> RequestPayloadSignature {
81        // create message hash
82        let message_hash = hash_message(request).to_bytes();
83        // add leading zeroes if necessary
84        let mut msg_vec = vec![0; MESSAGE_SIZE - message_hash.len()];
85        msg_vec.extend_from_slice(&message_hash);
86        let msg = Message::from_slice(&msg_vec).unwrap();
87
88        // SecretKey from BigInt
89        let vec = BigInt::to_vec(&self.api_keys.keys.payload_signing_key);
90        let mut v = vec![0; SECRET_KEY_SIZE - vec.len()];
91        v.extend(&vec);
92        let key = SecretKey::from_slice(&v).expect("invalid secret key");
93
94        // actual signature generation (and encoding)
95        let signature = get_context().sign(&msg, &key).serialize_compact();
96        let r = BigInt::from_bytes(&signature[0..COMPACT_SIGNATURE_SIZE / 2]);
97        let s = BigInt::from_bytes(&signature[COMPACT_SIGNATURE_SIZE / 2..COMPACT_SIGNATURE_SIZE]);
98        let sig = der_encode_sig(&r, &s);
99
100        RequestPayloadSignature {
101            signed_digest: hex::encode(sig),
102            public_key: self.request_payload_public_key(),
103        }
104    }
105
106    /// Sign data hashed to `BigInt` with the MPC child key for the given `Blockchain`
107    pub fn sign_child_key(
108        &self,
109        data: BigInt,
110        chain: Blockchain,
111    ) -> Result<(BigInt, BigInt, String)> {
112        if self.get_remaining_r_vals(&chain) <= 0 {
113            return Err(ProtocolError("Ran out of R values"));
114        }
115        let key = self.get_child_key(chain);
116        let curve = match chain {
117            Blockchain::Ethereum | Blockchain::Bitcoin => Curve::Secp256k1,
118            Blockchain::NEO => Curve::Secp256r1,
119        };
120        // FIX ME: Right now the pools are under a global mutex. Make them managed
121        let (sig, r) = nash_mpc::client::compute_presig(&key, &data, curve)
122            .map_err(|_| ProtocolError("Error computing presignature"))?;
123        // Track the fact that we now have one less R value
124        self.decr_r_vals(chain);
125        Ok((sig, r, key.public_key))
126    }
127
128    /// Get public key for child key on `chain`
129    pub fn child_public_key(&self, chain: Blockchain) -> Result<PublicKey> {
130        PublicKey::new(chain, &self.get_child_key(chain).public_key)
131    }
132
133    /// Return public key for payload signing in format expected by the Nash backend service
134    /// BigInt conversion to hex will strip leading zeros, which Nash backend doesn't like
135    pub fn request_payload_public_key(&self) -> String {
136        let mut key_str = self.api_keys.keys.payload_public_key.to_str_radix(16);
137        if key_str.len() % 2 != 0 {
138            key_str = format!("0{}", &key_str);
139        }
140        key_str
141    }
142
143    pub fn paillier_pk(&self) -> &paillier_common::EncryptionKey {
144        &self.api_keys.keys.paillier_pk
145    }
146
147    pub fn get_address(&self, chain: Blockchain) -> &str {
148        &self.api_keys.keys.child_keys[chain_path(chain)].address
149    }
150
151    pub fn get_child_key(&self, chain: Blockchain) -> APIchildkey {
152        let key = &self.api_keys.keys.child_keys[chain_path(chain)];
153        // TODO: these should be unified! it was more convenient to parse the paillier_pk
154        // once for all the key data from deserialization, which is why I need this atm.
155        // is on list to fix once things are verified to be working
156        APIchildkey {
157            client_secret_share: key.client_secret_share.clone(),
158            paillier_pk: self.paillier_pk().clone(),
159            public_key: key.public_key.clone(),
160            server_secret_share_encrypted: key.server_secret_share_encrypted.clone(),
161        }
162    }
163
164    /// Get the current number of available R values for the given chain
165    pub fn get_remaining_r_vals(&self, chain: &Blockchain) -> u32 {
166        match chain {
167            Blockchain::Ethereum | Blockchain::Bitcoin => self.k1_remaining.load(Ordering::Acquire),
168            Blockchain::NEO => self.r1_remaining.load(Ordering::Acquire),
169        }
170    }
171
172    /// Call after filling R values to update tracking
173    pub fn fill_r_vals(&self, chain: Blockchain, n: u32) {
174        match chain {
175            Blockchain::Ethereum | Blockchain::Bitcoin => self.k1_remaining.fetch_add(n, Ordering::Release),
176            Blockchain::NEO => self.r1_remaining.fetch_add(n, Ordering::Release),
177        };
178        tracing::info!("filled {:?}: +{}", chain, n);
179    }
180
181    fn decr_r_vals(&self, chain: Blockchain) {
182        match chain {
183            Blockchain::Ethereum | Blockchain::Bitcoin => self.k1_remaining.fetch_sub(1, Ordering::Release),
184            Blockchain::NEO => self.r1_remaining.fetch_sub(1, Ordering::Release),
185        };
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::Signer;
192
193    #[test]
194    fn test_signing() {
195        let base64_key = "eyJjaGlsZF9rZXlzIjp7fSwKICAgICAgICAicGFpbGxpZXJfcGsiOnsibiI6IjU5ODdlNjIyMjYxY2FmOTZlMjU4MjZjNzBjZjMyM2IyNjE5NGZmOWNmZTY5ZTNmNDBmMzBkMzA2NTcxNjQyY2FlYThhMzE0M2QxMWZmOTRjMTM4ODM2MDQ4NjczNTdhZThjMGU2NjNiZjAzZDAwOTMwMTZkN2Y0ZDc5MGFlMjRlMjkxNzgwM2Q4MTJiNjQxYWYyZDZjMDk1NzNkMTEyZWI3Njg2NDY1MjkxY2QxNDZmZDY2MmY3N2Y1OTVlZjgzMjc3YmUxNjgwZDA0MGIxZjNjNDk5YzgxOTE3NTcyMDZlNTEwYWU1NDcyNGQ2NjdmYzA0MWEyYzdjMmZmM2QzYjY2YzM3MjlkYzI1ZTAyYzQwMTllZDNhMDEyZmQ3NWVjMGUwMzk0OGNmNzgzYWQzOTAyY2U1ZTVlNzIyMjljM2RkM2ExNGI5MzRkNjAyNjlhY2I3YmEwYmQ0MTVkMmRlMTI4ZWYxODcyMjQwMGJhZWEyZTg1MGU2ZDFmZDg3ODdhMDEzMGQ1MTYyMDZkNzE4YTQ5ZDdhMjFkNDI4YjBmYTM3NzMwNzliNjQ4NjE4MTExOTFiNTUwMDFkNGMyYzI5ZjYzMDMxNGJlMTkxY2YzY2EzZjBmOGUwOWVlMDk1NDNmZmRkYTNmOTdjZjE2OWQ1MmUwNjdjZmQ0MGNiMzAzOTQxIn0sCiAgICAgICAgInBheWxvYWRfcHVibGljX2tleSI6IjA0NjE2NDZmZGM0NTQ0ZjEwMjk0ZTIwZTk5NGNlNTZkOGMwZmY4NTI1OTZlYjZiM2FhMGJhOWQ0YjIwNzlkODZkNDJiM2I1ZTg0OTFhNDhmZjZlMTYyMDczMjU3OTgwNzkxNmVlYjA3YmViNmY5OTcwZGM1OTUyYmQ0NDQ0MDRmNzQiLAogICAgICAgICJwYXlsb2FkX3NpZ25pbmdfa2V5IjoiYmI4YmNmNTJhNWY5NDRmMzUxYzViYzg1NmI3YTRjNDFhNWYzNzBmNWNlOTlkY2UwYzhkNmYxZDQ5MWNkMzRiZiIsCiAgICAgICAgInZlcnNpb24iOjB9";
196        let signer = Signer::from_data(&base64_key, "").unwrap();
197        let signature = signer.sign_canonical_string("hello, world!");
198        assert_eq!(signature.signed_digest, "30440220135a79b11caa321f1548d4b86e17c9b53525ffcdeab5e559d6cca310623cc45d02205a0bb368cf79e41d4760f48c9d16ccd8351ac5e97e0a6825eac6acbe662007c4");
199    }
200}