1use std::{collections::HashMap, ops::Range};
9
10use async_trait::async_trait;
11use crypto::{
12 keys::{bip44::Bip44, slip10::Segment},
13 signatures::secp256k1_ecdsa::{self, EvmAddress},
14};
15use iota_ledger_nano::{
16 api::errors::APIError, get_app_config, get_buffer_size, get_ledger, get_opened_app, LedgerBIP32Index,
17 Packable as LedgerNanoPackable, TransportTypes,
18};
19use packable::{error::UnexpectedEOF, unpacker::SliceUnpacker, Packable, PackableExt};
20use tokio::sync::Mutex;
21
22use super::{GenerateAddressOptions, SecretManage, SecretManagerConfig};
23use crate::{
24 client::secret::{
25 is_alias_transition,
26 types::{LedgerApp, LedgerDeviceType},
27 LedgerNanoStatus, PreparedTransactionData,
28 },
29 types::block::{
30 address::{Address, AliasAddress, Ed25519Address, NftAddress},
31 output::Output,
32 payload::transaction::{TransactionEssence, TransactionPayload},
33 signature::{Ed25519Signature, Signature},
34 unlock::{AliasUnlock, NftUnlock, ReferenceUnlock, SignatureUnlock, Unlock, Unlocks},
35 },
36 utils::unix_timestamp_now,
37};
38
39#[derive(Debug, thiserror::Error)]
41pub enum Error {
42 #[error("denied by user")]
44 DeniedByUser,
45 #[error("ledger locked")]
47 DongleLocked,
48 #[error("ledger device not found")]
50 DeviceNotFound,
51 #[error("ledger essence too large")]
53 EssenceTooLarge,
54 #[error("ledger transport error")]
56 MiscError,
57 #[error("unsupported operation")]
59 UnsupportedOperation,
60 #[error("{0}")]
62 Block(Box<crate::types::block::Error>),
63 #[error("missing input with ed25519 address")]
65 MissingInputWithEd25519Address,
66 #[error("missing bip32 chain")]
68 MissingBip32Chain,
69 #[error("Bip32 chain mismatch")]
71 Bip32ChainMismatch,
72 #[error("{0}")]
74 Unpack(#[from] packable::error::UnpackError<crate::types::block::Error, UnexpectedEOF>),
75 #[error("No available inputs provided")]
77 NoAvailableInputsProvided,
78}
79
80impl From<crate::types::block::Error> for Error {
81 fn from(error: crate::types::block::Error) -> Self {
82 Self::Block(Box::new(error))
83 }
84}
85
86impl From<APIError> for Error {
94 fn from(error: APIError) -> Self {
95 log::info!("ledger error: {}", error);
96 match error {
97 APIError::ConditionsOfUseNotSatisfied => Self::DeniedByUser,
98 APIError::EssenceTooLarge => Self::EssenceTooLarge,
99 APIError::SecurityStatusNotSatisfied => Self::DongleLocked,
100 APIError::TransportError => Self::DeviceNotFound,
101 _ => Self::MiscError,
102 }
103 }
104}
105
106#[derive(Default, Debug)]
108pub struct LedgerSecretManager {
109 pub is_simulator: bool,
111 pub non_interactive: bool,
113 mutex: Mutex<()>,
115}
116
117impl TryFrom<u8> for LedgerDeviceType {
118 type Error = Error;
119
120 fn try_from(device: u8) -> Result<Self, Self::Error> {
121 match device {
122 0 => Ok(Self::LedgerNanoS),
123 1 => Ok(Self::LedgerNanoX),
124 2 => Ok(Self::LedgerNanoSPlus),
125 _ => Err(Error::MiscError),
126 }
127 }
128}
129
130#[async_trait]
131impl SecretManage for LedgerSecretManager {
132 type Error = crate::client::Error;
133
134 async fn generate_ed25519_addresses(
135 &self,
136 coin_type: u32,
139 account_index: u32,
140 address_indexes: Range<u32>,
141 options: impl Into<Option<GenerateAddressOptions>> + Send,
142 ) -> Result<Vec<Ed25519Address>, Self::Error> {
143 let options = options.into().unwrap_or_default();
144 let bip32_account = account_index.harden().into();
145
146 let bip32 = LedgerBIP32Index {
147 bip32_index: address_indexes.start.harden().into(),
148 bip32_change: u32::from(options.internal).harden().into(),
149 };
150
151 let lock = self.mutex.lock().await;
153
154 let ledger = get_ledger(coin_type, bip32_account, self.is_simulator).map_err(Error::from)?;
156 if ledger.is_debug_app() {
157 ledger
158 .set_non_interactive_mode(self.non_interactive)
159 .map_err(Error::from)?;
160 }
161
162 let addresses = ledger
163 .get_addresses(options.ledger_nano_prompt, bip32, address_indexes.len())
164 .map_err(Error::from)?;
165
166 drop(lock);
167
168 Ok(addresses.into_iter().map(Ed25519Address::new).collect())
169 }
170
171 async fn generate_evm_addresses(
172 &self,
173 _coin_type: u32,
174 _account_index: u32,
175 _address_indexes: Range<u32>,
176 _options: impl Into<Option<GenerateAddressOptions>> + Send,
177 ) -> Result<Vec<EvmAddress>, Self::Error> {
178 Err(Error::UnsupportedOperation.into())
179 }
180
181 async fn sign_ed25519(&self, msg: &[u8], chain: Bip44) -> Result<Ed25519Signature, Self::Error> {
183 if msg.len() != 32 {
184 return Err(Error::UnsupportedOperation.into());
185 }
186
187 let msg = msg.to_vec();
188
189 let coin_type = chain.coin_type;
190 let account_index = chain.account.harden().into();
191 let bip32_index = LedgerBIP32Index {
192 bip32_change: chain.change.harden().into(),
193 bip32_index: chain.address_index.harden().into(),
194 };
195
196 let lock = self.mutex.lock().await;
198
199 let ledger = get_ledger(coin_type, account_index, self.is_simulator).map_err(Error::from)?;
200 if ledger.is_debug_app() {
201 ledger
202 .set_non_interactive_mode(self.non_interactive)
203 .map_err(Error::from)?;
204 }
205
206 log::debug!("[LEDGER] prepare_blind_signing");
207 log::debug!("[LEDGER] {:?} {:?}", bip32_index, msg);
208 ledger
209 .prepare_blind_signing(vec![bip32_index], msg)
210 .map_err(Error::from)?;
211
212 log::debug!("[LEDGER] await user confirmation");
214 ledger.user_confirm().map_err(Error::from)?;
215
216 let signature_bytes = ledger.sign(1).map_err(Error::from)?;
218
219 drop(ledger);
220 drop(lock);
221
222 let mut unpacker = SliceUnpacker::new(&signature_bytes);
223
224 return match Unlock::unpack::<_, true>(&mut unpacker, &())? {
226 Unlock::Signature(SignatureUnlock(Signature::Ed25519(signature))) => Ok(*signature),
227 _ => Err(Error::UnsupportedOperation.into()),
228 };
229 }
230
231 async fn sign_secp256k1_ecdsa(
232 &self,
233 _msg: &[u8],
234 _chain: Bip44,
235 ) -> Result<(secp256k1_ecdsa::PublicKey, secp256k1_ecdsa::RecoverableSignature), Self::Error> {
236 Err(Error::UnsupportedOperation.into())
237 }
238
239 async fn sign_transaction_essence(
240 &self,
241 prepared_transaction: &PreparedTransactionData,
242 time: Option<u32>,
243 ) -> Result<Unlocks, <Self as SecretManage>::Error> {
244 let mut input_bip32_indices = Vec::new();
245 let mut coin_type = None;
246 let mut account_index = None;
247
248 let input_len = prepared_transaction.inputs_data.len();
249
250 for input in &prepared_transaction.inputs_data {
251 let chain = input.chain.ok_or(Error::MissingBip32Chain)?;
252
253 if (coin_type.is_some() && coin_type != Some(chain.coin_type))
255 || (account_index.is_some() && account_index != Some(chain.account))
256 {
257 return Err(Error::Bip32ChainMismatch.into());
258 }
259
260 coin_type = Some(chain.coin_type);
261 account_index = Some(chain.account);
262 input_bip32_indices.push(LedgerBIP32Index {
263 bip32_change: chain.change.harden().into(),
264 bip32_index: chain.address_index.harden().into(),
265 });
266 }
267
268 let (coin_type, account_index) = coin_type.zip(account_index).ok_or(Error::NoAvailableInputsProvided)?;
269
270 let bip32_account = account_index.harden().into();
271
272 let essence_bytes = prepared_transaction.essence.pack_to_vec();
274 let essence_hash = prepared_transaction.essence.hash().to_vec();
275
276 let lock = self.mutex.lock().await;
278
279 let ledger = get_ledger(coin_type, bip32_account, self.is_simulator).map_err(Error::from)?;
280 if ledger.is_debug_app() {
281 ledger
282 .set_non_interactive_mode(self.non_interactive)
283 .map_err(Error::from)?;
284 }
285 let blind_signing = needs_blind_signing(prepared_transaction, ledger.get_buffer_size());
286
287 if blind_signing {
290 log::debug!("[LEDGER] prepare_blind_signing");
292 log::debug!("[LEDGER] {:?} {:?}", input_bip32_indices, essence_hash);
293 ledger
294 .prepare_blind_signing(input_bip32_indices, essence_hash)
295 .map_err(Error::from)?;
296 } else {
297 #[allow(clippy::option_if_let_else)]
299 let (remainder_output, remainder_bip32) = match &prepared_transaction.remainder {
300 Some(remainder) => {
301 if let Some(chain) = remainder.chain {
302 (
303 Some(&remainder.output),
304 LedgerBIP32Index {
305 bip32_change: chain.change.harden().into(),
306 bip32_index: chain.address_index.harden().into(),
307 },
308 )
309 } else {
310 (None, LedgerBIP32Index::default())
311 }
312 }
313 None => (None, LedgerBIP32Index::default()),
314 };
315
316 let mut remainder_index = 0u16;
317 if let Some(remainder_output) = remainder_output {
318 match &prepared_transaction.essence {
319 TransactionEssence::Regular(essence) => {
320 for output in essence.outputs().iter() {
324 if !output.is_basic() {
325 log::debug!("[LEDGER] unsupported output");
326 return Err(Error::MiscError.into());
327 }
328
329 if remainder_output == output {
330 break;
331 }
332
333 remainder_index += 1;
334 }
335
336 if remainder_index as usize == essence.outputs().len() {
338 log::debug!("[LEDGER] remainder_index not found");
339 return Err(Error::MiscError.into());
340 }
341 }
342 }
343 }
344
345 log::debug!("[LEDGER] prepare signing");
347 log::debug!(
348 "[LEDGER] {:?} {:02x?} {} {} {:?}",
349 input_bip32_indices,
350 essence_bytes,
351 remainder_output.is_some(),
352 remainder_index,
353 remainder_bip32
354 );
355 ledger
356 .prepare_signing(
357 input_bip32_indices,
358 essence_bytes,
359 remainder_output.is_some(),
360 remainder_index,
361 remainder_bip32,
362 )
363 .map_err(Error::from)?;
364 }
365
366 log::debug!("[LEDGER] await user confirmation");
369 ledger.user_confirm().map_err(Error::from)?;
370
371 let signature_bytes = ledger.sign(input_len as u16).map_err(Error::from)?;
373 drop(ledger);
374 drop(lock);
375 let mut unpacker = SliceUnpacker::new(&signature_bytes);
376
377 let mut unlocks = Vec::new();
379 for _ in 0..input_len {
380 let unlock = Unlock::unpack::<_, true>(&mut unpacker, &())?;
381 match unlock {
383 Unlock::Signature(_) => {
384 if !unlocks.contains(&unlock) {
385 unlocks.push(unlock);
386 }
387 }
388 _ => unlocks.push(unlock),
390 }
391 }
392
393 if blind_signing {
396 unlocks = merge_unlocks(prepared_transaction, unlocks.into_iter(), time)?;
397 }
398
399 Ok(Unlocks::new(unlocks)?)
400 }
401
402 async fn sign_transaction(
403 &self,
404 prepared_transaction_data: PreparedTransactionData,
405 ) -> Result<TransactionPayload, Self::Error> {
406 super::default_sign_transaction(self, prepared_transaction_data).await
407 }
408}
409
410impl SecretManagerConfig for LedgerSecretManager {
411 type Config = bool;
412
413 fn to_config(&self) -> Option<Self::Config> {
414 Some(self.is_simulator)
415 }
416
417 fn from_config(config: &Self::Config) -> Result<Self, Self::Error> {
418 Ok(Self::new(*config))
419 }
420}
421
422pub fn needs_blind_signing(prepared_transaction: &PreparedTransactionData, buffer_size: usize) -> bool {
427 let TransactionEssence::Regular(essence) = &prepared_transaction.essence;
428
429 if !essence.outputs().iter().all(
430 |output| matches!(output, Output::Basic(o) if o.simple_deposit_address().is_some() && o.address().is_ed25519()),
431 ) {
432 return true;
433 }
434
435 let total_size = LedgerBIP32Index::default().packed_len() * prepared_transaction.inputs_data.len()
437 + prepared_transaction.essence.packed_len();
438
439 total_size > buffer_size
441}
442
443impl LedgerSecretManager {
444 pub fn new(is_simulator: bool) -> Self {
448 Self {
449 is_simulator,
450 non_interactive: false,
451 mutex: Mutex::new(()),
452 }
453 }
454
455 pub async fn get_ledger_nano_status(&self) -> LedgerNanoStatus {
457 log::debug!("get_ledger_nano_status");
458 let _lock = self.mutex.lock().await;
460 let transport_type = if self.is_simulator {
461 TransportTypes::TCP
462 } else {
463 TransportTypes::NativeHID
464 };
465
466 log::debug!("get_opened_app");
467 let app = match get_opened_app(&transport_type) {
468 Ok((name, version)) => Some(LedgerApp { name, version }),
469 _ => None,
470 };
471
472 log::debug!("get_app_config");
473 let (connected_, locked, blind_signing_enabled, device) =
477 get_app_config(&transport_type).map_or((false, None, false, None), |config| {
478 (
479 true,
480 Some(config.flags & (1 << 0) != 0),
482 config.flags & (1 << 1) != 0,
484 LedgerDeviceType::try_from(config.device).ok(),
485 )
486 });
487
488 log::debug!("get_buffer_size");
489 let buffer_size = get_buffer_size(&transport_type).ok();
491
492 let connected = if app.is_some() { true } else { connected_ };
496 LedgerNanoStatus {
497 connected,
498 locked,
499 blind_signing_enabled,
500 app,
501 device,
502 buffer_size,
503 }
504 }
505}
506
507fn merge_unlocks(
509 prepared_transaction_data: &PreparedTransactionData,
510 mut unlocks: impl Iterator<Item = Unlock>,
511 time: Option<u32>,
512) -> Result<Vec<Unlock>, Error> {
513 let hashed_essence = prepared_transaction_data.essence.hash();
515
516 let time = time.unwrap_or_else(|| unix_timestamp_now().as_secs() as u32);
517
518 let mut merged_unlocks = Vec::new();
519 let mut block_indexes = HashMap::<Address, usize>::new();
520
521 for (current_block_index, input) in prepared_transaction_data.inputs_data.iter().enumerate() {
523 let TransactionEssence::Regular(regular) = &prepared_transaction_data.essence;
525 let alias_transition = is_alias_transition(&input.output, *input.output_id(), regular.outputs(), None);
526 let (input_address, _) =
527 input
528 .output
529 .required_and_unlocked_address(time, input.output_metadata.output_id(), alias_transition)?;
530
531 match block_indexes.get(&input_address) {
533 Some(block_index) => match input_address {
535 Address::Alias(_alias) => merged_unlocks.push(Unlock::Alias(AliasUnlock::new(*block_index as u16)?)),
536 Address::Ed25519(_ed25519) => {
537 merged_unlocks.push(Unlock::Reference(ReferenceUnlock::new(*block_index as u16)?));
538 }
539 Address::Nft(_nft) => merged_unlocks.push(Unlock::Nft(NftUnlock::new(*block_index as u16)?)),
540 },
541 None => {
542 if !input_address.is_ed25519() {
546 return Err(Error::MissingInputWithEd25519Address);
547 }
548
549 let unlock = unlocks.next().ok_or(Error::MissingInputWithEd25519Address)?;
550
551 if let Unlock::Signature(signature_unlock) = &unlock {
552 let Signature::Ed25519(ed25519_signature) = signature_unlock.signature();
553 let ed25519_address = match input_address {
554 Address::Ed25519(ed25519_address) => ed25519_address,
555 _ => return Err(Error::MissingInputWithEd25519Address)?,
556 };
557 ed25519_signature.is_valid(&hashed_essence, &ed25519_address)?;
558 }
559
560 merged_unlocks.push(unlock);
561
562 block_indexes.insert(input_address, current_block_index);
565 }
566 }
567
568 match &input.output {
572 Output::Alias(alias_output) => block_indexes.insert(
573 Address::Alias(AliasAddress::new(alias_output.alias_id_non_null(input.output_id()))),
574 current_block_index,
575 ),
576 Output::Nft(nft_output) => block_indexes.insert(
577 Address::Nft(NftAddress::new(nft_output.nft_id_non_null(input.output_id()))),
578 current_block_index,
579 ),
580 _ => None,
581 };
582 }
583 Ok(merged_unlocks)
584}
585
586#[cfg(test)]
587mod tests {
588 use pretty_assertions::assert_eq;
589
590 use super::*;
591 use crate::{
592 client::{api::GetAddressesOptions, constants::IOTA_COIN_TYPE, secret::SecretManager},
593 types::block::address::ToBech32Ext,
594 };
595
596 #[tokio::test]
597 #[ignore = "requires ledger nano instance"]
598 async fn ed25519_address() {
599 let mut secret_manager = LedgerSecretManager::new(true);
600 secret_manager.non_interactive = true;
601
602 let addresses = SecretManager::LedgerNano(secret_manager)
603 .generate_ed25519_addresses(
604 GetAddressesOptions::default()
605 .with_coin_type(IOTA_COIN_TYPE)
606 .with_account_index(0)
607 .with_range(0..1),
608 )
609 .await
610 .unwrap();
611
612 assert_eq!(
613 addresses[0].to_bech32_unchecked("atoi").to_string(),
614 "atoi1qqdnv60ryxynaeyu8paq3lp9rkll7d7d92vpumz88fdj4l0pn5mru50gvd8"
615 );
616 }
617}