#[cfg(feature = "k256")]
use k256::ecdsa::signature::Signer as k256_Signer;
#[cfg(feature = "k256")]
use k256::ecdsa::{Signature, SigningKey};
#[cfg(feature = "secp256k1")]
use rust_bigint::traits::Converter;
#[cfg(feature = "secp256k1")]
use secp256k1::constants::{COMPACT_SIGNATURE_SIZE, MESSAGE_SIZE, SECRET_KEY_SIZE};
#[cfg(feature = "secp256k1")]
use secp256k1::{Message, SecretKey};
use nash_mpc::client::APIchildkey;
use nash_mpc::common::Curve;
#[cfg(feature = "secp256k1")]
use nash_mpc::curves::secp256_k1::get_context;
#[cfg(feature = "k256")]
use nash_mpc::curves::secp256_k1_rust::Secp256k1Scalar;
#[cfg(feature = "k256")]
use nash_mpc::curves::traits::ECScalar;
use nash_mpc::paillier_common;
use nash_mpc::rust_bigint::BigInt;
use crate::errors::{ProtocolError, Result};
use crate::protocol::RequestPayloadSignature;
use crate::types::ApiKeys;
use crate::types::Blockchain;
use crate::types::PublicKey;
#[cfg(feature = "secp256k1")]
use crate::utils::{der_encode_sig, hash_message};
use std::sync::atomic::{AtomicU32, Ordering};
pub fn chain_path(chain: Blockchain) -> &'static str {
match chain {
Blockchain::NEO => "m/44'/888'/0'/0/0",
Blockchain::Ethereum => "m/44'/60'/0'/0/0",
Blockchain::Bitcoin => "m/44'/0'/0'/0/0",
}
}
#[derive(Debug)]
pub struct Signer {
pub api_keys: ApiKeys,
k1_remaining: AtomicU32,
r1_remaining: AtomicU32,
}
impl Signer {
pub fn new(key_path: &str) -> Result<Self> {
Ok(Self {
api_keys: ApiKeys::new(key_path)?,
k1_remaining: AtomicU32::new(0),
r1_remaining: AtomicU32::new(0),
})
}
pub fn from_data(secret: &str, session: &str) -> Result<Self> {
Ok(Self {
api_keys: ApiKeys::from_data(secret, session)?,
k1_remaining: AtomicU32::new(0),
r1_remaining: AtomicU32::new(0),
})
}
/// Sign GraphQL payload request via payload signing key
/// The output is a hex string where signature has been DER encoded
/// Either implemented with k256 from rustcrypto (pure rust) or secp256k1 (better performance)
#[cfg(feature = "rustcrypto")]
pub fn sign_canonical_string(&self, request: &str) -> RequestPayloadSignature {
let signing_key: Secp256k1Scalar =
ECScalar::from(&self.api_keys.keys.payload_signing_key).expect("Invalid key");
let key = SigningKey::from_bytes(&signing_key.to_vec()).expect("invalid secret key");
let sig_pre: Signature = key.try_sign(request.as_bytes()).expect("signing failed");
let sig = sig_pre.to_der();
RequestPayloadSignature {
signed_digest: hex::encode(sig),
public_key: self.request_payload_public_key(),
}
}
#[cfg(feature = "secp256k1")]
pub fn sign_canonical_string(&self, request: &str) -> RequestPayloadSignature {
// create message hash
let message_hash = hash_message(request).to_bytes();
// add leading zeroes if necessary
let mut msg_vec = vec![0; MESSAGE_SIZE - message_hash.len()];
msg_vec.extend_from_slice(&message_hash);
let msg = Message::from_slice(&msg_vec).unwrap();
// SecretKey from BigInt
let vec = BigInt::to_vec(&self.api_keys.keys.payload_signing_key);
let mut v = vec![0; SECRET_KEY_SIZE - vec.len()];
v.extend(&vec);
let key = SecretKey::from_slice(&v).expect("invalid secret key");
// actual signature generation (and encoding)
let signature = get_context().sign(&msg, &key).serialize_compact();
let r = BigInt::from_bytes(&signature[0..COMPACT_SIGNATURE_SIZE / 2]);
let s = BigInt::from_bytes(&signature[COMPACT_SIGNATURE_SIZE / 2..COMPACT_SIGNATURE_SIZE]);
let sig = der_encode_sig(&r, &s);
RequestPayloadSignature {
signed_digest: hex::encode(sig),
public_key: self.request_payload_public_key(),
}
}
/// Sign data hashed to `BigInt` with the MPC child key for the given `Blockchain`
pub fn sign_child_key(
&self,
data: BigInt,
chain: Blockchain,
) -> Result<(BigInt, BigInt, String)> {
if self.get_remaining_r_vals(&chain) <= 0 {
return Err(ProtocolError("Ran out of R values"));
}
let key = self.get_child_key(chain);
let curve = match chain {
Blockchain::Ethereum | Blockchain::Bitcoin => Curve::Secp256k1,
Blockchain::NEO => Curve::Secp256r1,
};
// FIX ME: Right now the pools are under a global mutex. Make them managed
let (sig, r) = nash_mpc::client::compute_presig(&key, &data, curve)
.map_err(|_| ProtocolError("Error computing presignature"))?;
// Track the fact that we now have one less R value
self.decr_r_vals(chain);
Ok((sig, r, key.public_key))
}
/// Get public key for child key on `chain`
pub fn child_public_key(&self, chain: Blockchain) -> Result<PublicKey> {
PublicKey::new(chain, &self.get_child_key(chain).public_key)
}
/// Return public key for payload signing in format expected by the Nash backend service
/// BigInt conversion to hex will strip leading zeros, which Nash backend doesn't like
pub fn request_payload_public_key(&self) -> String {
let mut key_str = self.api_keys.keys.payload_public_key.to_str_radix(16);
if key_str.len() % 2 != 0 {
key_str = format!("0{}", &key_str);
}
key_str
}
pub fn paillier_pk(&self) -> &paillier_common::EncryptionKey {
&self.api_keys.keys.paillier_pk
}
pub fn get_address(&self, chain: Blockchain) -> &str {
&self.api_keys.keys.child_keys[chain_path(chain)].address
}
pub fn get_child_key(&self, chain: Blockchain) -> APIchildkey {
let key = &self.api_keys.keys.child_keys[chain_path(chain)];
// TODO: these should be unified! it was more convenient to parse the paillier_pk
// once for all the key data from deserialization, which is why I need this atm.
// is on list to fix once things are verified to be working
APIchildkey {
client_secret_share: key.client_secret_share.clone(),
paillier_pk: self.paillier_pk().clone(),
public_key: key.public_key.clone(),
server_secret_share_encrypted: key.server_secret_share_encrypted.clone(),
}
}
/// Get the current number of available R values for the given chain
pub fn get_remaining_r_vals(&self, chain: &Blockchain) -> u32 {
match chain {
Blockchain::Ethereum | Blockchain::Bitcoin => self.k1_remaining.load(Ordering::Acquire),
Blockchain::NEO => self.r1_remaining.load(Ordering::Acquire),
}
}
/// Call after filling R values to update tracking
pub fn fill_r_vals(&self, chain: Blockchain, n: u32) {
match chain {
Blockchain::Ethereum | Blockchain::Bitcoin => self.k1_remaining.fetch_add(n, Ordering::Release),
Blockchain::NEO => self.r1_remaining.fetch_add(n, Ordering::Release),
};
tracing::info!("filled {:?}: +{}", chain, n);
}
fn decr_r_vals(&self, chain: Blockchain) {
match chain {
Blockchain::Ethereum | Blockchain::Bitcoin => self.k1_remaining.fetch_sub(1, Ordering::Release),
Blockchain::NEO => self.r1_remaining.fetch_sub(1, Ordering::Release),
};
}
}
#[cfg(test)]
mod tests {
use super::Signer;
#[test]
fn test_signing() {
let base64_key = "eyJjaGlsZF9rZXlzIjp7fSwKICAgICAgICAicGFpbGxpZXJfcGsiOnsibiI6IjU5ODdlNjIyMjYxY2FmOTZlMjU4MjZjNzBjZjMyM2IyNjE5NGZmOWNmZTY5ZTNmNDBmMzBkMzA2NTcxNjQyY2FlYThhMzE0M2QxMWZmOTRjMTM4ODM2MDQ4NjczNTdhZThjMGU2NjNiZjAzZDAwOTMwMTZkN2Y0ZDc5MGFlMjRlMjkxNzgwM2Q4MTJiNjQxYWYyZDZjMDk1NzNkMTEyZWI3Njg2NDY1MjkxY2QxNDZmZDY2MmY3N2Y1OTVlZjgzMjc3YmUxNjgwZDA0MGIxZjNjNDk5YzgxOTE3NTcyMDZlNTEwYWU1NDcyNGQ2NjdmYzA0MWEyYzdjMmZmM2QzYjY2YzM3MjlkYzI1ZTAyYzQwMTllZDNhMDEyZmQ3NWVjMGUwMzk0OGNmNzgzYWQzOTAyY2U1ZTVlNzIyMjljM2RkM2ExNGI5MzRkNjAyNjlhY2I3YmEwYmQ0MTVkMmRlMTI4ZWYxODcyMjQwMGJhZWEyZTg1MGU2ZDFmZDg3ODdhMDEzMGQ1MTYyMDZkNzE4YTQ5ZDdhMjFkNDI4YjBmYTM3NzMwNzliNjQ4NjE4MTExOTFiNTUwMDFkNGMyYzI5ZjYzMDMxNGJlMTkxY2YzY2EzZjBmOGUwOWVlMDk1NDNmZmRkYTNmOTdjZjE2OWQ1MmUwNjdjZmQ0MGNiMzAzOTQxIn0sCiAgICAgICAgInBheWxvYWRfcHVibGljX2tleSI6IjA0NjE2NDZmZGM0NTQ0ZjEwMjk0ZTIwZTk5NGNlNTZkOGMwZmY4NTI1OTZlYjZiM2FhMGJhOWQ0YjIwNzlkODZkNDJiM2I1ZTg0OTFhNDhmZjZlMTYyMDczMjU3OTgwNzkxNmVlYjA3YmViNmY5OTcwZGM1OTUyYmQ0NDQ0MDRmNzQiLAogICAgICAgICJwYXlsb2FkX3NpZ25pbmdfa2V5IjoiYmI4YmNmNTJhNWY5NDRmMzUxYzViYzg1NmI3YTRjNDFhNWYzNzBmNWNlOTlkY2UwYzhkNmYxZDQ5MWNkMzRiZiIsCiAgICAgICAgInZlcnNpb24iOjB9";
let signer = Signer::from_data(&base64_key, "").unwrap();
let signature = signer.sign_canonical_string("hello, world!");
assert_eq!(signature.signed_digest, "30440220135a79b11caa321f1548d4b86e17c9b53525ffcdeab5e559d6cca310623cc45d02205a0bb368cf79e41d4760f48c9d16ccd8351ac5e97e0a6825eac6acbe662007c4");
}
}