use std::{collections::HashMap, ops::Range};
use async_trait::async_trait;
use crypto::{
keys::{bip44::Bip44, slip10::Segment},
signatures::secp256k1_ecdsa::{self, EvmAddress},
};
use iota_ledger_nano::{
api::errors::APIError, get_app_config, get_buffer_size, get_ledger, get_opened_app, LedgerBIP32Index,
Packable as LedgerNanoPackable, TransportTypes,
};
use packable::{error::UnexpectedEOF, unpacker::SliceUnpacker, Packable, PackableExt};
use tokio::sync::Mutex;
use super::{GenerateAddressOptions, SecretManage, SecretManagerConfig};
use crate::{
client::secret::{
is_alias_transition,
types::{LedgerApp, LedgerDeviceType},
LedgerNanoStatus, PreparedTransactionData,
},
types::block::{
address::{Address, AliasAddress, Ed25519Address, NftAddress},
output::Output,
payload::transaction::{TransactionEssence, TransactionPayload},
signature::{Ed25519Signature, Signature},
unlock::{AliasUnlock, NftUnlock, ReferenceUnlock, SignatureUnlock, Unlock, Unlocks},
},
utils::unix_timestamp_now,
};
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("denied by user")]
DeniedByUser,
#[error("ledger locked")]
DongleLocked,
#[error("ledger device not found")]
DeviceNotFound,
#[error("ledger essence too large")]
EssenceTooLarge,
#[error("ledger transport error")]
MiscError,
#[error("unsupported operation")]
UnsupportedOperation,
#[error("{0}")]
Block(Box<crate::types::block::Error>),
#[error("missing input with ed25519 address")]
MissingInputWithEd25519Address,
#[error("missing bip32 chain")]
MissingBip32Chain,
#[error("Bip32 chain mismatch")]
Bip32ChainMismatch,
#[error("{0}")]
Unpack(#[from] packable::error::UnpackError<crate::types::block::Error, UnexpectedEOF>),
#[error("No available inputs provided")]
NoAvailableInputsProvided,
}
impl From<crate::types::block::Error> for Error {
fn from(error: crate::types::block::Error) -> Self {
Self::Block(Box::new(error))
}
}
impl From<APIError> for Error {
fn from(error: APIError) -> Self {
log::info!("ledger error: {}", error);
match error {
APIError::ConditionsOfUseNotSatisfied => Self::DeniedByUser,
APIError::EssenceTooLarge => Self::EssenceTooLarge,
APIError::SecurityStatusNotSatisfied => Self::DongleLocked,
APIError::TransportError => Self::DeviceNotFound,
_ => Self::MiscError,
}
}
}
#[derive(Default, Debug)]
pub struct LedgerSecretManager {
pub is_simulator: bool,
pub non_interactive: bool,
mutex: Mutex<()>,
}
impl TryFrom<u8> for LedgerDeviceType {
type Error = Error;
fn try_from(device: u8) -> Result<Self, Self::Error> {
match device {
0 => Ok(Self::LedgerNanoS),
1 => Ok(Self::LedgerNanoX),
2 => Ok(Self::LedgerNanoSPlus),
_ => Err(Error::MiscError),
}
}
}
#[async_trait]
impl SecretManage for LedgerSecretManager {
type Error = crate::client::Error;
async fn generate_ed25519_addresses(
&self,
coin_type: u32,
account_index: u32,
address_indexes: Range<u32>,
options: impl Into<Option<GenerateAddressOptions>> + Send,
) -> Result<Vec<Ed25519Address>, Self::Error> {
let options = options.into().unwrap_or_default();
let bip32_account = account_index.harden().into();
let bip32 = LedgerBIP32Index {
bip32_index: address_indexes.start.harden().into(),
bip32_change: u32::from(options.internal).harden().into(),
};
let lock = self.mutex.lock().await;
let ledger = get_ledger(coin_type, bip32_account, self.is_simulator).map_err(Error::from)?;
if ledger.is_debug_app() {
ledger
.set_non_interactive_mode(self.non_interactive)
.map_err(Error::from)?;
}
let addresses = ledger
.get_addresses(options.ledger_nano_prompt, bip32, address_indexes.len())
.map_err(Error::from)?;
drop(lock);
Ok(addresses.into_iter().map(Ed25519Address::new).collect())
}
async fn generate_evm_addresses(
&self,
_coin_type: u32,
_account_index: u32,
_address_indexes: Range<u32>,
_options: impl Into<Option<GenerateAddressOptions>> + Send,
) -> Result<Vec<EvmAddress>, Self::Error> {
Err(Error::UnsupportedOperation.into())
}
async fn sign_ed25519(&self, msg: &[u8], chain: Bip44) -> Result<Ed25519Signature, Self::Error> {
if msg.len() != 32 {
return Err(Error::UnsupportedOperation.into());
}
let msg = msg.to_vec();
let coin_type = chain.coin_type;
let account_index = chain.account.harden().into();
let bip32_index = LedgerBIP32Index {
bip32_change: chain.change.harden().into(),
bip32_index: chain.address_index.harden().into(),
};
let lock = self.mutex.lock().await;
let ledger = get_ledger(coin_type, account_index, self.is_simulator).map_err(Error::from)?;
if ledger.is_debug_app() {
ledger
.set_non_interactive_mode(self.non_interactive)
.map_err(Error::from)?;
}
log::debug!("[LEDGER] prepare_blind_signing");
log::debug!("[LEDGER] {:?} {:?}", bip32_index, msg);
ledger
.prepare_blind_signing(vec![bip32_index], msg)
.map_err(Error::from)?;
log::debug!("[LEDGER] await user confirmation");
ledger.user_confirm().map_err(Error::from)?;
let signature_bytes = ledger.sign(1).map_err(Error::from)?;
drop(ledger);
drop(lock);
let mut unpacker = SliceUnpacker::new(&signature_bytes);
return match Unlock::unpack::<_, true>(&mut unpacker, &())? {
Unlock::Signature(SignatureUnlock(Signature::Ed25519(signature))) => Ok(*signature),
_ => Err(Error::UnsupportedOperation.into()),
};
}
async fn sign_secp256k1_ecdsa(
&self,
_msg: &[u8],
_chain: Bip44,
) -> Result<(secp256k1_ecdsa::PublicKey, secp256k1_ecdsa::RecoverableSignature), Self::Error> {
Err(Error::UnsupportedOperation.into())
}
async fn sign_transaction_essence(
&self,
prepared_transaction: &PreparedTransactionData,
time: Option<u32>,
) -> Result<Unlocks, <Self as SecretManage>::Error> {
let mut input_bip32_indices = Vec::new();
let mut coin_type = None;
let mut account_index = None;
let input_len = prepared_transaction.inputs_data.len();
for input in &prepared_transaction.inputs_data {
let chain = input.chain.ok_or(Error::MissingBip32Chain)?;
if (coin_type.is_some() && coin_type != Some(chain.coin_type))
|| (account_index.is_some() && account_index != Some(chain.account))
{
return Err(Error::Bip32ChainMismatch.into());
}
coin_type = Some(chain.coin_type);
account_index = Some(chain.account);
input_bip32_indices.push(LedgerBIP32Index {
bip32_change: chain.change.harden().into(),
bip32_index: chain.address_index.harden().into(),
});
}
let (coin_type, account_index) = coin_type.zip(account_index).ok_or(Error::NoAvailableInputsProvided)?;
let bip32_account = account_index.harden().into();
let essence_bytes = prepared_transaction.essence.pack_to_vec();
let essence_hash = prepared_transaction.essence.hash().to_vec();
let lock = self.mutex.lock().await;
let ledger = get_ledger(coin_type, bip32_account, self.is_simulator).map_err(Error::from)?;
if ledger.is_debug_app() {
ledger
.set_non_interactive_mode(self.non_interactive)
.map_err(Error::from)?;
}
let blind_signing = needs_blind_signing(prepared_transaction, ledger.get_buffer_size());
if blind_signing {
log::debug!("[LEDGER] prepare_blind_signing");
log::debug!("[LEDGER] {:?} {:?}", input_bip32_indices, essence_hash);
ledger
.prepare_blind_signing(input_bip32_indices, essence_hash)
.map_err(Error::from)?;
} else {
let (remainder_address, remainder_bip32): (Option<&Address>, LedgerBIP32Index) =
match &prepared_transaction.remainder {
Some(a) => {
let chain = a.chain.ok_or(Error::MissingBip32Chain)?;
(
Some(&a.address),
LedgerBIP32Index {
bip32_change: chain.change.harden().into(),
bip32_index: chain.address_index.harden().into(),
},
)
}
None => (None, LedgerBIP32Index::default()),
};
let mut remainder_index = 0u16;
if let Some(remainder_address) = remainder_address {
match &prepared_transaction.essence {
TransactionEssence::Regular(essence) => {
'essence_outputs: for output in essence.outputs().iter() {
if let Output::Basic(s) = output {
if let Some(address) = s.unlock_conditions().address() {
if *remainder_address == *address.address() {
break 'essence_outputs;
}
}
} else {
log::debug!("[LEDGER] unsupported output");
return Err(Error::MiscError.into());
}
remainder_index += 1;
}
if remainder_index as usize == essence.outputs().len() {
log::debug!("[LEDGER] remainder_index not found");
return Err(Error::MiscError.into());
}
}
}
}
log::debug!("[LEDGER] prepare signing");
log::debug!(
"[LEDGER] {:?} {:02x?} {} {} {:?}",
input_bip32_indices,
essence_bytes,
remainder_address.is_some(),
remainder_index,
remainder_bip32
);
ledger
.prepare_signing(
input_bip32_indices,
essence_bytes,
remainder_address.is_some(),
remainder_index,
remainder_bip32,
)
.map_err(Error::from)?;
}
log::debug!("[LEDGER] await user confirmation");
ledger.user_confirm().map_err(Error::from)?;
let signature_bytes = ledger.sign(input_len as u16).map_err(Error::from)?;
drop(ledger);
drop(lock);
let mut unpacker = SliceUnpacker::new(&signature_bytes);
let mut unlocks = Vec::new();
for _ in 0..input_len {
let unlock = Unlock::unpack::<_, true>(&mut unpacker, &())?;
match unlock {
Unlock::Signature(_) => {
if !unlocks.contains(&unlock) {
unlocks.push(unlock);
}
}
_ => unlocks.push(unlock),
}
}
if blind_signing {
unlocks = merge_unlocks(prepared_transaction, unlocks.into_iter(), time)?;
}
Ok(Unlocks::new(unlocks)?)
}
async fn sign_transaction(
&self,
prepared_transaction_data: PreparedTransactionData,
) -> Result<TransactionPayload, Self::Error> {
super::default_sign_transaction(self, prepared_transaction_data).await
}
}
impl SecretManagerConfig for LedgerSecretManager {
type Config = bool;
fn to_config(&self) -> Option<Self::Config> {
Some(self.is_simulator)
}
fn from_config(config: &Self::Config) -> Result<Self, Self::Error> {
Ok(Self::new(*config))
}
}
pub fn needs_blind_signing(prepared_transaction: &PreparedTransactionData, buffer_size: usize) -> bool {
let TransactionEssence::Regular(essence) = &prepared_transaction.essence;
if !essence
.outputs()
.iter()
.all(|output| matches!(output, Output::Basic(o) if o.simple_deposit_address().is_some()))
{
return true;
}
let total_size = LedgerBIP32Index::default().packed_len() * prepared_transaction.inputs_data.len()
+ prepared_transaction.essence.packed_len();
total_size > buffer_size
}
impl LedgerSecretManager {
pub fn new(is_simulator: bool) -> Self {
Self {
is_simulator,
non_interactive: false,
mutex: Mutex::new(()),
}
}
pub async fn get_ledger_nano_status(&self) -> LedgerNanoStatus {
log::debug!("get_ledger_nano_status");
let _lock = self.mutex.lock().await;
let transport_type = if self.is_simulator {
TransportTypes::TCP
} else {
TransportTypes::NativeHID
};
log::debug!("get_opened_app");
let app = match get_opened_app(&transport_type) {
Ok((name, version)) => Some(LedgerApp { name, version }),
_ => None,
};
log::debug!("get_app_config");
let (connected_, locked, blind_signing_enabled, device) =
get_app_config(&transport_type).map_or((false, None, false, None), |config| {
(
true,
Some(config.flags & (1 << 0) != 0),
config.flags & (1 << 1) != 0,
LedgerDeviceType::try_from(config.device).ok(),
)
});
log::debug!("get_buffer_size");
let buffer_size = get_buffer_size(&transport_type).ok();
let connected = if app.is_some() { true } else { connected_ };
LedgerNanoStatus {
connected,
locked,
blind_signing_enabled,
app,
device,
buffer_size,
}
}
}
fn merge_unlocks(
prepared_transaction_data: &PreparedTransactionData,
mut unlocks: impl Iterator<Item = Unlock>,
time: Option<u32>,
) -> Result<Vec<Unlock>, Error> {
let hashed_essence = prepared_transaction_data.essence.hash();
let time = time.unwrap_or_else(|| unix_timestamp_now().as_secs() as u32);
let mut merged_unlocks = Vec::new();
let mut block_indexes = HashMap::<Address, usize>::new();
for (current_block_index, input) in prepared_transaction_data.inputs_data.iter().enumerate() {
let TransactionEssence::Regular(regular) = &prepared_transaction_data.essence;
let alias_transition = is_alias_transition(&input.output, *input.output_id(), regular.outputs(), None);
let (input_address, _) =
input
.output
.required_and_unlocked_address(time, input.output_metadata.output_id(), alias_transition)?;
match block_indexes.get(&input_address) {
Some(block_index) => match input_address {
Address::Alias(_alias) => merged_unlocks.push(Unlock::Alias(AliasUnlock::new(*block_index as u16)?)),
Address::Ed25519(_ed25519) => {
merged_unlocks.push(Unlock::Reference(ReferenceUnlock::new(*block_index as u16)?));
}
Address::Nft(_nft) => merged_unlocks.push(Unlock::Nft(NftUnlock::new(*block_index as u16)?)),
},
None => {
if !input_address.is_ed25519() {
return Err(Error::MissingInputWithEd25519Address)?;
}
let unlock = unlocks.next().ok_or(Error::MissingInputWithEd25519Address)?;
if let Unlock::Signature(signature_unlock) = &unlock {
let Signature::Ed25519(ed25519_signature) = signature_unlock.signature();
let ed25519_address = match input_address {
Address::Ed25519(ed25519_address) => ed25519_address,
_ => return Err(Error::MissingInputWithEd25519Address)?,
};
ed25519_signature.is_valid(&hashed_essence, &ed25519_address)?;
}
merged_unlocks.push(unlock);
block_indexes.insert(input_address, current_block_index);
}
}
match &input.output {
Output::Alias(alias_output) => block_indexes.insert(
Address::Alias(AliasAddress::new(alias_output.alias_id_non_null(input.output_id()))),
current_block_index,
),
Output::Nft(nft_output) => block_indexes.insert(
Address::Nft(NftAddress::new(nft_output.nft_id_non_null(input.output_id()))),
current_block_index,
),
_ => None,
};
}
Ok(merged_unlocks)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
client::{api::GetAddressesOptions, constants::IOTA_COIN_TYPE, secret::SecretManager},
types::block::address::ToBech32Ext,
};
#[tokio::test]
#[ignore = "requires ledger nano instance"]
async fn ed25519_address() {
let mut secret_manager = LedgerSecretManager::new(true);
secret_manager.non_interactive = true;
let addresses = SecretManager::LedgerNano(secret_manager)
.generate_ed25519_addresses(
GetAddressesOptions::default()
.with_coin_type(IOTA_COIN_TYPE)
.with_account_index(0)
.with_range(0..1),
)
.await
.unwrap();
assert_eq!(
addresses[0].to_bech32_unchecked("atoi").to_string(),
"atoi1qqdnv60ryxynaeyu8paq3lp9rkll7d7d92vpumz88fdj4l0pn5mru50gvd8"
);
}
}