ledger_filecoin/
lib.rs

1/*******************************************************************************
2*   (c) 2020 ZondaX GmbH
3*
4*  Licensed under the Apache License, Version 2.0 (the "License");
5*  you may not use this file except in compliance with the License.
6*  You may obtain a copy of the License at
7*
8*      http://www.apache.org/licenses/LICENSE-2.0
9*
10*  Unless required by applicable law or agreed to in writing, software
11*  distributed under the License is distributed on an "AS IS" BASIS,
12*  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13*  See the License for the specific language governing permissions and
14*  limitations under the License.
15********************************************************************************/
16//! Support library for Filecoin Ledger Nano S/X apps
17
18#![deny(warnings, trivial_casts, trivial_numeric_casts)]
19#![deny(unused_import_braces, unused_qualifications)]
20#![deny(missing_docs)]
21#![doc(html_root_url = "https://docs.rs/ledger-filecoin/0.1.0")]
22
23use ledger_transport::{APDUCommand, APDUErrorCode, Exchange};
24use ledger_zondax_generic::{App, AppExt, ChunkPayloadType, Version};
25
26pub use ledger_zondax_generic::LedgerAppError;
27
28mod params;
29use params::{InstructionCode, CLA};
30
31use std::str;
32
33/// Public Key Length
34const PK_LEN: usize = 65;
35
36/// Ledger App Error
37#[derive(Debug, thiserror::Error)]
38pub enum FilError<E>
39where
40    E: std::error::Error,
41{
42    #[error("Ledger | {0}")]
43    /// Common Ledger errors
44    Ledger(#[from] LedgerAppError<E>),
45
46    /// Device related errors
47    #[error("Secp256k1 error: {0}")]
48    Secp256k1(#[from] k256::elliptic_curve::Error),
49
50    /// Device related errors
51    #[error("Ecdsa error: {0}")]
52    Ecdsa(#[from] k256::ecdsa::Error),
53}
54
55/// Filecoin App
56pub struct FilecoinApp<E> {
57    apdu_transport: E,
58}
59
60impl<E: Exchange> App for FilecoinApp<E> {
61    const CLA: u8 = CLA;
62}
63
64/// FilecoinApp address (includes pubkey and the corresponding ss58 address)
65pub struct Address {
66    /// Public Key
67    pub public_key: k256::PublicKey,
68
69    /// Address byte format
70    pub addr_byte: [u8; 21],
71
72    /// Address string format
73    pub addr_string: String,
74}
75
76/// FilecoinApp signature (includes R, S, V and der format)
77pub struct Signature {
78    /// r value
79    pub r: [u8; 32],
80
81    /// s value
82    pub s: [u8; 32],
83
84    /// v value
85    pub v: u8,
86
87    /// der signature
88    pub sig: k256::ecdsa::Signature,
89}
90
91/// BIP44 Path
92pub struct BIP44Path {
93    /// Purpose
94    pub purpose: u32,
95    /// Coin
96    pub coin: u32,
97    /// Account
98    pub account: u32,
99    /// Change
100    pub change: u32,
101    /// Address Index
102    pub index: u32,
103}
104
105impl BIP44Path {
106    /// Serialize a [`BIP44Path`] in the format used in the app
107    pub fn serialize_bip44(&self) -> Vec<u8> {
108        use byteorder::{LittleEndian, WriteBytesExt};
109        let mut m = Vec::new();
110
111        m.write_u32::<LittleEndian>(self.purpose).unwrap();
112        m.write_u32::<LittleEndian>(self.coin).unwrap();
113        m.write_u32::<LittleEndian>(self.account).unwrap();
114        m.write_u32::<LittleEndian>(self.change).unwrap();
115        m.write_u32::<LittleEndian>(self.index).unwrap();
116
117        m
118    }
119}
120
121impl<E> FilecoinApp<E> {
122    /// Create a new [`FilecoinApp`] with the given transport
123    pub const fn new(transport: E) -> Self {
124        FilecoinApp {
125            apdu_transport: transport,
126        }
127    }
128}
129
130impl<E> FilecoinApp<E>
131where
132    E: Exchange + Send + Sync,
133    E::Error: std::error::Error,
134{
135    /// Retrieve the app version
136    pub async fn version(&self) -> Result<Version, FilError<E::Error>> {
137        <Self as AppExt<E>>::get_version(&self.apdu_transport)
138            .await
139            .map_err(Into::into)
140    }
141
142    /// Retrieves the public key and address
143    pub async fn address(
144        &self,
145        path: &BIP44Path,
146        require_confirmation: bool,
147    ) -> Result<Address, FilError<E::Error>> {
148        let serialized_path = path.serialize_bip44();
149        let p1 = if require_confirmation { 1 } else { 0 };
150
151        let command = APDUCommand {
152            cla: CLA,
153            ins: InstructionCode::GetAddrSecp256k1 as _,
154            p1,
155            p2: 0x00,
156            data: serialized_path,
157        };
158
159        let response = self
160            .apdu_transport
161            .exchange(&command)
162            .await
163            .map_err(LedgerAppError::TransportError)?;
164
165        let response_data = response.data();
166        match response.error_code() {
167            Ok(APDUErrorCode::NoError) if response_data.len() < PK_LEN => {
168                return Err(FilError::Ledger(LedgerAppError::InvalidPK))
169            }
170            Ok(APDUErrorCode::NoError) => {}
171            Ok(err) => {
172                return Err(FilError::Ledger(LedgerAppError::AppSpecific(
173                    err as _,
174                    err.description(),
175                )))
176            }
177            Err(err) => {
178                return Err(FilError::Ledger(LedgerAppError::AppSpecific(
179                    err,
180                    "[APDU_ERROR] Unknown".to_string(),
181                )))
182            }
183        }
184
185        let public_key = k256::PublicKey::from_sec1_bytes(&response_data[..PK_LEN])?;
186        let mut addr_byte = [Default::default(); 21];
187        addr_byte.copy_from_slice(&response_data[PK_LEN + 1..PK_LEN + 1 + 21]);
188        let tmp =
189            str::from_utf8(&response_data[PK_LEN + 2 + 21..]).map_err(|_| LedgerAppError::Utf8)?;
190        let addr_string = tmp.to_owned();
191
192        Ok(Address {
193            public_key,
194            addr_byte,
195            addr_string,
196        })
197    }
198
199    /// Sign a transaction
200    pub async fn sign(
201        &self,
202        path: &BIP44Path,
203        message: &[u8],
204    ) -> Result<Signature, FilError<E::Error>> {
205        let bip44path = path.serialize_bip44();
206
207        let start_command = APDUCommand {
208            cla: CLA,
209            ins: InstructionCode::SignSecp256k1 as _,
210            p1: ChunkPayloadType::Init as u8,
211            p2: 0x00,
212            data: bip44path,
213        };
214
215        let response =
216            <Self as AppExt<E>>::send_chunks(&self.apdu_transport, start_command, message).await?;
217
218        let response_data = response.data();
219        match response.error_code() {
220            Ok(APDUErrorCode::NoError) if response_data.is_empty() => {
221                return Err(FilError::Ledger(LedgerAppError::NoSignature))
222            }
223            // Last response should contain the answer
224            Ok(APDUErrorCode::NoError) if response_data.len() < 3 => {
225                return Err(FilError::Ledger(LedgerAppError::InvalidSignature))
226            }
227            Ok(APDUErrorCode::NoError) => {}
228            Ok(err) => {
229                return Err(FilError::Ledger(LedgerAppError::AppSpecific(
230                    err as _,
231                    err.description(),
232                )))
233            }
234            Err(err) => {
235                return Err(FilError::Ledger(LedgerAppError::AppSpecific(
236                    err,
237                    "[APDU_ERROR] Unknown".to_string(),
238                )))
239            }
240        }
241
242        let mut r = [0; 32];
243        r.copy_from_slice(&response_data[..32]);
244
245        let mut s = [0; 32];
246        s.copy_from_slice(&response_data[32..64]);
247
248        let v = response_data[64];
249
250        let sig = k256::ecdsa::Signature::from_der(&response_data[65..])?;
251
252        let signature = Signature { r, s, v, sig };
253
254        Ok(signature)
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::BIP44Path;
261
262    #[test]
263    fn bip44() {
264        let path = BIP44Path {
265            purpose: 0x8000_0000 | 0x2c,
266            coin: 0x8000_0000 | 1,
267            account: 0x1234,
268            change: 0,
269            index: 0x5678,
270        };
271        let serialized_path = path.serialize_bip44();
272        assert_eq!(serialized_path.len(), 20);
273        assert_eq!(
274            hex::encode(&serialized_path),
275            "2c00008001000080341200000000000078560000"
276        );
277    }
278}