Skip to content

Commit

Permalink
Update app instruction handling and UI.
Browse files Browse the repository at this point in the history
  • Loading branch information
agrojean-ledger committed Nov 7, 2023
1 parent 720b20d commit 8a9f430
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 62 deletions.
47 changes: 47 additions & 0 deletions src/app_ui/address.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*****************************************************************************
* 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::utils;
use core::str::from_utf8;
use nanos_sdk::io;
use nanos_ui::bitmaps::{CROSSMARK, EYE, VALIDATE_14};
use nanos_ui::ui::{Field, MultiFieldReview};

pub fn ui_display_pk(pk: &[u8]) -> Result<bool, io::Reply> {
// Todo add error handling
// ======================
let hex = utils::to_hex(pk).unwrap();
let m = from_utf8(&hex).unwrap();
// ======================

let my_field = [Field {
name: "Public Key",
value: m[..pk.len() * 2].as_ref(),
}];

let my_review = MultiFieldReview::new(
&my_field,
&["Confirm Address"],
Some(&EYE),
"Approve",
Some(&VALIDATE_14),
"Reject",
Some(&CROSSMARK),
);

Ok(my_review.show())
}
2 changes: 1 addition & 1 deletion src/app_ui/menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pub fn ui_menu_main(comm: &mut io::Comm) -> io::Event<io::ApduHeader> {
// The from trait allows to create different styles of pages
// without having to use the new() function.
&Page::from((["Boilerplate", "is ready"], &APP_ICON)),
&Page::from((["Version", "2.0.0"], true)),
&Page::from((["Version", env!("CARGO_PKG_VERSION")], true)),
&Page::from(("About", &CERTIFICATE)),
&Page::from(("Quit", &DASHBOARD_X)),
];
Expand Down
81 changes: 81 additions & 0 deletions src/handlers/get_public_key.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*****************************************************************************
* 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::address::ui_display_pk;
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()?;

let path_len = read_bip32_path(data, &mut path)?;

let pk = Secp256k1::derive_from_path(&path[..path_len])
.public_key()
.map_err(|x| io::Reply(0x6eu16 | (x as u16 & 0xff)))?;

// Display public key on device if requested
if display {
testing::debug_print("showing public key\n");
if !ui_display_pk(&pk.pubkey)? {
testing::debug_print("denied\n");
return Err(io::Reply(SW_DENY));
}
}

comm.append(&[pk.pubkey.len() as u8]);
comm.append(&pk.pubkey);
// Rust SDK key derivation API does not return chaincode yet
// so we just append a dummy chaincode.
const CHAINCODE_LEN: usize = 32;
comm.append(&[CHAINCODE_LEN as u8]); // Dummy chaincode length
comm.append(&[0u8; CHAINCODE_LEN]); // Dummy chaincode

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)
}
39 changes: 39 additions & 0 deletions src/handlers/get_version.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*****************************************************************************
* 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 core::str::FromStr;
use nanos_sdk::io;

pub fn handler_get_version(comm: &mut io::Comm) -> Result<(), io::Reply> {
if let Some((major, minor, patch)) = parse_version_string(env!("CARGO_PKG_VERSION")) {
comm.append(&[major, minor, patch]);
Ok(())
} else {
Err(io::StatusWords::Unknown.into())
}
}

fn parse_version_string(input: &str) -> Option<(u8, u8, u8)> {
// Split the input string by '.'.
// Input should be of the form "major.minor.patch",
// where "major", "minor", and "patch" are integers.
let mut parts = input.split('.');
let major = u8::from_str(parts.next()?).ok()?;
let minor = u8::from_str(parts.next()?).ok()?;
let patch = u8::from_str(parts.next()?).ok()?;
Some((major, minor, patch))
}
113 changes: 55 additions & 58 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@

mod utils;
mod app_ui {
pub mod address;
pub mod menu;
}
mod handlers {
pub mod get_public_key;
pub mod get_version;
}

use core::str::from_utf8;
use nanos_sdk::buttons::ButtonEvent;
Expand All @@ -13,55 +18,19 @@ use nanos_sdk::io;
use nanos_sdk::io::SyscallError;
use nanos_ui::ui;

use nanos_ui::bitmaps::{EYE, VALIDATE_14, CROSSMARK};
use nanos_ui::bitmaps::{CROSSMARK, EYE, VALIDATE_14};

use app_ui::menu::ui_menu_main;
use handlers::{get_public_key::handler_get_public_key, get_version::handler_get_version};

nanos_sdk::set_panic!(nanos_sdk::exiting_panic);

pub const BIP32_PATH: [u32; 5] = nanos_sdk::ecc::make_bip32_path(b"m/44'/535348'/0'/0/0");

/// Display public key in two separate
/// message scrollers
fn show_pubkey() {
let pubkey = Secp256k1::derive_from_path(&BIP32_PATH).public_key();
match pubkey {
Ok(pk) => {
{
let hex0 = utils::to_hex(&pk.as_ref()[1..33]).unwrap();
let m = from_utf8(&hex0).unwrap();
ui::MessageScroller::new(m).event_loop();
}
{
let hex1 = utils::to_hex(&pk.as_ref()[33..65]).unwrap();
let m = from_utf8(&hex1).unwrap();
ui::MessageScroller::new(m).event_loop();
}
}
Err(_) => ui::popup("Error"),
}
}

