Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,8 @@ __pycache__/
# Virtual env for sideload (macOS and Windows)
ledger/
# Build directory
build/
build/src/main.rs.bak
src/main.rs.tmp
*.apdu
/tmp/speculos.log
proofs/
15 changes: 15 additions & 0 deletions apdu_echo.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -ne 1 ]]; then
echo "usage: $0 <32-byte-hex (64 chars)>"
exit 1
fi
HASH="$1"
if [[ ${#HASH} -ne 64 ]]; then
echo "error: expected 64 hex chars"; exit 2
fi
APDU="E010000020${HASH}"
curl -s -X POST http://localhost:6001/apdu \
-H 'Content-Type: application/json' \
-d "{\"data\":\"$APDU\"}"
echo
35 changes: 35 additions & 0 deletions dev.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
set -euo pipefail

# 1) Make/replace container and map host ports → container ports
docker rm -f ledger-dev 2>/dev/null || true
docker run --name ledger-dev --rm -d --privileged \
-v "$(pwd):/app" -w /app \
--publish 6001:15001 --publish 9998:19999 \
ghcr.io/ledgerhq/ledger-app-builder/ledger-app-dev-tools:latest

# 2) Build + (re)start Speculos inside the container
docker exec ledger-dev bash -lc '
set -e
cargo ledger build nanox
pkill -f speculos || true
nohup speculos --apdu-port 19999 --api-port 15001 --display headless \
--model nanox target/nanox/release/app-boilerplate-rust \
>/tmp/speculos.log 2>&1 &
'

# 3) Health check (expect 6d00)
echo "[health] expecting 6d00:"
curl -s -X POST http://localhost:6001/apdu \
-H "Content-Type: application/json" \
-d '{"data":"E001000000"}'
echo

# 4) Echo test (expect 64 'a' + 9000)
DATA="$(printf 'AA%.0s' {1..32})"
APDU="E010000020${DATA}"
echo "[echo test] expecting 64 'a' + 9000:"
curl -s -X POST http://localhost:6001/apdu \
-H "Content-Type: application/json" \
-d "{\"data\":\"$APDU\"}"
echo
45 changes: 45 additions & 0 deletions fix_echohash.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from pathlib import Path
import re

p = Path("src/main.rs")
s = p.read_text()

# a) Ensure INS mapping exists in the TryFrom<ApduHeader> (harmless if already there)
if "(INS_ECHO_HASH, 0, 0) => Ok(Instruction::EchoHash)" not in s:
s = re.sub(
r'\(\s*6\s*,[^)]*\)\s*=>\s*Ok\(Instruction::SignTx[^\)]*\)\s*,',
r'\g<0>\n (INS_ECHO_HASH, 0, 0) => Ok(Instruction::EchoHash),',
s
)

# b) Remove ANY existing EchoHash arms in handle_apdu
s = re.sub(r'\s*Instruction::EchoHash\s*=>\s*\{.*?\},', '', s, flags=re.S)

# c) Insert our single, borrow-safe EchoHash arm right after the SignTx arm in handle_apdu
echo_arm = r'''
Instruction::EchoHash => {
// Copy APDU body to a local stack buffer so the immutable borrow ends
let buf = {
let data = comm.get_data().map_err(|_| AppSW::WrongApduLength)?;
if data.len() != 32 { return Err(AppSW::WrongApduLength); }
let mut tmp = [0u8; 32];
tmp.copy_from_slice(data);
tmp
};
// Now we can mutably borrow comm
comm.append(&buf);
Ok(())
},
'''

# Find the SignTx arm and insert after it
s = re.sub(
r'(Instruction::SignTx\s*\{\s*chunk\s*:\s*\w+\s*,\s*more\s*:\s*\w+\s*\}\s*=>\s*handler_sign_tx\([^\)]*\)\s*,)',
r'\1' + echo_arm,
s,
count=1,
flags=re.S
)

p.write_text(s)
print("Patched src/main.rs")
75 changes: 31 additions & 44 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,7 @@
/*****************************************************************************
* 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.
* Apache-2.0
*****************************************************************************/

#![no_std]
Expand All @@ -29,9 +18,11 @@ mod handlers {
pub mod get_version;
pub mod sign_tx;
}

mod settings;

// New instruction: echo back a 32-byte payload (vault hash)
const INS_ECHO_HASH: u8 = 0x10;

use app_ui::menu::ui_menu_main;
use handlers::{
get_public_key::handler_get_public_key,
Expand Down Expand Up @@ -88,22 +79,13 @@ pub enum Instruction {
GetAppName,
GetPubkey { display: bool },
SignTx { chunk: u8, more: bool },
EchoHash, // NEW
}

impl TryFrom<ApduHeader> for Instruction {
type Error = AppSW;

/// APDU parsing logic.
///
/// Parses INS, P1 and P2 bytes to build an [`Instruction`]. P1 and P2 are translated to
/// strongly typed variables depending on the APDU instruction code. Invalid INS, P1 or P2
/// values result in errors with a status word, which are automatically sent to the host by the
/// SDK.
///
/// This design allows a clear separation of the APDU parsing logic and commands handling.
///
/// Note that CLA is not checked here. Instead the method [`Comm::set_expected_cla`] is used in
/// [`sample_main`] to have this verification automatically performed by the SDK.
fn try_from(value: ApduHeader) -> Result<Self, Self::Error> {
match (value.ins, value.p1, value.p2) {
(3, 0, 0) => Ok(Instruction::GetVersion),
Expand All @@ -112,14 +94,13 @@ impl TryFrom<ApduHeader> for Instruction {
display: value.p1 != 0,
}),
(6, P1_SIGN_TX_START, P2_SIGN_TX_MORE)
| (6, 1..=P1_SIGN_TX_MAX, P2_SIGN_TX_LAST | P2_SIGN_TX_MORE) => {
Ok(Instruction::SignTx {
chunk: value.p1,
more: value.p2 == P2_SIGN_TX_MORE,
})
}
| (6, 1..=P1_SIGN_TX_MAX, P2_SIGN_TX_LAST | P2_SIGN_TX_MORE) => Ok(Instruction::SignTx {
chunk: value.p1,
more: value.p2 == P2_SIGN_TX_MORE,
}),
(INS_ECHO_HASH, 0, 0) => Ok(Instruction::EchoHash), // NEW
(3..=6, _, _) => Err(AppSW::WrongP1P2),
(_, _, _) => Err(AppSW::InsNotSupported),
_ => Err(AppSW::InsNotSupported),
}
}
}
Expand All @@ -132,39 +113,32 @@ fn show_status_and_home_if_needed(ins: &Instruction, tx_ctx: &mut TxContext, sta
(Instruction::SignTx { .. }, AppSW::Deny | AppSW::Ok) if tx_ctx.finished() => {
(true, StatusType::Transaction)
}
(_, _) => (false, StatusType::Transaction),
_ => (false, StatusType::Transaction),
};

if show_status {
let success = *status == AppSW::Ok;
NbglReviewStatus::new()
.status_type(status_type)
.show(success);

// call home.show_and_return() to show home and setting screen
NbglReviewStatus::new().status_type(status_type).show(success);
tx_ctx.home.show_and_return();
}
}

#[no_mangle]
extern "C" fn sample_main() {
// Create the communication manager, and configure it to accept only APDU from the 0xe0 class.
// If any APDU with a wrong class value is received, comm will respond automatically with
// BadCla status word.
// Accept only CLA 0xE0.
let mut comm = Comm::new().set_expected_cla(0xe0);

let mut tx_ctx = TxContext::new();

// Initialize reference to Comm instance for NBGL
// API calls.
// Init NBGL / UI
init_comm(&mut comm);
tx_ctx.home = ui_menu_main(&mut comm);
tx_ctx.home.show_and_return();

loop {
let ins: Instruction = comm.next_command();

let _status = match handle_apdu(&mut comm, &ins, &mut tx_ctx) {
let status = match handle_apdu(&mut comm, &ins, &mut tx_ctx) {
Ok(()) => {
comm.reply_ok();
AppSW::Ok
Expand All @@ -174,18 +148,31 @@ extern "C" fn sample_main() {
sw
}
};
show_status_and_home_if_needed(&ins, &mut tx_ctx, &_status);
show_status_and_home_if_needed(&ins, &mut tx_ctx, &status);
}
}

fn handle_apdu(comm: &mut Comm, ins: &Instruction, ctx: &mut TxContext) -> Result<(), AppSW> {
match ins {
Instruction::GetAppName => {
Instruction::GetAppName => {
comm.append(env!("CARGO_PKG_NAME").as_bytes());
Ok(())
}
Instruction::GetVersion => handler_get_version(comm),
Instruction::GetPubkey { display } => handler_get_public_key(comm, *display),
Instruction::SignTx { chunk, more } => handler_sign_tx(comm, *chunk, *more, ctx),

// NEW: Echo back exactly 32 bytes from the APDU body.
Instruction::EchoHash => {
// Echo back exactly 32 bytes from APDU body without overlapping borrows.
let mut buf = [0u8; 32];
{
let d = comm.get_data().map_err(|_| AppSW::WrongApduLength)?;
if d.len() != 32 { return Err(AppSW::WrongApduLength); }
buf.copy_from_slice(d);
}
comm.append(&buf);
Ok(())
},
}
}
Loading