iota_sdk/client/secret/
ledger_nano.rs

1// Copyright 2022 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4//! Implementation of [`LedgerSecretManager`].
5//!
6//! Ledger status codes: <https://github.com/iotaledger/ledger-iota-app/blob/53c1f96d15f8b014ba8ba31a85f0401bb4d33e18/src/iota_io.h#L54>.
7
8use 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/// Ledger nano errors.
40#[derive(Debug, thiserror::Error)]
41pub enum Error {
42    /// Denied by User
43    #[error("denied by user")]
44    DeniedByUser,
45    /// Dongle Locked
46    #[error("ledger locked")]
47    DongleLocked,
48    /// Ledger Device not found
49    #[error("ledger device not found")]
50    DeviceNotFound,
51    /// Ledger Essence Too Large
52    #[error("ledger essence too large")]
53    EssenceTooLarge,
54    /// Ledger transport error
55    #[error("ledger transport error")]
56    MiscError,
57    /// Unsupported operation
58    #[error("unsupported operation")]
59    UnsupportedOperation,
60    /// Block error
61    #[error("{0}")]
62    Block(Box<crate::types::block::Error>),
63    /// Missing input with ed25519 address
64    #[error("missing input with ed25519 address")]
65    MissingInputWithEd25519Address,
66    /// Missing bip32 chain
67    #[error("missing bip32 chain")]
68    MissingBip32Chain,
69    /// Bip32 chain mismatch
70    #[error("Bip32 chain mismatch")]
71    Bip32ChainMismatch,
72    /// Unpack error
73    #[error("{0}")]
74    Unpack(#[from] packable::error::UnpackError<crate::types::block::Error, UnexpectedEOF>),
75    /// No available inputs provided
76    #[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
86// map most errors to a single error but there are some errors that
87// need special care.
88// LedgerDongleLocked: Ask the user to unlock the dongle
89// LedgerDeniedByUser: The user denied a signing
90// LedgerDeviceNotFound: No usable Ledger device was found
91// LedgerMiscError: Everything else.
92// LedgerEssenceTooLarge: Essence with bip32 input indices need more space then the internal buffer is big
93impl 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/// Secret manager that uses a Ledger hardware wallet.
107#[derive(Default, Debug)]
108pub struct LedgerSecretManager {
109    /// Specifies if a real Ledger hardware is used or only a simulator is used.
110    pub is_simulator: bool,
111    /// Specifies whether the wallet should be in non-interactive mode.
112    pub non_interactive: bool,
113    /// Mutex to prevent multiple simultaneous requests to a ledger.
114    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        // https://github.com/satoshilabs/slips/blob/master/slip-0044.md
137        // current ledger app only supports IOTA_COIN_TYPE, SHIMMER_COIN_TYPE and TESTNET_COIN_TYPE
138        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        // lock the mutex to prevent multiple simultaneous requests to a ledger
152        let lock = self.mutex.lock().await;
153
154        // get ledger
155        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    /// Ledger only allows signing messages of 32 bytes, anything else is unsupported and will result in an error.
182    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        // Lock the mutex to prevent multiple simultaneous requests to a ledger.
197        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        // Show essence to user, if denied by user, it returns with `DeniedByUser` Error.
213        log::debug!("[LEDGER] await user confirmation");
214        ledger.user_confirm().map_err(Error::from)?;
215
216        // Sign.
217        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        // Unpack and return signature.
225        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            // coin_type and account_index should be the same in each output
254            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        // pack essence and hash into vec
273        let essence_bytes = prepared_transaction.essence.pack_to_vec();
274        let essence_hash = prepared_transaction.essence.hash().to_vec();
275
276        // lock the mutex to prevent multiple simultaneous requests to a ledger
277        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 essence + bip32 input indices are larger than the buffer size or the essence contains
288        // features / types that are not supported blind signing will be needed
289        if blind_signing {
290            // prepare signing
291            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            // figure out the remainder output and bip32 index (if there is one)
298            #[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                        // Find the index of the remainder in the transaction because it is not always the last output.
321                        // The index within the transaction and the bip32 index will be validated by the hardware
322                        // wallet.
323                        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                        // Was index found?
337                        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            // prepare signing
346            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        // show essence to user
367        // if denied by user, it returns with `DeniedByUser` Error
368        log::debug!("[LEDGER] await user confirmation");
369        ledger.user_confirm().map_err(Error::from)?;
370
371        // sign
372        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        // unpack signature to unlocks
378        let mut unlocks = Vec::new();
379        for _ in 0..input_len {
380            let unlock = Unlock::unpack::<_, true>(&mut unpacker, &())?;
381            // The ledger nano can return the same SignatureUnlocks multiple times, so only insert it once
382            match unlock {
383                Unlock::Signature(_) => {
384                    if !unlocks.contains(&unlock) {
385                        unlocks.push(unlock);
386                    }
387                }
388                // Multiple reference unlocks with the same index are allowed
389                _ => unlocks.push(unlock),
390            }
391        }
392
393        // With blind signing the ledger only returns SignatureUnlocks, so we might have to merge them with
394        // Alias/Nft/Reference unlocks
395        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
422/// the Ledger Nano S(+)/X app can present the user a detailed view of the transaction before it
423/// is signed but only with BasicOutputs, without extra-features and if the Essence is not too large.
424/// If criteria are not met, blind signing is needed.
425/// This method finds out if we have to switch to blind signing mode.
426pub 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    // check if essence + bip32 indices fit into the buffer of the device
436    let total_size = LedgerBIP32Index::default().packed_len() * prepared_transaction.inputs_data.len()
437        + prepared_transaction.essence.packed_len();
438
439    // return true if too large
440    total_size > buffer_size
441}
442
443impl LedgerSecretManager {
444    /// Creates a [`LedgerSecretManager`].
445    ///
446    /// To use a Ledger Speculos simulator, pass `true` to `is_simulator`; `false` otherwise.
447    pub fn new(is_simulator: bool) -> Self {
448        Self {
449            is_simulator,
450            non_interactive: false,
451            mutex: Mutex::new(()),
452        }
453    }
454
455    /// Get Ledger hardware status.
456    pub async fn get_ledger_nano_status(&self) -> LedgerNanoStatus {
457        log::debug!("get_ledger_nano_status");
458        // lock the mutex
459        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        // if IOTA or Shimmer app is opened, the call will always succeed, returning information like
474        // device, debug-flag, version number, lock-state but here we only are interested in a
475        // successful call and the locked-flag
476        let (connected_, locked, blind_signing_enabled, device) =
477            get_app_config(&transport_type).map_or((false, None, false, None), |config| {
478                (
479                    true,
480                    // locked flag
481                    Some(config.flags & (1 << 0) != 0),
482                    // blind signing enabled flag
483                    config.flags & (1 << 1) != 0,
484                    LedgerDeviceType::try_from(config.device).ok(),
485                )
486            });
487
488        log::debug!("get_buffer_size");
489        // get buffer size of connected device
490        let buffer_size = get_buffer_size(&transport_type).ok();
491
492        // We get the app info also if not the iota app is open, but another one
493        // connected_ is in this case false, even tough the ledger is connected, that's why we always return true if we
494        // got the app
495        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
507// Merge signature unlocks with Alias/Nft/Reference unlocks
508fn merge_unlocks(
509    prepared_transaction_data: &PreparedTransactionData,
510    mut unlocks: impl Iterator<Item = Unlock>,
511    time: Option<u32>,
512) -> Result<Vec<Unlock>, Error> {
513    // The hashed_essence gets signed
514    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    // Assuming inputs_data is ordered by address type
522    for (current_block_index, input) in prepared_transaction_data.inputs_data.iter().enumerate() {
523        // Get the address that is required to unlock the input
524        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        // Check if we already added an [Unlock] for this address
532        match block_indexes.get(&input_address) {
533            // If we already have an [Unlock] for this address, add a [Unlock] based on the address type
534            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                // We can only sign ed25519 addresses and block_indexes needs to contain the alias or nft
543                // address already at this point, because the reference index needs to be lower
544                // than the current block index
545                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                // Add the ed25519 address to the block_indexes, so it gets referenced if further inputs have
563                // the same address in their unlock condition
564                block_indexes.insert(input_address, current_block_index);
565            }
566        }
567
568        // When we have an alias or Nft output, we will add their alias or nft address to block_indexes,
569        // because they can be used to unlock outputs via [Unlock::Alias] or [Unlock::Nft],
570        // that have the corresponding alias or nft address in their unlock condition
571        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}