Skip to content

Commit

Permalink
Add transaction signing APDU handling and display.
Browse files Browse the repository at this point in the history
  • Loading branch information
agrojean-ledger committed Nov 10, 2023
1 parent 3b35d0d commit b16dc85
Show file tree
Hide file tree
Showing 6 changed files with 430 additions and 78 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ edition = "2021"
nanos_sdk = { git = "https://github.com/LedgerHQ/ledger-nanos-sdk.git" }
nanos_ui = { git = "https://github.com/LedgerHQ/ledger-nanos-ui.git" }
include_gif = { git = "https://github.com/LedgerHQ/sdk_include_gif" }
numtoa = "0.2.4"

[patch."https://github.com/LedgerHQ/ledger-nanos-ui"]
nanos_ui = { path = "ledger-nanos-ui" }
Expand Down
120 changes: 120 additions & 0 deletions src/app_ui/sign.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*****************************************************************************
* Ledger App Boilerplate Rust.
* (c) 2023 Ledger SAS.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*****************************************************************************/

use nanos_sdk::io;
use nanos_ui::bitmaps::{EYE, VALIDATE_14, CROSSMARK};
use nanos_ui::ui::{MultiFieldReview, Field};
use crate::handlers::sign_tx::Tx;
use crate::utils::{concatenate, to_hex};
use numtoa::NumToA;

use nanos_sdk::testing;

pub fn ui_display_tx(tx: &Tx) -> Result<bool, io::Reply> {
// Todo add error handling

testing::debug_print("Display Tx 1\n");

// Format amount value
let mut amount_buf = [0u8; 20];
let mut amount_with_denom_buf = [0u8; 25];
concatenate(&["CRAB", " ", tx.value.numtoa_str(10, &mut amount_buf)], &mut amount_with_denom_buf);

testing::debug_print("Display Tx 2\n");

let amount_str_with_denom = core::str::from_utf8(&amount_with_denom_buf).unwrap();

testing::debug_print("Display Tx 3\n");

// Format destination address
let hex_address = to_hex(&tx.to).unwrap();
let hex_address_str = core::str::from_utf8(&hex_address).unwrap();

testing::debug_print("Display Tx 4\n");

// Format memo
let memo_str = core::str::from_utf8(&tx.memo).unwrap();

testing::debug_print("Display Tx 5\n");

// Define transaction review fields
let my_fields = [
Field {
name: "Amount",
value: amount_str_with_denom,
},
Field {
name: "Destination",
value: hex_address_str,
},
Field {
name: "Memo",
value: memo_str,
},
];

testing::debug_print("Display Tx 6\n");

// Create transaction review
let my_review = MultiFieldReview::new(
&my_fields,
&["Review ", "Transaction"],
Some(&EYE),
"Approve",
Some(&VALIDATE_14),
"Reject",
Some(&CROSSMARK),
);

testing::debug_print("Display Tx 7\n");

Ok(my_review.show())
}

// This is the UI flow for signing, composed of a scroller
// to read the incoming message, a panel that requests user
// validation, and an exit message.
// fn sign_ui(message: &[u8]) -> Result<Option<([u8; 72], u32, u32)>, SyscallError> {
// let hex = utils::to_hex(message).map_err(|_| SyscallError::Overflow)?;
// let m = from_utf8(&hex).map_err(|_| SyscallError::InvalidParameter)?;
// let my_field = [ui::Field {
// name: "Data",
// value: m,
// }];

// let my_review = ui::MultiFieldReview::new(
// &my_field,
// &["Review ", "Transaction"],
// Some(&EYE),
// "Approve",
// Some(&VALIDATE_14),
// "Reject",
// Some(&CROSSMARK),
// );

// if my_review.show() {
// let signature = Secp256k1::derive_from_path(&BIP32_PATH)
// .deterministic_sign(message)
// .map_err(|_| SyscallError::Unspecified)?;
// ui::popup("Done !");
// Ok(Some(signature))
// } else {
// ui::popup("Cancelled");
// Ok(None)
// }
// }

