use std::{collections::HashMap, ops::Range};
use async_trait::async_trait;
use crypto::{
keys::{bip44::Bip44, slip10::Segment},
signatures::{
ed25519,
secp256k1_ecdsa::{self, EvmAddress},
},
};
use iota_ledger_nano::{
api::{constants::Protocol, errors::APIError},
get_app_config, get_buffer_size, get_ledger, get_opened_app, LedgerBIP32Index, Packable as LedgerNanoPackable,
TransportTypes,
};
use packable::{
error::{UnexpectedEOF, UnpackErrorExt},
PackableExt,
};
use tokio::sync::Mutex;
use super::{GenerateAddressOptions, SecretManage, SecretManagerConfig};
use crate::{
client::{
secret::{
types::{LedgerApp, LedgerDeviceType},
LedgerNanoStatus, PreparedTransactionData,
},
ClientError,
},
types::block::{
address::{AccountAddress, Address, NftAddress},
output::{Output, OutputError},
payload::signed_transaction::SignedTransactionPayload,
protocol::ProtocolParameters,
signature::{Ed25519Signature, Signature, SignatureError},
unlock::{AccountUnlock, NftUnlock, ReferenceUnlock, SignatureUnlock, Unlock, UnlockError, Unlocks},
BlockError,
},
};
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
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(#[from] BlockError),
#[error("missing input with ed25519 address")]
MissingInputWithEd25519Address,
#[error("missing bip32 chain")]
MissingBip32Chain,
#[error("Bip32 chain mismatch")]
Bip32ChainMismatch,
#[error("{0}")]
Unpack(#[from] packable::error::UnpackError<BlockError, UnexpectedEOF>),
#[error("No available inputs provided")]
NoAvailableInputsProvided,
#[error("output not unlockable due to deadzone in expiration unlock condition")]
ExpirationDeadzone,
}
crate::impl_from_error_via!(Error via BlockError: OutputError, UnlockError, SignatureError);
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 = ClientError;
async fn generate_ed25519_public_keys(
&self,
coin_type: u32,
account_index: u32,
address_indexes: Range<u32>,
options: impl Into<Option<GenerateAddressOptions>> + Send,
) -> Result<Vec<ed25519::PublicKey>, 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(Protocol::Nova, 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 public_keys = ledger
.get_public_keys(options.ledger_nano_prompt, bip32, address_indexes.len())
.map_err(Error::from)?;
drop(lock);
Ok(public_keys
.into_iter()
.map(ed25519::PublicKey::try_from_bytes)
.collect::<Result<Vec<_>, _>>()?)
}
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 && msg.len() != 64 {
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(Protocol::Nova, 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);
return match Unlock::unpack_bytes_verified(signature_bytes, &()).coerce()? {
Unlock::Signature(s) => match *s {
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 transaction_unlocks(
&self,
prepared_transaction: &PreparedTransactionData,
protocol_parameters: &ProtocolParameters,
) -> 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 transaction_bytes = prepared_transaction.transaction.pack_to_vec();
let transaction_signing_hash = prepared_transaction.transaction.signing_hash().to_vec();
let lock = self.mutex.lock().await;
let ledger = get_ledger(Protocol::Nova, 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, transaction_signing_hash);
ledger
.prepare_blind_signing(input_bip32_indices, transaction_signing_hash)
.map_err(Error::from)?;
} else {
#[allow(clippy::option_if_let_else)]
let (remainder_output, remainder_bip32) = match &prepared_transaction.remainders.as_slice() {
[remainder] => {
if let Some(chain) = remainder.chain {
(
Some(&remainder.output),
LedgerBIP32Index {
bip32_change: chain.change.harden().into(),
bip32_index: chain.address_index.harden().into(),
},
)
} else {
(None, LedgerBIP32Index::default())
}
}
_ => (None, LedgerBIP32Index::default()),
};
let mut remainder_index = 0u16;
if let Some(remainder_output) = remainder_output {
for output in prepared_transaction.transaction.outputs().iter() {
if !output.is_basic() {
log::debug!("[LEDGER] unsupported output");
return Err(Error::MiscError.into());
}
if remainder_output == output {
break;
}
remainder_index += 1;
}
if remainder_index as usize == prepared_transaction.transaction.outputs().len() {
log::debug!("[LEDGER] remainder_index not found");
return Err(Error::MiscError.into());
}
if remainder_index as usize == prepared_transaction.transaction.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,
transaction_bytes,
remainder_output.is_some(),
remainder_index,
remainder_bip32
);
ledger
.prepare_signing(
input_bip32_indices,
transaction_bytes,
remainder_output.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 unlocks = Vec::new();
for _ in 0..input_len {
let unlock = Unlock::unpack_bytes_verified(&signature_bytes, &()).coerce()?;
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(), protocol_parameters)?;
}
Ok(Unlocks::new(unlocks)?)
}
async fn sign_transaction(
&self,
prepared_transaction_data: PreparedTransactionData,
protocol_parameters: &ProtocolParameters,
) -> Result<SignedTransactionPayload, Self::Error> {
super::default_sign_transaction(self, prepared_transaction_data, protocol_parameters).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 {
if !prepared_transaction.transaction.outputs().iter().all(
|output| matches!(output, Output::Basic(o) if o.simple_deposit_address().is_some() && o.address().is_ed25519()),
) {
return true;
}
let total_size = LedgerBIP32Index::default().packed_len() * prepared_transaction.inputs_data.len()
+ prepared_transaction.transaction.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>,
protocol_parameters: &ProtocolParameters,
) -> Result<Vec<Unlock>, Error> {
let commitment_slot_index = prepared_transaction_data
.transaction
.context_inputs()
.commitment()
.map(|c| c.slot_index());
let transaction_signing_hash = prepared_transaction_data.transaction.signing_hash();
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 required_address = input
.output
.required_address(commitment_slot_index, protocol_parameters.committable_age_range())?
.ok_or(Error::ExpirationDeadzone)?;
let required_address = match required_address {
Address::ImplicitAccountCreation(implicit) => Address::Ed25519(*implicit.ed25519_address()),
Address::Restricted(restricted) => restricted.address().clone(),
_ => required_address,
};
match block_indexes.get(&required_address) {
Some(block_index) => match required_address {
Address::Ed25519(_) | Address::ImplicitAccountCreation(_) => {
merged_unlocks.push(Unlock::Reference(ReferenceUnlock::new(*block_index as u16)?));
}
Address::Account(_) => merged_unlocks.push(Unlock::Account(AccountUnlock::new(*block_index as u16)?)),
Address::Nft(_) => merged_unlocks.push(Unlock::Nft(NftUnlock::new(*block_index as u16)?)),
_ => Err(BlockError::UnsupportedAddressKind(required_address.kind()))?,
},
None => {
match &required_address {
Address::Ed25519(_) | Address::ImplicitAccountCreation(_) => {}
_ => 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 required_address {
Address::Ed25519(ed25519_address) => ed25519_address,
_ => return Err(Error::MissingInputWithEd25519Address),
};
ed25519_signature.validate(transaction_signing_hash.as_ref(), &ed25519_address)?;
}
merged_unlocks.push(unlock);
block_indexes.insert(required_address.clone(), current_block_index);
}
}
match &input.output {
Output::Account(account_output) => block_indexes.insert(
Address::Account(AccountAddress::new(
account_output.account_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 pretty_assertions::assert_eq;
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].clone().to_bech32_unchecked("atoi").to_string(),
"atoi1qqdnv60ryxynaeyu8paq3lp9rkll7d7d92vpumz88fdj4l0pn5mru50gvd8"
);
}
}