Skip to content

Commit

Permalink
feat: Added support for blind signing with Ledger [requires updated L…
Browse files Browse the repository at this point in the history
…edger app that is not yet published] (near#259)
  • Loading branch information
dj8yfo authored Dec 2, 2023
1 parent 0f484b5 commit cc411f5
Show file tree
Hide file tree
Showing 11 changed files with 213 additions and 10 deletions.
7 changes: 5 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ thiserror = "1"
bytesize = "1.1.0"
prettytable = "0.10.0"

near-ledger = { version = "0.2.0", optional = true }
near-ledger = { version = "0.3.0", optional = true }

near-crypto = "0.17.0"
near-primitives = "0.17.0"
Expand Down
10 changes: 10 additions & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,16 @@ pub struct PrepopulatedTransaction {
pub actions: Vec<near_primitives::transaction::Action>,
}

impl From<near_primitives::transaction::Transaction> for PrepopulatedTransaction {
fn from(value: near_primitives::transaction::Transaction) -> Self {
Self {
signer_id: value.signer_id,
receiver_id: value.receiver_id,
actions: value.actions,
}
}
}

#[derive(Clone)]
pub struct ActionContext {
pub global_context: crate::GlobalContext,
Expand Down
6 changes: 6 additions & 0 deletions src/commands/transaction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
use strum::{EnumDiscriminants, EnumIter, EnumMessage};

pub mod construct_transaction;
mod print_transaction;
mod reconstruct_transaction;
mod send_meta_transaction;
mod send_signed_transaction;
Expand Down Expand Up @@ -41,6 +42,11 @@ pub enum TransactionActions {
))]
/// Sign previously prepared unsigned transaction
SignTransaction(self::sign_transaction::SignTransaction),
#[strum_discriminants(strum(
message = "print-transaction - Print all fields of previously prepared transaction without modification"
))]
/// Print previously prepared unsigned transaction without modification
PrintTransaction(self::print_transaction::PrintTransactionCommands),
#[strum_discriminants(strum(
message = "send-signed-transaction - Send a signed transaction"
))]
Expand Down
28 changes: 28 additions & 0 deletions src/commands/transaction/print_transaction/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#![allow(clippy::enum_variant_names, clippy::large_enum_variant)]
use strum::{EnumDiscriminants, EnumIter, EnumMessage};

mod signed;
mod unsigned;

#[derive(Debug, Clone, interactive_clap::InteractiveClap)]
#[interactive_clap(context = crate::GlobalContext)]
pub struct PrintTransactionCommands {
#[interactive_clap(subcommand)]
show_transaction_actions: PrintTransactionActions,
}

#[derive(Debug, EnumDiscriminants, Clone, interactive_clap::InteractiveClap)]
#[interactive_clap(context = crate::GlobalContext)]
#[strum_discriminants(derive(EnumMessage, EnumIter))]
pub enum PrintTransactionActions {
#[strum_discriminants(strum(
message = "unsigned - Print all fields of previously prepared unsigned transaction without modification"
))]
/// Print previously prepared unsigned transaction without modification
Unsigned(self::unsigned::PrintTransaction),
#[strum_discriminants(strum(
message = "signed - Print all fields of previously prepared signed transaction without modification"
))]
/// Send a signed transaction
Signed(self::signed::PrintTransaction),
}
26 changes: 26 additions & 0 deletions src/commands/transaction/print_transaction/signed/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#[derive(Debug, Clone, interactive_clap::InteractiveClap)]
#[interactive_clap(input_context = crate::GlobalContext)]
#[interactive_clap(output_context = PrintContext)]
pub struct PrintTransaction {
/// Enter the signed transaction encoded in base64:
signed_transaction: crate::types::signed_transaction::SignedTransactionAsBase64,
}

#[derive(Debug, Clone)]
pub struct PrintContext;