31 changes: 1 addition & 30 deletions src/handlers/get_public_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,11 @@
*****************************************************************************/

use crate::app_ui::address::ui_display_pk;
use crate::utils::{read_bip32_path, MAX_ALLOWED_PATH_LEN};
use crate::SW_DENY;
use nanos_sdk::ecc::{Secp256k1, SeedDerive};
use nanos_sdk::{io, testing};

const MAX_ALLOWED_PATH_LEN: usize = 10;

// const SW_DENY: u16 = 0x6985;

pub fn handler_get_public_key(comm: &mut io::Comm, display: bool) -> Result<(), io::Reply> {
let mut path = [0u32; MAX_ALLOWED_PATH_LEN];
let data = comm.get_data()?;
Expand Down Expand Up @@ -53,29 +50,3 @@ pub fn handler_get_public_key(comm: &mut io::Comm, display: bool) -> Result<(),

Ok(())
}

fn read_bip32_path(data: &[u8], path: &mut [u32]) -> Result<usize, io::Reply> {
// Check input length and path buffer capacity
if data.len() < 1 || path.len() < data.len() / 4 {
return Err(io::StatusWords::BadLen.into());
}

let path_len = data[0] as usize; // First byte is the length of the path
let path_data = &data[1..];

// Check path data length and alignment
if path_data.len() != path_len * 4
|| path_data.len() > MAX_ALLOWED_PATH_LEN * 4
|| path_data.len() % 4 != 0
{
return Err(io::StatusWords::BadLen.into());
}

let mut idx = 0;
for (i, chunk) in path_data.chunks(4).enumerate() {
path[idx] = u32::from_be_bytes(chunk.try_into().unwrap());
idx = i + 1;
}

Ok(idx)
}
220 changes: 220 additions & 0 deletions src/handlers/sign_tx.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
/*****************************************************************************
* Ledger App Boilerplate Rust.
* (c) 2023 Ledger SAS.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*****************************************************************************/
use crate::app_ui::sign::ui_display_tx;
use crate::utils::{read_bip32_path, MAX_ALLOWED_PATH_LEN};
use crate::{SW_DENY, SW_TX_PARSING_FAIL, SW_WRONG_TX_LENGTH};
// use nanos_sdk::ecc::{Secp256k1, SeedDerive};
use nanos_sdk::{io, testing};

const MAX_TRANSACTION_LEN: usize = 510;
const MAX_DER_SIG_LEN: usize = 72;

pub struct Tx {
nonce: u64,
pub value: u64,
pub to: [u8; 20],
pub memo: [u8; 255],
pub memo_len: usize,
}

// Implement deserialize for Tx with TryFrom u8 array
impl TryFrom<&[u8]> for Tx {
type Error = ();
fn try_from(raw_tx: &[u8]) -> Result<Self, Self::Error> {
if raw_tx.len() > MAX_TRANSACTION_LEN {
return Err(());
}

testing::debug_print("Parsing 1\n");

// Try to parse the transaction fields :
// Nonce
let nonce_bytes = match raw_tx.get(..8) {
Some(value) => value,
None => return Err(()),
};
let nonce = u64::from_be_bytes(nonce_bytes.try_into().unwrap());

testing::debug_print("Parsing 2\n");

// Amount value
let value_bytes = match raw_tx.get(8..16) {
Some(value) => value,
None => return Err(()),
};
let value = u64::from_be_bytes(value_bytes.try_into().unwrap());

testing::debug_print("Parsing 3\n");

// To address
let to: [u8;20] = match raw_tx.get(16..36) {
Some(value) => value.try_into().unwrap(),
None => return Err(()),
};

testing::debug_print("Parsing 4\n");

// Memo length
let memo_len: usize = match raw_tx.get(36) {
Some(value) => *value as usize,
None => return Err(()),
};

testing::debug_print("Parsing 5\n");

// Memo
let mut memo: [u8; 255] = [0u8; 255];
match raw_tx.get(37..(37 + memo_len) as usize) {
Some(value) => memo[0..memo_len].copy_from_slice(value),
None => return Err(()),
};

testing::debug_print("Parsing 6\n");

// Check memo ASCII encoding
if !memo.iter().all(|&byte| byte.is_ascii()) {
return Err(());
}

testing::debug_print("Parsing 7\n");

Ok(Tx {
nonce,
value,
to,
memo,
memo_len,
})
}
}

