From d00299f32158a34398943aa2bf77161a71666a37 Mon Sep 17 00:00:00 2001 From: akorchyn Date: Fri, 20 Dec 2024 18:08:33 +0200 Subject: [PATCH] feat: initial implementation of NEP-413 support --- src/errors.rs | 2 + src/signer/access_keyfile_signer.rs | 59 ---------- src/signer/keystore.rs | 39 ++----- src/signer/ledger.rs | 42 ++++++-- src/signer/mod.rs | 161 +++++++++++++++++++++------- src/signer/secret_key.rs | 34 ++---- 6 files changed, 182 insertions(+), 155 deletions(-) delete mode 100644 src/signer/access_keyfile_signer.rs diff --git a/src/errors.rs b/src/errors.rs index c743855..8c71280 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -48,6 +48,8 @@ pub enum SignerError { SecretKeyIsNotAvailable, #[error("Failed to fetch nonce: {0}")] FetchNonceError(#[from] QueryError), + #[error("IO error: {0}")] + IO(#[from] std::io::Error), #[cfg(feature = "ledger")] #[error(transparent)] diff --git a/src/signer/access_keyfile_signer.rs b/src/signer/access_keyfile_signer.rs deleted file mode 100644 index b618873..0000000 --- a/src/signer/access_keyfile_signer.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::path::PathBuf; - -use near_crypto::{PublicKey, SecretKey}; -use near_primitives::{transaction::Transaction, types::Nonce}; -use tracing::{debug, instrument, trace}; - -use super::{AccountKeyPair, SignerTrait}; -use crate::{ - errors::{AccessKeyFileError, SignerError}, - types::{transactions::PrepopulateTransaction, CryptoHash}, -}; - -const ACCESS_KEYFILE_SIGNER_TARGET: &str = "near_api::signer::access_keyfile"; - -#[derive(Debug, Clone)] -pub struct AccessKeyFileSigner { - keypair: AccountKeyPair, -} - -impl AccessKeyFileSigner { - #[instrument(skip(path), fields(path = %path.display()))] - pub fn new(path: PathBuf) -> Result { - let keypair = AccountKeyPair::load_access_key_file(&path)?; - debug!(target: ACCESS_KEYFILE_SIGNER_TARGET, "Access key file loaded successfully"); - - Ok(Self { keypair }) - } -} - -#[async_trait::async_trait] -impl SignerTrait for AccessKeyFileSigner { - #[instrument(skip(self, tr), fields(signer_id = %tr.signer_id, receiver_id = %tr.receiver_id))] - fn tx_and_secret( - &self, - tr: PrepopulateTransaction, - public_key: PublicKey, - nonce: Nonce, - block_hash: CryptoHash, - ) -> Result<(Transaction, SecretKey), SignerError> { - debug!(target: ACCESS_KEYFILE_SIGNER_TARGET, "Creating transaction"); - let mut transaction = Transaction::new_v0( - tr.signer_id.clone(), - public_key, - tr.receiver_id, - nonce, - block_hash.into(), - ); - *transaction.actions_mut() = tr.actions; - - trace!(target: ACCESS_KEYFILE_SIGNER_TARGET, "Transaction created, returning with secret key"); - Ok((transaction, self.keypair.private_key.to_owned())) - } - - #[instrument(skip(self))] - fn get_public_key(&self) -> Result { - debug!(target: ACCESS_KEYFILE_SIGNER_TARGET, "Retrieving public key"); - Ok(self.keypair.public_key.clone()) - } -} diff --git a/src/signer/keystore.rs b/src/signer/keystore.rs index 9a702d8..50f454f 100644 --- a/src/signer/keystore.rs +++ b/src/signer/keystore.rs @@ -1,15 +1,10 @@ use near_crypto::{PublicKey, SecretKey}; -use near_primitives::{ - transaction::Transaction, - types::{AccountId, Nonce}, - views::AccessKeyPermissionView, -}; +use near_primitives::{types::AccountId, views::AccessKeyPermissionView}; use tracing::{debug, info, instrument, trace, warn}; use crate::{ config::NetworkConfig, errors::{KeyStoreError, SignerError}, - types::{transactions::PrepopulateTransaction, CryptoHash}, }; use super::{AccountKeyPair, SignerTrait}; @@ -23,38 +18,26 @@ pub struct KeystoreSigner { #[async_trait::async_trait] impl SignerTrait for KeystoreSigner { - #[instrument(skip(self, tr), fields(signer_id = %tr.signer_id, receiver_id = %tr.receiver_id))] - fn tx_and_secret( + #[instrument(skip(self))] + fn secret( &self, - tr: PrepopulateTransaction, - public_key: PublicKey, - nonce: Nonce, - block_hash: CryptoHash, - ) -> Result<(Transaction, SecretKey), SignerError> { + signer_id: &AccountId, + public_key: &PublicKey, + ) -> Result { debug!(target: KEYSTORE_SIGNER_TARGET, "Searching for matching public key"); self.potential_pubkeys .iter() - .find(|key| *key == &public_key) + .find(|key| *key == public_key) .ok_or(SignerError::PublicKeyIsNotAvailable)?; info!(target: KEYSTORE_SIGNER_TARGET, "Retrieving secret key"); // TODO: fix this. Well the search is a bit suboptimal, but it's not a big deal for now - let secret = Self::get_secret_key(&tr.signer_id, &public_key, "mainnet") - .or_else(|_| Self::get_secret_key(&tr.signer_id, &public_key, "testnet")) + let secret = Self::get_secret_key(signer_id, public_key, "mainnet") + .or_else(|_| Self::get_secret_key(signer_id, public_key, "testnet")) .map_err(|_| SignerError::SecretKeyIsNotAvailable)?; - debug!(target: KEYSTORE_SIGNER_TARGET, "Creating transaction"); - let mut transaction = Transaction::new_v0( - tr.signer_id.clone(), - public_key, - tr.receiver_id, - nonce, - block_hash.into(), - ); - *transaction.actions_mut() = tr.actions; - - info!(target: KEYSTORE_SIGNER_TARGET, "Transaction and secret key prepared successfully"); - Ok((transaction, secret.private_key)) + info!(target: KEYSTORE_SIGNER_TARGET, "Secret key prepared successfully"); + Ok(secret.private_key) } #[instrument(skip(self))] diff --git a/src/signer/ledger.rs b/src/signer/ledger.rs index d540b1e..84caf5a 100644 --- a/src/signer/ledger.rs +++ b/src/signer/ledger.rs @@ -10,7 +10,7 @@ use crate::{ types::{transactions::PrepopulateTransaction, CryptoHash}, }; -use super::SignerTrait; +use super::{NEP413Payload, SignerTrait}; const LEDGER_SIGNER_TARGET: &str = "near_api::signer::ledger"; @@ -130,13 +130,43 @@ impl SignerTrait for LedgerSigner { }) } - fn tx_and_secret( + #[instrument(skip(self), fields(receiver_id = %payload.recipient, message = %payload.message))] + async fn sign_message_nep413( &self, - _tr: PrepopulateTransaction, + _signer_id: crate::AccountId, _public_key: PublicKey, - _nonce: Nonce, - _block_hash: CryptoHash, - ) -> Result<(Transaction, SecretKey), SignerError> { + payload: NEP413Payload, + ) -> Result { + info!(target: LEDGER_SIGNER_TARGET, "Signing NEP413 message with Ledger"); + let hd_path = self.hd_path.clone(); + let payload = payload.into(); + + let signature = tokio::task::spawn_blocking(move || { + let signature = + near_ledger::sign_message_nep413(&payload, hd_path).map_err(LedgerError::from)?; + + Ok::<_, LedgerError>(signature) + }) + .await + .map_err(LedgerError::from) + .map_err(SignerError::from)?; + + let signature = signature.map_err(SignerError::from)?; + + debug!(target: LEDGER_SIGNER_TARGET, "Creating Signature object for NEP413"); + let signature = + near_crypto::Signature::from_parts(near_crypto::KeyType::ED25519, &signature) + .map_err(LedgerError::from) + .map_err(SignerError::from)?; + + Ok(signature) + } + + fn secret( + &self, + _signer_id: &crate::AccountId, + _public_key: &PublicKey, + ) -> Result { warn!(target: LEDGER_SIGNER_TARGET, "Attempted to access secret key, which is not available for Ledger signer"); Err(SignerError::SecretKeyIsNotAvailable) } diff --git a/src/signer/mod.rs b/src/signer/mod.rs index c617585..31c9f9c 100644 --- a/src/signer/mod.rs +++ b/src/signer/mod.rs @@ -119,13 +119,15 @@ use std::{ }, }; -use near_crypto::{ED25519SecretKey, PublicKey, SecretKey}; +use borsh::{BorshDeserialize, BorshSerialize}; +use near_crypto::{ED25519SecretKey, PublicKey, SecretKey, Signature}; use near_primitives::{ action::delegate::SignedDelegateAction, transaction::{SignedTransaction, Transaction}, types::{AccountId, BlockHeight, Nonce}, }; -use serde::Deserialize; +use openssl::sha::sha256; +use serde::{Deserialize, Serialize}; use slipped10::BIP32Path; use tracing::{debug, info, instrument, trace, warn}; @@ -135,9 +137,8 @@ use crate::{ types::{transactions::PrepopulateTransaction, CryptoHash}, }; -use self::{access_keyfile_signer::AccessKeyFileSigner, secret_key::SecretKeySigner}; +use secret_key::SecretKeySigner; -pub mod access_keyfile_signer; #[cfg(feature = "keystore")] pub mod keystore; #[cfg(feature = "ledger")] @@ -152,7 +153,7 @@ pub const DEFAULT_WORD_COUNT: usize = 12; /// A struct representing a pair of public and private keys for an account. /// This might be useful for getting keys from a file. E.g. `~/.near-credentials`. -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct AccountKeyPair { pub public_key: near_crypto::PublicKey, pub private_key: near_crypto::SecretKey, @@ -165,6 +166,32 @@ impl AccountKeyPair { } } +/// [NEP413](https://github.com/near/NEPs/blob/master/neps/nep-0413.md) payload +/// Input for NEP413 message signing +#[derive(Debug, Clone, Serialize, Deserialize, BorshDeserialize, BorshSerialize)] +pub struct NEP413Payload { + /// The message that wants to be transmitted. + pub message: String, + /// The recipient to whom the message is destined (e.g. "alice.near" or "myapp.com"). + pub recipient: String, + /// A nonce that uniquely identifies this instance of the message, denoted as a 32 bytes array. + pub nonce: [u8; 32], + /// A callback URL that will be called with the signed message as a query parameter. + pub callback_url: Option, +} + +#[cfg(feature = "ledger")] +impl From for near_ledger::NEP413Payload { + fn from(payload: NEP413Payload) -> Self { + Self { + messsage: payload.message, + nonce: payload.nonce, + recipient: payload.recipient, + callback_url: payload.callback_url, + } + } +} + /// A trait for implementing custom signing logic. /// /// This trait provides the core functionality needed to sign transactions and delegate actions. @@ -184,22 +211,12 @@ impl AccountKeyPair { /// /// #[async_trait::async_trait] /// impl SignerTrait for CustomSigner { -/// fn tx_and_secret( +/// fn secret( /// &self, -/// tr: PrepopulateTransaction, -/// public_key: PublicKey, -/// nonce: u64, -/// block_hash: CryptoHash, -/// ) -> Result<(Transaction, SecretKey), SignerError> { -/// let mut transaction = Transaction::new_v0( -/// tr.signer_id.clone(), -/// public_key, -/// tr.receiver_id, -/// nonce, -/// block_hash.into(), -/// ); -/// *transaction.actions_mut() = tr.actions; -/// Ok((transaction, self.secret_key.clone())) +/// _signer_id: &AccountId, +/// _public_key: &PublicKey +/// ) -> Result { +/// Ok(self.secret_key.clone()) /// } /// /// fn get_public_key(&self) -> Result { @@ -250,8 +267,15 @@ pub trait SignerTrait { block_hash: CryptoHash, max_block_height: BlockHeight, ) -> Result { - let (unsigned_transaction, signer_secret_key) = - self.tx_and_secret(tr, public_key, nonce, block_hash)?; + let signer_secret_key = self.secret(&tr.signer_id, &public_key)?; + let mut unsigned_transaction = Transaction::new_v0( + tr.signer_id.clone(), + public_key, + tr.receiver_id, + nonce, + block_hash.into(), + ); + *unsigned_transaction.actions_mut() = tr.actions; get_signed_delegate_action(unsigned_transaction, signer_secret_key, max_block_height) } @@ -269,26 +293,51 @@ pub trait SignerTrait { nonce: Nonce, block_hash: CryptoHash, ) -> Result { - let (unsigned_transaction, signer_secret_key) = - self.tx_and_secret(tr, public_key, nonce, block_hash)?; + let signer_secret_key = self.secret(&tr.signer_id, &public_key)?; + let mut unsigned_transaction = Transaction::new_v0( + tr.signer_id.clone(), + public_key, + tr.receiver_id, + nonce, + block_hash.into(), + ); + *unsigned_transaction.actions_mut() = tr.actions; + let signature = signer_secret_key.sign(unsigned_transaction.get_hash_and_size().0.as_ref()); Ok(SignedTransaction::new(signature, unsigned_transaction)) } - /// Creates an unsigned transaction and returns it along with the secret key. - /// This is a `helper` method that should be implemented by the signer or fail with SignerError. - /// As long as this method works, the default implementation of the [sign_meta](`SignerTrait::sign_meta`) and [sign](`SignerTrait::sign`) methods should work. + /// Signs a [NEP413](https://github.com/near/NEPs/blob/master/neps/nep-0413.md) message. /// - /// If you can't provide a SecretKey for some reason (E.g. `Ledger``), - /// you can fail with SignerError and override `sign_meta` and `sign` methods. - fn tx_and_secret( + /// This method is used for NEP413 messages. It creates a signature that can be used to authenticate access to an account. + /// + /// The default implementation should work for most cases. + async fn sign_message_nep413( &self, - tr: PrepopulateTransaction, + signer_id: AccountId, public_key: PublicKey, - nonce: Nonce, - block_hash: CryptoHash, - ) -> Result<(Transaction, SecretKey), SignerError>; + payload: NEP413Payload, + ) -> Result { + let mut bytes = borsh::to_vec(&2147484061u32)?; + bytes.extend(borsh::to_vec(&payload)?); + let hash = sha256(&bytes); + let secret = self.secret(&signer_id, &public_key)?; + let signature = secret.sign(hash.as_ref()); + Ok(signature) + } + + /// Returns the secret key associated with this signer. + /// This is a `helper` method that should be implemented by the signer or fail with [`SignerError`]. + /// As long as this method works, the default implementation of the [sign_meta](`SignerTrait::sign_meta`) and [sign](`SignerTrait::sign`) methods should work. + /// + /// If you can't provide a [`SecretKey`] for some reason (E.g. `Ledger``), + /// you can fail with SignerError and override `sign_meta` and `sign`, `sign_message_nep413` methods. + fn secret( + &self, + signer_id: &AccountId, + public_key: &PublicKey, + ) -> Result; /// Returns the public key associated with this signer. /// @@ -406,9 +455,12 @@ impl Signer { Ok(SecretKeySigner::new(secret_key)) } - /// Creates a [AccessKeyFileSigner](`AccessKeyFileSigner`) using a path to the access key file. - pub fn from_access_keyfile(path: PathBuf) -> Result { - AccessKeyFileSigner::new(path) + /// Creates a [SecretKeySigner](`secret_key::SecretKeySigner`) using a path to the access key file. + pub fn from_access_keyfile(path: PathBuf) -> Result { + let keypair = AccountKeyPair::load_access_key_file(&path)?; + debug!(target: SIGNER_TARGET, "Access key file loaded successfully"); + + Ok(SecretKeySigner::new(keypair.private_key)) } /// Creates a [LedgerSigner](`ledger::LedgerSigner`) using default HD path. @@ -625,3 +677,38 @@ pub fn generate_secret_key_from_seed_phrase(seed_phrase: String) -> Result Result<(Transaction, SecretKey), SignerError> { - debug!(target: SECRET_KEY_SIGNER_TARGET, "Creating transaction"); - let mut transaction = Transaction::new_v0( - tr.signer_id.clone(), - public_key, - tr.receiver_id, - nonce, - block_hash.into(), - ); - *transaction.actions_mut() = tr.actions; - - trace!(target: SECRET_KEY_SIGNER_TARGET, "Transaction created, returning with secret key"); - Ok((transaction, self.secret_key.clone())) + signer_id: &crate::AccountId, + public_key: &PublicKey, + ) -> Result { + trace!(target: SECRET_KEY_SIGNER_TARGET, "returning with secret key"); + Ok(self.secret_key.clone()) } #[instrument(skip(self))]