Skip to main content

async_hwi/
ledger.rs

1use std::convert::TryFrom;
2use std::default::Default;
3use std::error::Error;
4use std::net::{IpAddr, Ipv4Addr, SocketAddr};
5
6use async_trait::async_trait;
7use bitcoin::{
8    bip32::{DerivationPath, Fingerprint, Xpub},
9    psbt::Psbt,
10};
11use ledger_bitcoin_client::psbt::PartialSignature;
12
13use ledger_apdu::APDUAnswer;
14use ledger_transport_hidapi::TransportNativeHID;
15use tokio::{
16    io::{AsyncReadExt, AsyncWriteExt},
17    net::TcpStream,
18    sync::Mutex,
19};
20
21use ledger_bitcoin_client::{
22    apdu::{APDUCommand, StatusWord},
23    async_client::BitcoinClient,
24    error::BitcoinClientError,
25    wallet::Version as WalletVersion,
26    WalletPolicy, WalletPubKey,
27};
28
29use crate::{
30    parse_version, utils, AddressScript, DeviceKind, Error as HWIError, CHANGE_INDEX, HWI,
31    RECV_INDEX,
32};
33
34pub use hidapi::{DeviceInfo, HidApi};
35pub use ledger_bitcoin_client::async_client::Transport;
36
37#[derive(Default)]
38struct CommandOptions {
39    wallet: Option<(WalletPolicy, Option<[u8; 32]>)>,
40    display_xpub: bool,
41}
42
43pub struct Ledger<T: Transport> {
44    client: BitcoinClient<T>,
45    options: CommandOptions,
46    kind: DeviceKind,
47}
48
49impl<T: Transport> Ledger<T> {
50    pub fn display_xpub(mut self, display: bool) -> Result<Self, HWIError> {
51        self.options.display_xpub = display;
52        Ok(self)
53    }
54
55    pub fn with_wallet(
56        mut self,
57        name: impl Into<String>,
58        policy: &str,
59        hmac: Option<[u8; 32]>,
60    ) -> Result<Self, HWIError> {
61        let (descriptor_template, keys) = utils::extract_keys_and_template::<WalletPubKey>(policy)?;
62        let wallet = WalletPolicy::new(name.into(), WalletVersion::V2, descriptor_template, keys);
63        self.options.wallet = Some((wallet, hmac));
64        Ok(self)
65    }
66}
67
68/// TODO: remove
69impl<T: Transport> std::fmt::Debug for Ledger<T> {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        f.debug_struct("Ledger").finish()
72    }
73}
74
75impl<T: 'static + Transport + Sync + Send> From<Ledger<T>> for Box<dyn HWI + Send> {
76    fn from(s: Ledger<T>) -> Box<dyn HWI + Send> {
77        Box::new(s)
78    }
79}
80
81#[async_trait]
82impl<T: Transport + Sync + Send> HWI for Ledger<T> {
83    fn device_kind(&self) -> DeviceKind {
84        self.kind
85    }
86
87    async fn get_version(&self) -> Result<super::Version, HWIError> {
88        let (_, version, _) = self.client.get_version().await?;
89        Ok(parse_version(&version)?)
90    }
91
92    async fn get_master_fingerprint(&self) -> Result<Fingerprint, HWIError> {
93        Ok(self.client.get_master_fingerprint().await?)
94    }
95
96    async fn get_extended_pubkey(&self, path: &DerivationPath) -> Result<Xpub, HWIError> {
97        Ok(self
98            .client
99            .get_extended_pubkey(path, self.options.display_xpub)
100            .await?)
101    }
102
103    async fn display_address(&self, script: &AddressScript) -> Result<(), HWIError> {
104        match script {
105            AddressScript::P2TR(path) => {
106                let children = utils::bip86_path_child_numbers(path.clone())?;
107                let (hardened_children, normal_children) = children.split_at(3);
108                let path = DerivationPath::from(hardened_children);
109                let fg = self.get_master_fingerprint().await?;
110                let xpub = self.get_extended_pubkey(&path).await?;
111                let policy = format!("tr({}/**)", key_string_from_parts(fg, path, xpub));
112                let (descriptor_template, keys) =
113                    utils::extract_keys_and_template::<WalletPubKey>(&policy)?;
114                let wallet =
115                    WalletPolicy::new("".into(), WalletVersion::V2, descriptor_template, keys);
116
117                if ![RECV_INDEX, CHANGE_INDEX].contains(&normal_children[0]) {
118                    return Err(HWIError::Bip86ChangeIndex);
119                }
120                self.client
121                    .get_wallet_address(
122                        &wallet,
123                        None,
124                        normal_children[0] == CHANGE_INDEX,
125                        normal_children[1].into(),
126                        true,
127                    )
128                    .await?;
129            }
130            AddressScript::Miniscript { index, change } => {
131                let (policy, hmac) = &self
132                    .options
133                    .wallet
134                    .as_ref()
135                    .ok_or_else(|| HWIError::MissingPolicy)?;
136                self.client
137                    .get_wallet_address(policy, hmac.as_ref(), *change, *index, true)
138                    .await?;
139            }
140        }
141        Ok(())
142    }
143
144    async fn register_wallet(
145        &self,
146        name: &str,
147        policy: &str,
148    ) -> Result<Option<[u8; 32]>, HWIError> {
149        let (descriptor_template, keys) = utils::extract_keys_and_template::<WalletPubKey>(policy)?;
150        let wallet = WalletPolicy::new(
151            name.to_string(),
152            WalletVersion::V2,
153            descriptor_template,
154            keys,
155        );
156        let (_id, hmac) = self.client.register_wallet(&wallet).await?;
157        Ok(Some(hmac))
158    }
159
160    async fn is_wallet_registered(&self, name: &str, policy: &str) -> Result<bool, HWIError> {
161        if let Some((wallet, hmac)) = &self.options.wallet {
162            let (descriptor_template, keys) =
163                utils::extract_keys_and_template::<WalletPubKey>(policy)?;
164            Ok(hmac.is_some()
165                && name == wallet.name
166                && descriptor_template == wallet.descriptor_template
167                && keys == wallet.keys)
168        } else {
169            Ok(false)
170        }
171    }
172
173    async fn sign_tx(&self, psbt: &mut Psbt) -> Result<(), HWIError> {
174        if let Some((policy, hmac)) = &self.options.wallet {
175            let sigs = self.client.sign_psbt(psbt, policy, hmac.as_ref()).await?;
176            for (i, sig) in sigs {
177                let input = psbt.inputs.get_mut(i).ok_or(HWIError::DeviceDidNotSign)?;
178                match sig {
179                    PartialSignature::Sig(key, sig) => {
180                        input.partial_sigs.insert(key, sig);
181                    }
182                    PartialSignature::TapScriptSig(key, Some(tapleaf_hash), sig) => {
183                        input.tap_script_sigs.insert((key, tapleaf_hash), sig);
184                    }
185                    PartialSignature::TapScriptSig(_, None, sig) => {
186                        input.tap_key_sig = Some(sig);
187                    }
188                }
189            }
190            Ok(())
191        } else {
192            // Ledger cannot sign without policy.
193            Err(HWIError::UnimplementedMethod)
194        }
195    }
196}
197
198fn key_string_from_parts(fg: Fingerprint, path: DerivationPath, xpub: Xpub) -> String {
199    format!(
200        "[{}/{}]{}",
201        fg,
202        path.to_string().trim_start_matches("m/"),
203        xpub
204    )
205}
206
207impl Ledger<TransportHID> {
208    pub fn enumerate(api: &HidApi) -> impl Iterator<Item = &DeviceInfo> {
209        TransportNativeHID::list_ledgers(api)
210    }
211
212    pub fn connect(api: &HidApi, device: &DeviceInfo) -> Result<Self, HWIError> {
213        let hid =
214            TransportNativeHID::open_device(api, device).map_err(|_| HWIError::DeviceNotFound)?;
215        Ok(Ledger {
216            client: BitcoinClient::new(TransportHID(hid)),
217            options: CommandOptions::default(),
218            kind: DeviceKind::Ledger,
219        })
220    }
221
222    pub fn try_connect_hid() -> Result<Self, HWIError> {
223        let hid = TransportNativeHID::new(&HidApi::new().map_err(|_| HWIError::DeviceNotFound)?)
224            .map_err(|_| HWIError::DeviceNotFound)?;
225        Ok(Ledger {
226            client: BitcoinClient::new(TransportHID(hid)),
227            options: CommandOptions::default(),
228            kind: DeviceKind::Ledger,
229        })
230    }
231}
232
233/// Transport with the Ledger device.
234pub struct TransportHID(TransportNativeHID);
235
236#[async_trait]
237impl Transport for TransportHID {
238    type Error = Box<dyn Error>;
239    async fn exchange(&self, cmd: &APDUCommand) -> Result<(StatusWord, Vec<u8>), Self::Error> {
240        self.0
241            .exchange(&ledger_apdu::APDUCommand {
242                ins: cmd.ins,
243                cla: cmd.cla,
244                p1: cmd.p1,
245                p2: cmd.p2,
246                data: cmd.data.clone(),
247            })
248            .map(|answer| {
249                (
250                    StatusWord::try_from(answer.retcode()).unwrap_or(StatusWord::Unknown),
251                    answer.data().to_vec(),
252                )
253            })
254            .map_err(|e| e.into())
255    }
256}
257
258pub type LedgerSimulator = Ledger<TransportTcp>;
259
260impl LedgerSimulator {
261    pub async fn try_connect() -> Result<Self, HWIError> {
262        let transport = TransportTcp::new()
263            .await
264            .map_err(|_| HWIError::DeviceNotFound)?;
265        Ok(Ledger {
266            client: BitcoinClient::new(transport),
267            options: CommandOptions::default(),
268            kind: DeviceKind::LedgerSimulator,
269        })
270    }
271}
272
273/// Transport to communicate with the Ledger Speculos simulator.
274pub struct TransportTcp {
275    connection: Mutex<TcpStream>,
276}
277
278impl TransportTcp {
279    pub async fn new() -> Result<Self, Box<dyn Error>> {
280        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 9999);
281        let stream = TcpStream::connect(addr).await?;
282        Ok(Self {
283            connection: Mutex::new(stream),
284        })
285    }
286}
287
288#[async_trait]
289impl Transport for TransportTcp {
290    type Error = Box<dyn Error>;
291    async fn exchange(&self, command: &APDUCommand) -> Result<(StatusWord, Vec<u8>), Self::Error> {
292        let mut stream = self.connection.lock().await;
293        let command_bytes = command.encode();
294
295        let mut req = vec![0u8; command_bytes.len() + 4];
296        req[..4].copy_from_slice(&(command_bytes.len() as u32).to_be_bytes());
297        req[4..].copy_from_slice(&command_bytes);
298        stream.write_all(&req).await?;
299
300        let mut buff = [0u8; 4];
301        let len = match stream.read(&mut buff).await? {
302            4 => u32::from_be_bytes(buff),
303            _ => return Err("Invalid Length".into()),
304        };
305
306        let mut resp = vec![0u8; len as usize + 2];
307        stream.read_exact(&mut resp).await?;
308        let answer = APDUAnswer::from_answer(resp).map_err(|_| "Invalid Answer")?;
309        Ok((
310            StatusWord::try_from(answer.retcode()).unwrap_or(StatusWord::Unknown),
311            answer.data().to_vec(),
312        ))
313    }
314}
315
316impl<T: core::fmt::Debug> From<BitcoinClientError<T>> for HWIError {
317    fn from(e: BitcoinClientError<T>) -> HWIError {
318        if let BitcoinClientError::Device { status, .. } = e {
319            if status == StatusWord::Deny {
320                return HWIError::UserRefused;
321            }
322        };
323        HWIError::Device(format!("{:#?}", e))
324    }
325}
326
327#[cfg(test)]
328mod tests {
329
330    use super::*;
331    use std::str::FromStr;
332
333    // This is a foolproof test in case next rust-bitcoin version introduces again the m/
334    #[test]
335    fn test_key_string_from_parts() {
336        let path = DerivationPath::from_str("m/48'/1'/0'/2'").unwrap();
337        let fg = Fingerprint::from_hex("aabbccdd").unwrap();
338        let xpub = Xpub::from_str("tpubDExA3EC3iAsPxPhFn4j6gMiVup6V2eH3qKyk69RcTc9TTNRfFYVPad8bJD5FCHVQxyBT4izKsvr7Btd2R4xmQ1hZkvsqGBaeE82J71uTK4N").unwrap();
339        assert_eq!(key_string_from_parts(fg, path, xpub), "[aabbccdd/48'/1'/0'/2']tpubDExA3EC3iAsPxPhFn4j6gMiVup6V2eH3qKyk69RcTc9TTNRfFYVPad8bJD5FCHVQxyBT4izKsvr7Btd2R4xmQ1hZkvsqGBaeE82J71uTK4N");
340    }
341}