// Implement constructor for Tx with default values
impl Tx {
fn new() -> Tx {
Tx {
nonce: 0,
value: 0,
to: [0u8; 20],
memo: [0u8; 255],
memo_len: 0,
}
}
}

// #[derive(Copy, Clone)]
pub struct TxInfo {
raw_tx: [u8; MAX_TRANSACTION_LEN], // raw transaction serialized
raw_tx_len: usize, // length of raw transaction
transaction: Tx, // structured transaction
m_hash: [u8; 32], // message hash digest
signature: [u8; MAX_DER_SIG_LEN], // transaction signature encoded in DER
signature_len: usize, // length of transaction signature
v: u8, // parity of y-coordinate of R in ECDSA signature
}

// Implement constructor for TxInfo with default values
impl TxInfo {
pub fn new() -> TxInfo {
TxInfo {
raw_tx: [0u8; MAX_TRANSACTION_LEN],
raw_tx_len: 0,
transaction: Tx::new(),
m_hash: [0u8; 32],
signature: [0u8; MAX_DER_SIG_LEN],
signature_len: 0,
v: 0,
}
}
// Implement reset for TxInfo
fn reset(&mut self) {
self.raw_tx_len = 0;
self.transaction = Tx::new();
self.m_hash = [0u8; 32];
self.signature = [0u8; MAX_DER_SIG_LEN];
self.signature_len = 0;
self.v = 0;
}
}

pub fn handler_sign_tx(
comm: &mut io::Comm,
chunk: u8,
more: bool,
txinfo: &mut TxInfo,
) -> Result<(), io::Reply> {
// Try to get data from comm. If there is no data,
// the '?' operator will propagate the error.
let data = comm.get_data()?;
// First chunk, try to parse the path
if chunk == 0 {
testing::debug_print("First transaction chunk\n");
// Reset txinfo
txinfo.reset();
let mut path = [0u32; MAX_ALLOWED_PATH_LEN];
// This will propagate the error if the path is invalid
read_bip32_path(data, &mut path)?;
// Next chunks, append data to raw_tx and return or parse
// the transaction if it is the last chunk.
} else {
if txinfo.raw_tx_len + data.len() > MAX_TRANSACTION_LEN {
return Err(io::Reply(SW_WRONG_TX_LENGTH));
}

testing::debug_print("Next transaction chunks\n");

// Append data to raw_tx
txinfo.raw_tx[txinfo.raw_tx_len..txinfo.raw_tx_len + data.len()].copy_from_slice(data);
txinfo.raw_tx_len += data.len();

// If we expect more chunks, return
if more {
testing::debug_print("More chunks expected : return\n");
return Ok(());
// Otherwise, try to parse the transaction
} else {
testing::debug_print("Last chunk : parse transaction\n");
match Tx::try_from(&txinfo.raw_tx[..txinfo.raw_tx_len]) {
Ok(tx) => {
testing::debug_print("Transaction parsed\n");
txinfo.transaction = tx;
}
Err(_) => {
testing::debug_print("Transaction parsing failed\n");
return Err(io::Reply(SW_TX_PARSING_FAIL));
}
}

// Display transaction. If user approves
// the transaction, sign it. Otherwise,
// return an error.
if ui_display_tx(&txinfo.transaction)?
{
return Ok(())
}
else {
return Err(io::Reply(SW_DENY));
}

// let out = sign_ui(comm.get_data()?)?;
// if let Some((signature_buf, length, _)) = out
// {
// comm.append(&signature_buf[..length as usize])
// }
}
}
Ok(())
}
Loading

0 comments on commit b16dc85

Please sign in to comment.