impl PrintContext {
pub fn from_previous_context(
_previous_context: crate::GlobalContext,
scope: &<PrintTransaction as interactive_clap::ToInteractiveClapContextScope>::InteractiveClapContextScope,
) -> color_eyre::eyre::Result<Self> {
let signed_transaction: near_primitives::transaction::SignedTransaction =
scope.signed_transaction.clone().into();

eprintln!("\nSigned transaction (full):\n");
crate::common::print_full_signed_transaction(signed_transaction);
eprintln!();

Ok(Self)
}
}
26 changes: 26 additions & 0 deletions src/commands/transaction/print_transaction/unsigned/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#[derive(Debug, Clone, interactive_clap::InteractiveClap)]
#[interactive_clap(input_context = crate::GlobalContext)]
#[interactive_clap(output_context = PrintContext)]
pub struct PrintTransaction {
/// Enter the unsigned transaction encoded in base64:
unsigned_transaction: crate::types::transaction::TransactionAsBase64,
}

#[derive(Debug, Clone)]
pub struct PrintContext;

impl PrintContext {
pub fn from_previous_context(
_previous_context: crate::GlobalContext,
scope: &<PrintTransaction as interactive_clap::ToInteractiveClapContextScope>::InteractiveClapContextScope,
) -> color_eyre::eyre::Result<Self> {
let unsigned_transaction: near_primitives::transaction::Transaction =
scope.unsigned_transaction.clone().into();

eprintln!("\nUnsigned transaction (full):\n");
crate::common::print_full_unsigned_transaction(unsigned_transaction);
eprintln!();

Ok(Self)
}
}
8 changes: 3 additions & 5 deletions src/commands/transaction/sign_transaction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,9 @@ impl SignTransactionContext {
scope.unsigned_transaction.clone().into();

move |_network_config| {
Ok(crate::commands::PrepopulatedTransaction {
signer_id: unsigned_transaction.signer_id.clone(),
receiver_id: unsigned_transaction.receiver_id.clone(),
actions: unsigned_transaction.actions.clone(),
})
Ok(crate::commands::PrepopulatedTransaction::from(
unsigned_transaction.clone(),
))
}
});

Expand Down
23 changes: 23 additions & 0 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::str::FromStr;

use color_eyre::eyre::WrapErr;
use futures::{StreamExt, TryStreamExt};
use near_primitives::borsh::BorshSerialize;
use prettytable::Table;

use near_primitives::{hash::CryptoHash, types::BlockReference, views::AccessKeyPermissionView};
Expand Down Expand Up @@ -517,6 +518,28 @@ pub fn generate_keypair() -> color_eyre::eyre::Result<KeyPairProperties> {
Ok(key_pair_properties)
}

pub fn print_full_signed_transaction(transaction: near_primitives::transaction::SignedTransaction) {
eprintln!("{:<25} {}\n", "signature:", transaction.signature);
crate::common::print_full_unsigned_transaction(transaction.transaction);
}

pub fn print_full_unsigned_transaction(transaction: near_primitives::transaction::Transaction) {
let bytes = transaction
.try_to_vec()
.expect("Transaction is not expected to fail on serialization");
eprintln!(
"Unsigned transaction hash (Base58-encoded SHA-256 hash): {}\n\n",
CryptoHash::hash_bytes(&bytes)
);

eprintln!("{:<13} {}", "public_key:", &transaction.public_key);
eprintln!("{:<13} {}", "nonce:", &transaction.nonce);
eprintln!("{:<13} {}", "block_hash:", &transaction.block_hash);

let prepopulated = crate::commands::PrepopulatedTransaction::from(transaction);
print_unsigned_transaction(&prepopulated);
}