/// Basic nested menu. Will be subject
/// to simplifications in the future.
#[allow(clippy::needless_borrow)]
fn menu_example() {
loop {
match ui::Menu::new(&[&"PubKey", &"Infos", &"Back", &"Exit App"]).show() {
0 => show_pubkey(),
1 => loop {
match ui::Menu::new(&[&"Copyright", &"Authors", &"Back"]).show() {
0 => ui::popup("2020 Ledger"),
1 => ui::popup("???"),
_ => break,
}
},
2 => return,
3 => nanos_sdk::exit_app(0),
_ => (),
}
}
}
pub const SW_INS_NOT_SUPPORTED: u16 = 0x6D00;
pub const SW_DENY: u16 = 0x6985;
pub const SW_WRONG_P1P2: u16 = 0x6A86;
pub const SW_WRONG_DATA_LENGTH: u16 = 0x6A87;

/// This is the UI flow for signing, composed of a scroller
/// to read the incoming message, a panel that requests user
Expand All @@ -76,7 +45,7 @@ fn sign_ui(message: &[u8]) -> Result<Option<([u8; 72], u32, u32)>, SyscallError>

let my_review = ui::MultiFieldReview::new(
&my_field,
&["Review ","Transaction"],
&["Review ", "Transaction"],
Some(&EYE),
"Approve",
Some(&VALIDATE_14),
Expand Down Expand Up @@ -134,21 +103,25 @@ extern "C" fn sample_main() {
}

#[repr(u8)]

enum Ins {
GetVersion,
GetAppName,
GetPubkey,
Sign,
Menu,
Exit,
SignTx,
UnknownIns,
}

const CLA: u8 = 0xe0;

impl From<io::ApduHeader> for Ins {
fn from(header: io::ApduHeader) -> Ins {
match header.ins {
2 => Ins::GetPubkey,
3 => Ins::Sign,
4 => Ins::Menu,
0xff => Ins::Exit,
_ => panic!(),
3 => Ins::GetVersion,
4 => Ins::GetAppName,
5 => Ins::GetPubkey,
6 => Ins::SignTx,
_ => Ins::UnknownIns,
}
}
}
Expand All @@ -160,21 +133,45 @@ fn handle_apdu(comm: &mut io::Comm, ins: Ins) -> Result<(), Reply> {
return Err(io::StatusWords::NothingReceived.into());
}

let apdu_metadata = comm.get_apdu_metadata();

if apdu_metadata.cla != CLA {
return Err(io::StatusWords::BadCla.into());
}

match ins {
Ins::GetAppName => {
if apdu_metadata.p1 != 0 || apdu_metadata.p2 != 0 {
return Err(io::Reply(SW_WRONG_P1P2));
}
comm.append(env!("CARGO_PKG_NAME").as_bytes());
}
Ins::GetVersion => {
if apdu_metadata.p1 != 0 || apdu_metadata.p2 != 0 {
return Err(io::Reply(SW_WRONG_P1P2));
}
return handler_get_version(comm);
}
Ins::GetPubkey => {
let pk = Secp256k1::derive_from_path(&BIP32_PATH)
.public_key()
.map_err(|x| Reply(0x6eu16 | (x as u16 & 0xff)))?;
comm.append(pk.as_ref());
if apdu_metadata.p1 > 1 || apdu_metadata.p2 != 0 {
return Err(io::Reply(SW_WRONG_P1P2));
}

if (comm.get_data()?.len()) == 0 {
return Err(io::Reply(SW_WRONG_DATA_LENGTH));
}

return handler_get_public_key(comm, apdu_metadata.p1 == 1);
}
Ins::Sign => {
Ins::SignTx => {
let out = sign_ui(comm.get_data()?)?;
if let Some((signature_buf, length, _)) = out {
comm.append(&signature_buf[..length as usize])
}
}
Ins::Menu => menu_example(),
Ins::Exit => nanos_sdk::exit_app(0),
Ins::UnknownIns => {
return Err(io::Reply(SW_INS_NOT_SUPPORTED));
}
}
Ok(())
}
8 changes: 5 additions & 3 deletions src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use core::char;
use nanos_sdk::testing;

/// Convert to hex. Returns a static buffer of 64 bytes
#[inline]
pub fn to_hex(m: &[u8]) -> Result<[u8; 64], ()> {
if 2 * m.len() > 64 {
pub fn to_hex(m: &[u8]) -> Result<[u8; 255], ()> {
if 2 * m.len() > 255 {
testing::debug_print("to_hex: buffer too small\n");
return Err(());
}
let mut hex = [0u8; 64];
let mut hex = [0u8; 255];
let mut i = 0;
for c in m {
let c0 = char::from_digit((c >> 4).into(), 16).unwrap();
Expand Down

0 comments on commit 8a9f430

Please sign in to comment.