use bech32::{self, ToBase32}; use clap::{App, Arg}; use rand::rngs::SmallRng; use rand::{RngCore, SeedableRng}; use std::io::prelude::*; use std::sync::Mutex; use std::str::FromStr; use bip39::Mnemonic; use std::collections::HashMap; use blake2::digest::{Update, VariableOutput}; use blake2::VarBlake2b; use iota_ledger_nano::LedgerBIP32Index; use iota_ledger_nano::api::constants::Protocol; use iota_ledger_nano::{APDUAnswer, APDUCommand}; use packable::Packable; /* use bee_block::address::{Address, Ed25519Address}; */ use bee_block::address::Address; use bee_block::address::Ed25519Address; use bee_block::input::{Input, UtxoInput}; use bee_block::output::{BasicOutput, BasicOutputBuilder, InputsCommitment, Output}; use bee_block::signature::Signature::Ed25519; use bee_block::signature::{Ed25519Signature, Signature}; use bee_block::unlock::SignatureUnlock; use bee_block::payload::transaction::TransactionEssence; use packable::PackableExt; use bee_block::output::unlock_condition::{AddressUnlockCondition, UnlockCondition}; use bee_block::unlock::{ReferenceUnlock, Unlock}; use bee_block::payload::transaction::{ RegularTransactionEssence, RegularTransactionEssenceBuilder, TransactionId, }; use crypto::signatures::ed25519; use std::error::Error; use std::sync::atomic::{AtomicBool, Ordering}; use std::fs::File; lazy_static! { static ref WRITER: Mutex = Mutex::new(WriterWrapper::default()); } enum FormatTypes { Json, Hex, Bin, } #[derive(Default)] struct WriterWrapper { writer: Option, format: Option, } impl WriterWrapper { pub fn open(&mut self, filename: String) -> std::io::Result<()> { self.writer = Some(File::create(filename)?); Ok(()) } pub fn write(&mut self, s: &str) { let file = self.writer.as_mut().unwrap(); writeln!(file, "{}", s).expect("error writing file"); } pub fn write_bin(&mut self, b: &[u8]) { let file = self.writer.as_mut().unwrap(); file.write_all(b).expect("error writing file"); } pub fn set_format_type(&mut self, format_type: FormatTypes) { self.format = Some(format_type); } } // deterministic tests are important for finding errors! const PRNG_SEED: [u8; 32] = [ 0xfc, 0x11, 0xb4, 0xdd, 0x28, 0x74, 0x1c, 0x15, 0xcc, 0xac, 0x4b, 0x26, 0xaf, 0x43, 0x06, 0x84, 0xc8, 0x04, 0x55, 0x56, 0x3a, 0xda, 0xea, 0x1d, 0x80, 0x21, 0xd9, 0xbf, 0x6e, 0x5b, 0x25, 0x22, ]; const DEFAULT_WORDS : &str = "glory promote mansion idle axis finger extra february uncover one trip resource lawn turtle enact monster seven myth punch hobby comfort wild raise skin"; const DEFAULT_SEED: [u8; 64] = [ 0xb1, 0x19, 0x97, 0xfa, 0xff, 0x42, 0x0a, 0x33, 0x1b, 0xb4, 0xa4, 0xff, 0xdc, 0x8b, 0xdc, 0x8b, 0xa7, 0xc0, 0x17, 0x32, 0xa9, 0x9a, 0x30, 0xd8, 0x3d, 0xbb, 0xeb, 0xd4, 0x69, 0x66, 0x6c, 0x84, 0xb4, 0x7d, 0x09, 0xd3, 0xf5, 0xf4, 0x72, 0xb3, 0xb9, 0x38, 0x4a, 0xc6, 0x34, 0xbe, 0xba, 0x2a, 0x44, 0x0b, 0xa3, 0x6e, 0xc7, 0x66, 0x11, 0x44, 0x13, 0x2f, 0x35, 0xe2, 0x06, 0x87, 0x35, 0x64, ]; const DEFAULT_KEY_DEBUG: &str = "171167b16cb8dcfa0b4f46e9bbb196cfbb2ee9b5ba7d9f19786ac6974ece46d1"; const DEFAULT_KEY_SMR: &str = "388d5c8d2092737ced931879db1a79248444084afdf968331f4e7ae290946a4a"; const DEFAULT_KEY_IOTA: &str = "f14f5bc7f78179df26fed411de31e6e1344f272597972bc975cedff700819d95"; // output-range to test address pooling const MAX_INPUT_RANGE: u32 = 10; //100; const MAX_OUTPUT_RANGE: u32 = 10; //100; const MAX_REMAINDER_RANGE: u32 = 100; const MAX_ACCOUNT_RANGE: u32 = 4; #[macro_use] extern crate lazy_static; lazy_static! { static ref HASHMAP: Mutex> = Mutex::new(HashMap::new()); } fn hex(bytes: &[u8]) -> String { let mut ret = String::new(); for b in bytes.iter() { ret.push_str(&format!("{:02x}", b)); } ret } lazy_static! { static ref DEBUG_APP: AtomicBool = AtomicBool::new(false); } const HARDENED: u32 = 0x80000000; const MAX_INPUTS: u16 = 126; const MAX_OUTPUTS: u16 = 126; // nano s const MAX_SUM_INPUTS_OUTPUTS: u16 = 15; use anyhow::Result; pub fn get_seed() -> [u8; 64] { // create a new randomly generated mnemonic phrase let mnemonic = match Mnemonic::parse(DEFAULT_WORDS) { Ok(b) => b, Err(e) => { panic!("e: {}", e); } }; // get the HD wallet seed mnemonic.to_seed("") } pub fn get_key( chain: u32, seed: &[u8], account: u32, index: LedgerBIP32Index, ) -> Result { let path = format!( "44'/{}'/{}'/{}'/{}'", chain, account & !0x80000000, index.bip32_change & !0x80000000, index.bip32_index & !0x80000000 ); let bip32_path = slip10::path::BIP32Path::from_str(&path)?; slip10::derive_key_from_path(seed, slip10::Curve::Ed25519, &bip32_path) } pub fn get_addr_from_pubkey(pubkey: [u8; 32]) -> [u8; 32] { let mut hasher = VarBlake2b::new(32).unwrap(); hasher.update(pubkey); let mut result: [u8; 32] = [0; 32]; hasher.finalize_variable(|res| { result[..32].clone_from_slice(&res[..32]); }); result } pub fn get_addr( chain: u32, seed: &[u8], account: u32, index: LedgerBIP32Index, ) -> Result<[u8; 32], slip10::Error> { let key = get_key(chain, seed, account, index)?; let pubkey = key.public_key(); let mut truncated = [0u8; 32]; truncated.clone_from_slice(&pubkey[1..33]); Ok(get_addr_from_pubkey(truncated)) } /// A record matching an Input with its address. #[derive(Debug, Clone)] pub struct InputIndexRecorder { /// the input pub input: Input, pub bech32: String, /// address index pub address_index: usize, pub bip32_index: LedgerBIP32Index, } #[derive(Debug, Clone)] pub struct OutputIndexRecorder { pub output: Output, pub bech32: String, pub bip32_index: LedgerBIP32Index, pub value: u64, pub is_remainder: bool, } pub fn get_transaction_unlock_blocks_blindsigning_essence_hash( chain: u32, account: u32, essence_hash: [u8; 32], address_indices: &[LedgerBIP32Index], ) -> Result> { let seed = get_seed(); let mut unlock_blocks = vec![]; for bip32_index in address_indices.iter() { // If not, we should create a signature unlock block let private_key = get_key(chain, &seed, account, *bip32_index).unwrap(); let iota_priv_key = ed25519::SecretKey::from_bytes(&private_key.key); let public_key = private_key.public_key(); let mut public_key_trunc = [0u8; 32]; public_key_trunc.clone_from_slice(&public_key[1..33]); // The block should sign the entire transaction essence part of the transaction payload let signature = Box::new(iota_priv_key.sign(&essence_hash).to_bytes()); unlock_blocks.push(Unlock::Signature(SignatureUnlock::new(Signature::Ed25519( Ed25519Signature::new(public_key_trunc, *signature), )))); log::info!( "put {:08x}:{:08x} into signatures_indexes", bip32_index.bip32_change, bip32_index.bip32_index, ); // Update current block index } Ok(unlock_blocks) } pub fn get_transaction_unlock_blocks_essence_hash( chain: u32, account: u32, essence_hash: [u8; 32], address_index_recorders: &mut [InputIndexRecorder], ) -> Result> { let seed = get_seed(); let mut unlock_blocks = vec![]; let mut signature_indexes = HashMap::::new(); //address_index_recorders.sort_by(|a, b| a.input.cmp(&b.input)); XXX for (current_block_index, recorder) in address_index_recorders.iter().enumerate() { // Check if current path is same as previous path // If so, add a reference unlock block if let Some((_, v)) = signature_indexes.iter().find_map(|(k, v)| { if k == &recorder.bip32_index { Some((k, v)) } else { None } }) { log::info!( "found reference {:08x}:{:08x} {} -> {}", recorder.bip32_index.bip32_change, recorder.bip32_index.bip32_index, current_block_index, *v ); unlock_blocks .push(Unlock::Reference(ReferenceUnlock::new(*v as u16).map_err( |_| anyhow::anyhow!("failed to create reference unlock block"), )?)); } else { // If not, we should create a signature unlock block let private_key = get_key(chain, &seed, account, recorder.bip32_index).unwrap(); let iota_priv_key = ed25519::SecretKey::from_bytes(&private_key.key); let public_key = private_key.public_key(); let mut public_key_trunc = [0u8; 32]; public_key_trunc.clone_from_slice(&public_key[1..33]); // The block should sign the entire transaction essence part of the transaction payload let signature = Box::new(iota_priv_key.sign(&essence_hash).to_bytes()); unlock_blocks.push(Unlock::Signature(SignatureUnlock::new(Signature::Ed25519( Ed25519Signature::new(public_key_trunc, *signature), )))); signature_indexes.insert(recorder.bip32_index, current_block_index); log::info!( "put {:08x}:{:08x} {} into signatures_indexes", recorder.bip32_index.bip32_change, recorder.bip32_index.bip32_index, current_block_index ); // Update current block index } } Ok(unlock_blocks) } /// Gets the unlock blocks for a transaction. pub fn get_transaction_unlock_blocks( chain: u32, account: u32, essence: &RegularTransactionEssence, address_index_recorders: &mut [InputIndexRecorder], ) -> Result> { let serialized_essence: Vec = TransactionEssence::from(essence.clone()).pack_to_vec(); let mut hasher = VarBlake2b::new(32).unwrap(); hasher.update(serialized_essence); let mut hashed_essence: [u8; 32] = [0; 32]; hasher.finalize_variable(|res| { hashed_essence[..32].clone_from_slice(&res[..32]); }); get_transaction_unlock_blocks_essence_hash( chain, account, hashed_essence, address_index_recorders, ) } pub fn test_blindsigning( hrp: &str, chain: u32, ledger: &mut iota_ledger_nano::LedgerHardwareWallet, rnd: &mut SmallRng, non_interactive: bool, ) -> Result> { // build random config let num_inputs = rnd.next_u32() as u16 % 10 /*MAX_INPUTS*/ + 1; let config = format!("{}", num_inputs,); let mut hm = HASHMAP.lock().unwrap(); let is_new = match hm.get(&config) { Some(_) => false, None => { hm.insert(config.clone(), true); true } }; drop(hm); if !is_new { return Ok(false); } let mut essence_hash = [0u8; 32]; rnd.fill_bytes(&mut essence_hash); #[allow(clippy::modulo_one)] let account = (rnd.next_u32() % MAX_ACCOUNT_RANGE) | HARDENED; println!("account: 0x{:08x}", account & !0x80000000); // get new ledger object (for testing) ledger.set_account(Protocol::Stardust, chain, account)?; let mut address_index_recorders: Vec = Vec::new(); let mut key_indices: Vec = Vec::new(); let mut key_strings: Vec = Vec::new(); for i in 0..num_inputs { let mut txid = [0u8; 32]; rnd.fill_bytes(&mut txid); let input = Input::Utxo( UtxoInput::new(TransactionId::from(txid), rnd.next_u32() as u16 % 127).unwrap(), ); let is_change = rnd.next_u32() & 0x1 == 0x1; let input_bip32_index = LedgerBIP32Index { bip32_index: (rnd.next_u32() % MAX_INPUT_RANGE) | HARDENED, bip32_change: if is_change { 1 } else { 0 } | HARDENED, }; let input_addr_bytes: [u8; 32] = *ledger .get_addresses(false, input_bip32_index, 1) .expect("error get new address") .first() .unwrap(); let mut addr_bytes_with_type = [0u8; 33]; addr_bytes_with_type[0] = 0; // ed25519 addr_bytes_with_type[1..33].clone_from_slice(&input_addr_bytes[..]); let b32 = bech32::encode(hrp, addr_bytes_with_type.to_base32()).unwrap(); key_indices.push(input_bip32_index); key_strings.push(b32.clone()); address_index_recorders.push(InputIndexRecorder { address_index: i as usize, bip32_index: input_bip32_index, input, bech32: b32.clone(), }); } // Gets the unlock blocks for a transaction. let ref_blocks = get_transaction_unlock_blocks_blindsigning_essence_hash( chain, account, essence_hash, &key_indices, ) .unwrap(); println!("essence_hash: {}", hex(&essence_hash)); println!("new configuration: {}", config); // prepare signing in signgle-signing mode (ssm will be the default when finished) ledger .prepare_blind_signing(key_indices, essence_hash.to_vec()) .expect("error prepare blindsigning"); // show essence to user if ledger.is_debug_app() { ledger.set_non_interactive_mode(non_interactive)?; } ledger.user_confirm().expect("error user confirm"); println!(); // sign let signature_bytes = ledger.sign(num_inputs).expect("error signing"); println!("signature: {}", hex(&signature_bytes)); println!(); let mut readable = &mut &*signature_bytes; for t in 0..num_inputs { let signature = Unlock::unpack::<&mut &[u8], true>(&mut readable).expect("error unpacking signature"); let signature2 = ref_blocks.get(t as usize).unwrap(); assert_eq!(&signature, signature2); match signature { Unlock::Signature(s) => { let sig = s.signature(); match sig { Ed25519(s) => { let sig: [u8; 64] = *s.signature(); let sig = ed25519::Signature::from_bytes(sig); let pub_key_bytes = s.public_key(); let addr_bytes = get_addr_from_pubkey(*pub_key_bytes); let mut addr_bytes_with_type = [0u8; 33]; addr_bytes_with_type[0] = 0; // ed25519 addr_bytes_with_type[1..33].clone_from_slice(&addr_bytes[..]); let b32 = bech32::encode(hrp, addr_bytes_with_type.to_base32()).unwrap(); // println!("{} vs {}", b32, key_strings.get(t as usize).unwrap()); assert_eq!(b32, *key_strings.get(t as usize).unwrap()); let pub_key = ed25519::PublicKey::try_from_bytes(*pub_key_bytes).unwrap(); if !pub_key.verify(&sig, &essence_hash) { panic!("error verifying signature"); } println!("found valid signature"); } } } Unlock::Reference(_) => { // NOP println!("found reference"); } _ => unreachable!(), } } Ok(true) } pub fn random_essence( hrp: &str, chain: u32, ledger: &mut iota_ledger_nano::LedgerHardwareWallet, seed: &[u8], rnd: &mut SmallRng, non_interactive: bool, ) -> Result> { // build random config let num_inputs = rnd.next_u32() as u16 % MAX_INPUTS + 1; let num_outputs = rnd.next_u32() as u16 % MAX_OUTPUTS + 1; let num_remainder: u16 = rnd.next_u32() as u16 % 2; if num_inputs + num_outputs + num_remainder > MAX_SUM_INPUTS_OUTPUTS { return Ok(false); } let has_remainder = num_remainder > 0; let mut remainder_index: u16 = 0; let config = format!( "{}|{}|{}|{}", num_inputs, num_outputs, if has_remainder { 1 } else { 0 }, remainder_index ); let mut hm = HASHMAP.lock().unwrap(); let is_new = match hm.get(&config) { Some(_) => false, None => { hm.insert(config.clone(), true); true } }; drop(hm); if !is_new { return Ok(false); } // add to essence // build essence and add input and output let mut essence_builder = RegularTransactionEssenceBuilder::new(0u64, InputsCommitment::from([0u8; 32])); #[allow(clippy::modulo_one)] let account = (rnd.next_u32() % MAX_ACCOUNT_RANGE) | HARDENED; println!("account: 0x{:08x}", account & !0x80000000); // get new ledger object (for testing) ledger.set_account(Protocol::Stardust, chain, account)?; let mut address_index_recorders: Vec = Vec::new(); let mut key_indices: Vec = Vec::new(); let mut key_strings: Vec = Vec::new(); for i in 0..num_inputs { let mut txid = [0u8; 32]; rnd.fill_bytes(&mut txid); let input = Input::Utxo( UtxoInput::new(TransactionId::from(txid), rnd.next_u32() as u16 % 127).unwrap(), ); let is_change = rnd.next_u32() & 0x1 == 0x1; let input_bip32_index = LedgerBIP32Index { bip32_index: (rnd.next_u32() % MAX_INPUT_RANGE) | HARDENED, bip32_change: if is_change { 1 } else { 0 } | HARDENED, }; // println!("index: {:08x}{:08x}", input_bip32_index.bip32_change, input_bip32_index.bip32_index); let input_addr_bytes: [u8; 32] = *ledger .get_addresses(false, input_bip32_index, 1) .expect("error get new address") .first() .unwrap(); let mut addr_bytes_with_type = [0u8; 33]; addr_bytes_with_type[0] = 0; // ed25519 addr_bytes_with_type[1..33].clone_from_slice(&input_addr_bytes[..]); let b32 = bech32::encode(hrp, addr_bytes_with_type.to_base32()).unwrap(); // println!("input: {}", b32); key_indices.push(input_bip32_index); key_strings.push(b32.clone()); address_index_recorders.push(InputIndexRecorder { address_index: i as usize, bip32_index: input_bip32_index, input, bech32: b32.clone(), }); } let mut output_recorder: Vec = Vec::new(); for _ in 0..num_outputs { // duplicate outputs are allowed in stardust let output_bip32_index = LedgerBIP32Index{bip32_index: (rnd.next_u32() % MAX_OUTPUT_RANGE) | HARDENED, bip32_change: /* 0 | */ HARDENED}; let output_addr_bytes: [u8; 32] = *ledger .get_addresses(false, output_bip32_index, 1) .expect("error get new address") .first() .unwrap(); let value_out = rnd.next_u32() as u64 + 1u64; let output = BasicOutputBuilder::new_with_amount(value_out)?.add_unlock_condition( UnlockCondition::Address(AddressUnlockCondition::new(Address::Ed25519( Ed25519Address::new(output_addr_bytes), ))), ); let output: BasicOutput = output.finish()?; //essence_builder = essence_builder.add_output(output.clone()); let mut addr_bytes_with_type = [0u8; 33]; addr_bytes_with_type[0] = 0; // ED25519 addr_bytes_with_type[1..33].clone_from_slice(&output_addr_bytes[..]); let b32 = bech32::encode(hrp, addr_bytes_with_type.to_base32()).unwrap(); let cmp_addr = get_addr(chain, seed, account, output_bip32_index).unwrap(); addr_bytes_with_type[1..33].clone_from_slice(&cmp_addr[..]); let cmp_b32 = bech32::encode(hrp, addr_bytes_with_type.to_base32()).unwrap(); output_recorder.push(OutputIndexRecorder { output: Output::Basic(output.clone()), bech32: b32.clone(), bip32_index: output_bip32_index, value: value_out, is_remainder: false, }); // println!("output: {} {}", b32, value_out); assert_eq!(b32, cmp_b32); } let mut remainder_addr_bytes = [0u8; 32]; let mut remainder_bip32 = LedgerBIP32Index::default(); // let mut remainder_recorer : Vec = Vec::new(); if has_remainder { if ledger.is_debug_app() { ledger.set_non_interactive_mode(non_interactive)?; } // duplicat outputs are allowed in stardust remainder_bip32 = LedgerBIP32Index { bip32_index: (rnd.next_u32() % MAX_REMAINDER_RANGE) | HARDENED, bip32_change: 1 | HARDENED, }; remainder_addr_bytes = *ledger .get_addresses(true, remainder_bip32, 1) .expect("error new remainder") .first() .unwrap(); let value_remainder = rnd.next_u32() as u64; // create output with remainder address let remainder = BasicOutputBuilder::new_with_amount(value_remainder)?.add_unlock_condition( UnlockCondition::Address(AddressUnlockCondition::new(Address::Ed25519( Ed25519Address::new(remainder_addr_bytes), ))), ); let remainder: BasicOutput = remainder.finish()?; //essence_builder = essence_builder.add_output(remainder.clone()); let mut addr_bytes_with_type = [0u8; 33]; addr_bytes_with_type[0] = 0; // ed25519 addr_bytes_with_type[1..33].clone_from_slice(&remainder_addr_bytes[..32]); let b32 = bech32::encode(hrp, addr_bytes_with_type.to_base32()).unwrap(); output_recorder.push(OutputIndexRecorder { output: Output::Basic(remainder), bech32: b32.clone(), bip32_index: remainder_bip32, value: value_remainder, is_remainder: true, }); let cmp_addr = get_addr(chain, seed, account, remainder_bip32).unwrap(); addr_bytes_with_type[1..33].clone_from_slice(&cmp_addr[..]); let cmp_b32 = bech32::encode(hrp, addr_bytes_with_type.to_base32()).unwrap(); /* println!( "remainder: {} {} {:08x}{:08x} {}", b32, remainder_index, remainder_bip32.bip32_change & !0x80000000, remainder_bip32.bip32_index & !0x80000000, value_remainder ); */ assert_eq!(b32, cmp_b32); } // sorting not needed in stardust //output_recorder.sort_by(|a, b| a.output.cmp(&b.output)); XXX //address_index_recorders.sort_by(|a, b| a.input.cmp(&b.input)); XXX // sort inputs for recorder in address_index_recorders.clone() { essence_builder = essence_builder.add_input(recorder.input.clone()); } // sort outputs for recorder in output_recorder.clone() { essence_builder = essence_builder.add_output(recorder.output.clone()); } // finish essence let essence = essence_builder.finish().unwrap(); // swap the remainder - mainly for displaying the remainder as last output later if has_remainder { let mut or_rem_idx = 0; let l = output_recorder.len(); for (i, item) in output_recorder.iter().enumerate() { if item.is_remainder { or_rem_idx = i; } } output_recorder.swap(or_rem_idx, l - 1); } // pack the essence to bytes let essence_bytes: Vec = TransactionEssence::from(essence.clone()).pack_to_vec(); println!("essence: {}", hex(&essence_bytes)); if has_remainder { // after finish, search the index of the remainder output // because outputs are sorted lexically and index may have changed (probably) for output in essence.outputs().iter() { match output { Output::Basic(s) => { let remainder_output = Address::Ed25519(Ed25519Address::new(remainder_addr_bytes)); if remainder_output == *s.address() { println!("found at {}", remainder_index); break; } } _ => { panic!("unknown output type"); } } remainder_index += 1; } // was index found? if remainder_index as usize == essence.outputs().len() { panic!("index was not found"); } } println!("new configuration: {}", config); // Gets the unlock blocks for a transaction. let ref_blocks = get_transaction_unlock_blocks(chain, account, &essence, &mut address_index_recorders) .unwrap(); let mut key_indices: Vec = Vec::new(); let mut key_strings_new: Vec = Vec::new(); println!(); for m in address_index_recorders { // println!("index: {:08x}{:08x}", m.bip32_index.bip32_change, m.bip32_index.bip32_index); let path = format!( "2c'/{:x}'/{:x}'/{:x}'/{:x}'", chain, account & !0x80000000, m.bip32_index.bip32_change & !0x80000000, m.bip32_index.bip32_index & !0x80000000 ); println!("input: {} {}", m.bech32, path); key_indices.push(m.bip32_index); let bsx = (*key_strings.get(m.address_index).unwrap()).clone(); // println!("input: {}", bsx); key_strings_new.push(bsx); } let key_strings = key_strings_new; println!(); for m in output_recorder { let path = format!( "2c'/{:x}'/{:x}'/{:x}'/{:x}'", chain, account & !0x80000000, m.bip32_index.bip32_change & !0x80000000, m.bip32_index.bip32_index & !0x80000000 ); if !m.is_remainder { println!("output: {} {} {}", m.bech32, m.value, path); } else { let path = format!( "2c'/{:x}'/{:x}'/{:x}'/{:x}'", chain, account & !0x80000000, m.bip32_index.bip32_change & !0x80000000, m.bip32_index.bip32_index & !0x80000000 ); println!(); println!("remainder: {} {} {} {}", m.bech32, path, m.value, path); } } /*for i in 0_usize..num_inputs as usize { println!("input indices: {:08x} vs {:08x}", *key_indices.get(i).unwrap(), (*address_index_recorders.get(i).unwrap()).bip32_index | HARDENED) }*/ // prepare signing in signgle-signing mode (ssm will be the default when finished) ledger .prepare_signing( key_indices, essence_bytes.clone(), has_remainder, remainder_index, remainder_bip32, ) .expect("error prepare signing"); // show essence to user if ledger.is_debug_app() { ledger.set_non_interactive_mode(non_interactive)?; } ledger.user_confirm().expect("error user confirm"); println!(); // sign let signature_bytes = ledger.sign(num_inputs).expect("error signing"); println!("signature: {}", hex(&signature_bytes)); println!(); let mut hasher = VarBlake2b::new(32).unwrap(); hasher.update(essence_bytes); let mut hashed_essence: [u8; 32] = [0; 32]; hasher.finalize_variable(|res| { hashed_essence[..32].clone_from_slice(&res[..32]); }); // unpack all signatures to vector let mut readable = &mut &*signature_bytes; for t in 0..num_inputs { let signature = Unlock::unpack::<&mut &[u8], true>(&mut readable).expect("error unpacking signature"); let signature2 = ref_blocks.get(t as usize).unwrap(); assert_eq!(&signature, signature2); match signature { Unlock::Signature(s) => { let sig = s.signature(); match sig { Ed25519(s) => { let sig: [u8; 64] = *s.signature(); let sig = ed25519::Signature::from_bytes(sig); let pub_key_bytes = s.public_key(); let addr_bytes = get_addr_from_pubkey(*pub_key_bytes); let mut addr_bytes_with_type = [0u8; 33]; addr_bytes_with_type[0] = 0; // ed25519 addr_bytes_with_type[1..33].clone_from_slice(&addr_bytes[..]); let b32 = bech32::encode(hrp, addr_bytes_with_type.to_base32()).unwrap(); // println!("{} vs {}", b32, key_strings.get(t as usize).unwrap()); assert_eq!(b32, *key_strings.get(t as usize).unwrap()); let pub_key = ed25519::PublicKey::try_from_bytes(*pub_key_bytes).unwrap(); if !pub_key.verify(&sig, &hashed_essence) { panic!("error verifying signature"); } println!("found valid signature"); } } } Unlock::Reference(_) => { // NOP println!("found reference"); } _ => unreachable!(), } } Ok(true) } fn watcher_cb(apdu_command: &APDUCommand>, apdu_answer: &APDUAnswer>) { let mut writer = WRITER.lock().unwrap(); let raw_command = apdu_command.serialize(); let mut raw_answer: Vec = Vec::new(); raw_answer.extend(apdu_answer.data()); raw_answer.extend(&apdu_answer.retcode().to_be_bytes()); match writer.format { Some(FormatTypes::Json) => { let command_data: String = apdu_command .data .iter() .map(|b| format!("{}", b)) .collect::>() .join(", "); let answer_data: String = apdu_answer .data() .iter() .map(|b| format!("{}", b)) .collect::>() .join(", "); let command = format!( "{{\"cla\":{}, \"ins\":{}, \"p1\":{}, \"p2\":{}, \"data\":[{}]}}", apdu_command.cla, apdu_command.ins, apdu_command.p1, apdu_command.p2, command_data ); let answer = format!( "{{\"data\":[{}], \"retcode\":{}}}", answer_data, apdu_answer.retcode() ); writer.write(&command); writer.write(&answer); } Some(FormatTypes::Hex) => { writer.write(&format!(">>{}", hex(&raw_command))); writer.write(&format!("<<{}", hex(&raw_answer))); } Some(FormatTypes::Bin) => { let rcl = raw_command.len() as u32; let ral = raw_answer.len() as u32; writer.write_bin(&rcl.to_le_bytes()); writer.write_bin(&raw_command); writer.write_bin(&ral.to_le_bytes()); writer.write_bin(&raw_answer); } None => { panic!("no output format set"); } } drop(writer); } pub fn main() -> Result<(), Box> { env_logger::init(); let matches = App::new("ledger iota tester") .version("1.0") .author("Thomas Pototschnig ") .arg( Arg::with_name("is-simulator") .short("s") .long("simulator") .value_name("is_simulator") .help("select the simulator as transport") .takes_value(false), ) .arg( Arg::with_name("blindsigning") .short("b") .long("blindsigning") .value_name("blindsigning") .help("use blindsigning") .takes_value(false), ) .arg( Arg::with_name("non-interactive") .short("n") .long("non-interactive") .value_name("non_interactive") .help("run the program in non-interactive mode for automatic testing") .takes_value(false), ) .arg( Arg::with_name("recorder") .short("r") .long("recorder") .help("record APDU requests and responses to a file") .takes_value(true), ) .arg( Arg::with_name("format") .short("f") .long("format") .help("user output format hex, bin, json (default) as output file format") .takes_value(true), ) .arg( Arg::with_name("dump") .short("d") .long("dump") .help("dump memory after tests") .takes_value(false), ) .arg( Arg::with_name("limit") .short("l") .long("limit") .help("maximum number of tests done") .takes_value(true), ) .arg( Arg::with_name("coin-type") .short("c") .long("coin-type") .help("select coin type (iota, smr)") .takes_value(true), ) .get_matches(); let is_simulator = matches.is_present("is-simulator"); let non_interactive = matches.is_present("non-interactive"); let limit = match matches.is_present("limit") { true => matches.value_of("limit").unwrap().parse::().unwrap(), false => 0, }; let transport_type = if is_simulator { iota_ledger_nano::TransportTypes::TCP } else { iota_ledger_nano::TransportTypes::NativeHID }; if matches.is_present("recorder") { if !is_simulator { panic!("transport watcher only is supported for the simulator"); } let filename = matches.value_of("recorder"); if filename.is_none() { panic!("no filename provided!"); } let mut writer = WRITER.lock().unwrap(); let format_type = match matches.value_of("format") { Some(f) => match f { "hex" => FormatTypes::Hex, "json" => FormatTypes::Json, "bin" => FormatTypes::Bin, _ => panic!("unknown format"), }, None => FormatTypes::Json, }; writer.set_format_type(format_type); writer.open(String::from(filename.unwrap()))?; drop(writer); } let blindsigning = matches.is_present("blindsigning"); println!("{} {}", is_simulator, non_interactive); let mut rnd = SmallRng::from_seed(PRNG_SEED); let seed = get_seed(); assert_eq!(DEFAULT_SEED, &seed[..]); let hrp; let chain; (hrp, chain) = match matches.value_of("coin-type") { Some(c) => match c { "iota" => ("iota", 0x107a), "smr" => ("smr", 0x107b), "rms" => ("rms", 0x1), "atoi" => ("atoi", 0x1), _ => panic!("unknown coin type"), }, None => ("iota", 0x107a), }; let mut ledger = iota_ledger_nano::get_ledger_by_type( Protocol::Stardust, chain, 0x80000000, &transport_type, if matches.is_present("recorder") { Some(watcher_cb) } else { None }, )?; let is_debug_app = ledger.is_debug_app(); DEBUG_APP.store(is_debug_app, Ordering::Release); let path = format!("44'/{}'/{}'/{}'/{}'", chain, 0, 0, 0); println!("path: {}", path); let bip32_path = slip10::path::BIP32Path::from_str(&path).unwrap(); let key = slip10::derive_key_from_path(&seed[..], slip10::Curve::Ed25519, &bip32_path).unwrap(); match chain { 0x1 => assert_eq!(DEFAULT_KEY_DEBUG, hex(&key.key)), 0x107a => assert_eq!(DEFAULT_KEY_IOTA, hex(&key.key)), 0x107b => assert_eq!(DEFAULT_KEY_SMR, hex(&key.key)), _ => unreachable!(), } let mut run: u32 = 0; for _ in 0..10000 { let was_new = match blindsigning { false => random_essence(hrp, chain, &mut ledger, &seed, &mut rnd, non_interactive)?, true => test_blindsigning(hrp, chain, &mut ledger, &mut rnd, non_interactive)?, }; if was_new { run += 1; println!("\nrun {} successful\n", run); if limit == run { println!("limit reached."); break; } } } if matches.is_present("dump") { ledger.memory_dump(String::from("dump_after_test.bin"))?; } Ok(()) }