pub fn print_unsigned_transaction(transaction: &crate::commands::PrepopulatedTransaction) {
eprintln!("{:<13} {}", "signer_id:", &transaction.signer_id);
eprintln!("{:<13} {}", "receiver_id:", &transaction.receiver_id);
Expand Down
81 changes: 79 additions & 2 deletions src/transaction_signature_options/sign_with_ledger/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use std::str::FromStr;

use color_eyre::eyre::{ContextCompat, WrapErr};
use inquire::{CustomType, Text};
use inquire::{CustomType, Select, Text};

use near_primitives::borsh::BorshSerialize;
use slip10::BIP32Path;

use crate::common::JsonRpcClientExt;
use crate::common::RpcQueryResponseExt;
Expand Down Expand Up @@ -42,8 +43,77 @@ pub struct SignLedgerContext {
on_after_sending_transaction_callback:
crate::transaction_signature_options::OnAfterSendingTransactionCallback,
}
const BLIND_SIGN_MEMO: &str = "Blind signature means that transaction is prepared by CLI, but cannot be reviewed on the Ledger device. \
In order to be absolutely sure that the transaction you are signing is not forged, take the constructed transaction, \
verify its content using NEAR CLI on another host or use any other tool capable of displaying unsigned NEAR transactions, \
and confirm that the SHA256 hash matches the one displayed above and another identical one, that will be displayed on your Ledger device after confirming the prompt. \
Following helper command on NEAR CLI can be used:";

impl SignLedgerContext {
fn input_blind_agree() -> color_eyre::eyre::Result<bool> {
let options: Vec<&str> = vec!["Yes", "No"];

Ok(
Select::new("Do you agree to continue with blind signature? ", options)
.prompt()
.map(|selected| selected == "Yes")?,
)
}

fn blind_sign_subflow(
hash: near_primitives::hash::CryptoHash,
hd_path: BIP32Path,
unsigned_transaction: near_primitives::transaction::Transaction,
) -> color_eyre::eyre::Result<near_crypto::Signature> {
eprintln!("\n\nBuffer overflow on Ledger device occured. Transaction is too large for normal signature.");
eprintln!("\nThe following is Base58-encoded SHA-256 hash of unsigned transaction:");
eprintln!("{}", hash);

eprintln!(
"\nUnsigned transaction (serialized as base64):\n{}\n",
crate::types::transaction::TransactionAsBase64::from(unsigned_transaction)
);
eprintln!("{}", BLIND_SIGN_MEMO);
eprintln!(
"$ {} transaction print-transaction unsigned\n\n",
crate::common::get_near_exec_path()
);

eprintln!("Make sure to enable blind sign in NEAR app's settings on Ledger device\n");
let agree = Self::input_blind_agree()?;
if agree {
eprintln!(
"Confirm transaction blind signing on your Ledger device (HD Path: {})",
hd_path,
);
let result = near_ledger::blind_sign_transaction(hash, hd_path);
let signature = result.map_err(|err| {
match err {
near_ledger::NEARLedgerError::BlindSignatureDisabled => {
color_eyre::Report::msg("Blind signature is disabled in NEAR app's settings on Ledger device".to_string())
},
near_ledger::NEARLedgerError::BlindSignatureNotSupported => {
color_eyre::Report::msg("Blind signature is not supported by the version of NEAR app installed on Ledger device. \
Version of the app with the feature available is tracked in https://github.com/LedgerHQ/app-near/pull/32".to_string())
},
err => {
color_eyre::Report::msg(format!(
"Error occurred while signing the transaction: {:?}",
err
))
}
}
})?;
let signature =
near_crypto::Signature::from_parts(near_crypto::KeyType::ED25519, &signature)
.expect("Signature is not expected to fail on deserialization");

Ok(signature)
} else {
Err(color_eyre::Report::msg("signing with ledger aborted"))
}
}

pub fn from_previous_context(
previous_context: crate::commands::TransactionContext,
scope: &<SignLedger as interactive_clap::ToInteractiveClapContextScope>::InteractiveClapContextScope,
Expand Down Expand Up @@ -101,12 +171,19 @@ impl SignLedgerContext {
unsigned_transaction
.try_to_vec()
.expect("Transaction is not expected to fail on serialization"),
seed_phrase_hd_path,
seed_phrase_hd_path.clone(),
) {
Ok(signature) => {
near_crypto::Signature::from_parts(near_crypto::KeyType::ED25519, &signature)
.expect("Signature is not expected to fail on deserialization")
}
Err(near_ledger::NEARLedgerError::BufferOverflow { transaction_hash }) => {
Self::blind_sign_subflow(
transaction_hash,
seed_phrase_hd_path,
unsigned_transaction.clone(),
)?
}
Err(near_ledger_error) => {
return Err(color_eyre::Report::msg(format!(
"Error occurred while signing the transaction: {:?}",
Expand Down
6 changes: 6 additions & 0 deletions src/types/signed_transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ pub struct SignedTransactionAsBase64 {
pub inner: near_primitives::transaction::SignedTransaction,
}

impl From<SignedTransactionAsBase64> for near_primitives::transaction::SignedTransaction {
fn from(transaction: SignedTransactionAsBase64) -> Self {
transaction.inner
}
}

impl std::str::FromStr for SignedTransactionAsBase64 {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Expand Down

0 comments on commit cc411f5

Please sign in to comment.