1use std::str::from_utf8;
2use std::sync::{Arc, Mutex};
3use crate::{
4 errors::HWKeyError,
5 ledger::{
6 apdu::ApduBuilder,
7 app::{AsChainCode, AsExtendedKey, AsPubkey, LedgerApp, PubkeyAddressApp},
8 comm::{sendrecv, LedgerTransport},
9 commons::as_compact
10 },
11};
12use std::convert::TryFrom;
13use hdpath::{AccountHDPath, HDPath, Purpose};
14use bitcoin::{
15 secp256k1::PublicKey,
16 bip32::ChainCode
17};
18use crate::ledger::connect::direct::CHUNK_SIZE;
19
20pub const ECDSA_SIGNATURE_BYTES: usize = 65;
22
23const COMMAND_GET_ADDRESS: u8 = 0x02;
24const COMMAND_SIGN_TRANSACTION: u8 = 0x04;
25const COMMAND_SIGN_MESSAGE: u8 = 0x08;
26const COMMAND_APP_CONFIG: u8 = 0x06;
27const COMMAND_SIGN_EIP712: u8 = 0x0C;
28
29#[derive(Clone, Eq, PartialEq, Hash)]
34pub struct SignatureBytes([u8; ECDSA_SIGNATURE_BYTES]);
35
36impl SignatureBytes {
37
38 pub fn from_ledger_bytes(bytes: &[u8]) -> Result<Self, HWKeyError> {
40 if bytes.len() != ECDSA_SIGNATURE_BYTES {
41 return Err(HWKeyError::CryptoError(format!(
42 "Invalid signature length. Expected: {}, received: {}",
43 ECDSA_SIGNATURE_BYTES, bytes.len()
44 )));
45 }
46 let mut val: [u8; ECDSA_SIGNATURE_BYTES] = [0; ECDSA_SIGNATURE_BYTES];
47 val[0..64].copy_from_slice(&bytes[1..65]);
48 val[64] = bytes[0];
49 Ok(SignatureBytes(val))
50 }
51
52 pub fn to_vec(&self) -> Vec<u8> {
53 self.0.to_vec()
54 }
55
56 pub fn to_ledger_bytes(&self) -> [u8; ECDSA_SIGNATURE_BYTES] {
58 let mut vrs = [0; ECDSA_SIGNATURE_BYTES];
59 vrs[1..65].copy_from_slice(&self.0[0..64]);
60 vrs[0] = self.0[64];
61 vrs
62 }
63}
64
65impl std::fmt::Debug for SignatureBytes {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 write!(f, "0x{}", hex::encode(self.0))
68 }
69}
70
71impl AsRef<[u8]> for SignatureBytes {
72 fn as_ref(&self) -> &[u8] {
73 &self.0
74 }
75}
76
77impl AsRef<[u8; ECDSA_SIGNATURE_BYTES]> for SignatureBytes {
78 fn as_ref(&self) -> &[u8; ECDSA_SIGNATURE_BYTES] {
79 &self.0
80 }
81}
82
83pub struct EthereumApp {
84 ledger: Arc<Mutex<dyn LedgerTransport>>
85}
86
87#[derive(Debug, Clone, Eq, PartialEq)]
88pub struct AddressResponse {
89 pub pubkey: PublicKey,
90 pub address: String,
91 pub chaincode: ChainCode
92}
93
94impl AsPubkey for AddressResponse {
95 fn as_pubkey(&self) -> &PublicKey {
96 &self.pubkey
97 }
98}
99
100impl AsChainCode for AddressResponse {
101 fn as_chaincode(&self) -> &ChainCode {
102 &self.chaincode
103 }
104}
105
106impl AsExtendedKey for AddressResponse {}
107
108impl TryFrom<Vec<u8>> for AddressResponse {
109 type Error = HWKeyError;
110
111 fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
112 if value.is_empty() {
113 return Err(HWKeyError::EncodingError("Empty data".to_string()))
114 }
115 let pubkey_len = value[0] as usize;
116 if 1 + pubkey_len > value.len() {
117 return Err(HWKeyError::EncodingError(
118 format!("Pubkey cutoff. {:?} > {:?} for {:?}", 1 + pubkey_len, value.len(), pubkey_len)
119 ))
120 }
121 let pubkey = &value[1..pubkey_len+1];
122 let pubkey = PublicKey::from_slice(pubkey)
123 .map_err(|_| HWKeyError::CryptoError("Invalid public key".to_string()))?;
124 let pubkey_comp = as_compact(&pubkey)?;
125
126 let address_len = value[pubkey_len + 1] as usize;
127 let address_start = 1 + pubkey_len + 1;
128 let address_end = address_start + address_len;
129 if address_end > value.len() {
130 return Err(HWKeyError::EncodingError(
131 format!("Address cutoff. {:?} > {:?} as {:?}..{:?} for {:?}", address_end, value.len(), address_start, address_end, address_len)
132 ))
133 }
134 let address = &value[address_start..address_end];
135 let address = from_utf8(address)
136 .map(|a| a.to_string())
137 .map(|a| if a.starts_with("0x") { a } else { format!("0x{}", a)} )
138 .map_err(|e| HWKeyError::EncodingError(format!("Can't parse address: {}", e)))?;
139
140 let chaincode_len = 32_usize;
141 let chaincode_start = address_end;
142 let chaincode_end = chaincode_start + chaincode_len;
143 if chaincode_end > value.len() {
144 return Err(HWKeyError::EncodingError(
145 format!("Chaincode cutoff. {:?} > {:?}", chaincode_end, value.len())
146 ))
147 }
148 let chaincode = value[chaincode_start..chaincode_end].to_vec();
149 let chaincode = ChainCode::try_from(chaincode.as_slice()).unwrap();
150
151 Ok(AddressResponse {
152 pubkey: pubkey_comp, address, chaincode
153 })
154 }
155}
156
157#[derive(Debug, Clone, Eq, PartialEq)]
158pub struct AppVersion {
159 pub data_sign_enabled: bool,
161 pub external_erc20: bool,
163 pub version_major: u8,
164 pub version_minor: u8,
165 pub version_patch: u8,
166}
167
168impl TryFrom<Vec<u8>> for AppVersion {
169 type Error = ();
170
171 fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
172 if value.len() < 4 {
173 return Err(())
174 }
175 let flags = value[0];
176 Ok(AppVersion {
177 data_sign_enabled: flags & 0x01 > 0,
178 external_erc20: flags & 0x02 > 0,
179 version_major: value[1],
180 version_minor: value[2],
181 version_patch: value[3]
182 })
183 }
184}
185
186impl EthereumApp {
187
188 fn chunk_data<'a>(&self, data: &'a [u8], hd_path: &dyn HDPath) -> (&'a [u8], &'a [u8]) {
199 match data.len() {
200 0..=CHUNK_SIZE => (data, &[]),
201 _ => data.split_at(CHUNK_SIZE - hd_path.to_bytes().len()),
202 }
203 }
204
205 fn send_chunked_data(&self, command: u8, hd_path: &dyn HDPath, data: &[u8]) -> Result<Vec<u8>, HWKeyError> {
223 let (init, cont) = self.chunk_data(data, hd_path);
224
225 let init_apdu = ApduBuilder::new(command)
226 .with_p1(0x00)
227 .with_data(hd_path.to_bytes().as_slice())
228 .with_data(init)
229 .build();
230
231 let handle = self.ledger.lock().unwrap();
232 let mut res = sendrecv(&*handle, &init_apdu)?;
233
234 for chunk in cont.chunks(CHUNK_SIZE) {
235 let apdu_cont = ApduBuilder::new(command)
236 .with_p1(0x80)
237 .with_data(chunk)
238 .build();
239 res = sendrecv(&*handle, &apdu_cont)?;
240 }
241
242 Ok(res)
243 }
244
245 pub fn get_address(&self, hd_path: &dyn HDPath, confirm: bool) -> Result<AddressResponse, HWKeyError> {
251 let apdu = ApduBuilder::new(COMMAND_GET_ADDRESS)
252 .with_p1(if confirm {0x01} else {0x00})
255 .with_p2(0x01)
257 .with_data(hd_path.to_bytes().as_slice())
258 .build();
259
260 let ledger = self.ledger.lock().unwrap();
261
262
263 sendrecv(&*ledger, &apdu)
265 .and_then(AddressResponse::try_from)
266 }
267
268 pub fn sign_transaction(
282 &self,
283 tx: &[u8],
284 hd_path: &dyn HDPath,
285 ) -> Result<SignatureBytes, HWKeyError> {
286
287 let res = self.send_chunked_data(COMMAND_SIGN_TRANSACTION, hd_path, tx)?;
290 let signature = SignatureBytes::from_ledger_bytes(res.as_slice())?;
291 debug!("Received signature: {:?}", signature);
292 Ok(signature)
293 }
294
295 pub fn sign_message_erc191(
314 &self,
315 message: String,
316 hd_path: &dyn HDPath,
317 ) -> Result<SignatureBytes, HWKeyError> {
318
319 let message = message.as_bytes();
320 let mut message_data = Vec::<u8>::with_capacity(message.len() + 4);
322 message_data.extend_from_slice((message.len() as u32).to_be_bytes().as_slice());
323 message_data.extend_from_slice(message);
324 let message_data = message_data.as_slice();
325
326 let res = self.send_chunked_data(COMMAND_SIGN_MESSAGE, hd_path, message_data)?;
327 let signature = SignatureBytes::from_ledger_bytes(res.as_slice())?;
328 debug!("Received signature: {:?}", signature);
329 Ok(signature)
330 }
331
332 pub fn sign_message_eip712(
355 &self,
356 domain_hash: &[u8; 32],
357 message_hash: &[u8; 32],
358 hd_path: &dyn HDPath,
359 ) -> Result<SignatureBytes, HWKeyError> {
360 let mut data = Vec::<u8>::with_capacity(64);
362 data.extend_from_slice(domain_hash);
363 data.extend_from_slice(message_hash);
364
365 let apdu = ApduBuilder::new(COMMAND_SIGN_EIP712)
366 .with_p1(0x00)
367 .with_p2(0x00)
369 .with_data(hd_path.to_bytes().as_slice())
370 .with_data(&data)
371 .build();
372
373 let handle = self.ledger.lock().unwrap();
374
375 let res = sendrecv(&*handle, &apdu)?;
376 let signature = SignatureBytes::from_ledger_bytes(res.as_slice())?;
377 debug!("Received EIP-712 signature: {:?}", signature);
378 Ok(signature)
379 }
380
381 pub fn get_version(&self) -> Result<AppVersion, HWKeyError> {
382 let apdu = ApduBuilder::new(COMMAND_APP_CONFIG)
383 .build();
384 let handle = self.ledger.lock().unwrap();
385 let resp = sendrecv(&*handle, &apdu)?;
386 AppVersion::try_from(resp).map_err(|_| HWKeyError::EncodingError("Invalid version config".to_string()))
387 }
388
389 fn is_path_available(&self, hd_path: &dyn HDPath) -> bool {
390 self.get_address(hd_path, false).is_ok_and(|_| true)
391 }
392}
393
394impl PubkeyAddressApp for EthereumApp {
395 fn get_extkey_at(&self, hd_path: &dyn HDPath) -> Result<Box<dyn AsExtendedKey>, HWKeyError> {
396 let address = self.get_address(hd_path, false)?;
397 Ok(Box::new(address))
398 }
399}
400
401#[derive(Copy, Debug, Clone, Eq, PartialEq)]
402pub enum EthereumApps {
403 Ethereum,
404 EthereumClassic
405}
406
407impl LedgerApp for EthereumApp {
408 type Networks = EthereumApps;
409
410 fn new(manager: Arc<Mutex<dyn LedgerTransport>>) -> Self{
411 EthereumApp {
412 ledger: manager
413 }
414 }
415
416 fn is_open(&self) -> Option<Self::Networks> {
417 self.get_version().ok().and_then(|_| {
418 let has_60 = self.is_path_available(
422 &AccountHDPath::try_new(Purpose::Pubkey, 60, 0).expect("no-eth-acc")
423 );
424 let has_61 = self.is_path_available(
425 &AccountHDPath::try_new(Purpose::Pubkey, 61, 0).expect("no-etc-acc")
426 );
427
428 if has_60 && has_61 {
429 Some(EthereumApps::EthereumClassic)
430 } else if has_60 && !has_61 {
431 Some(EthereumApps::Ethereum)
432 } else {
433 None
434 }
435 })
436 }
437}
438
439#[cfg(test)]
440mod tests {
441 use crate::ledger::app::ethereum::{AddressResponse, EthereumApp, SignatureBytes, ECDSA_SIGNATURE_BYTES};
442 use crate::ledger::app::LedgerApp;
443 use crate::ledger::connect::direct::CHUNK_SIZE;
444 use crate::ledger::connect::mock::MockTransport;
445 use std::convert::TryFrom;
446 use std::sync::{Arc, Mutex};
447 use hdpath::{StandardHDPath, HDPath};
448
449 #[test]
450 fn chunk_data_small_data() {
451 let transport = Arc::new(Mutex::new(MockTransport::new()));
452 let app = EthereumApp::new(transport);
453 let hd_path = StandardHDPath::try_from("m/44'/60'/0'/0/0").unwrap();
454
455 let small_data = vec![1, 2, 3, 4, 5];
456 let (init, cont) = app.chunk_data(&small_data, &hd_path);
457
458 assert_eq!(init, &small_data);
459 assert_eq!(cont.len(), 0);
460 }
461
462 #[test]
463 fn chunk_data_large_data() {
464 let transport = Arc::new(Mutex::new(MockTransport::new()));
465 let app = EthereumApp::new(transport);
466 let hd_path = StandardHDPath::try_from("m/44'/60'/0'/0/0").unwrap();
467
468 let large_data = vec![0u8; CHUNK_SIZE + 100];
470 let (init, cont) = app.chunk_data(&large_data, &hd_path);
471
472 let expected_init_size = CHUNK_SIZE - hd_path.to_bytes().len();
473 assert_eq!(init.len(), expected_init_size);
474 assert_eq!(cont.len(), large_data.len() - expected_init_size);
475
476 let mut reconstructed = Vec::new();
478 reconstructed.extend_from_slice(init);
479 reconstructed.extend_from_slice(cont);
480 assert_eq!(reconstructed, large_data);
481 }
482
483 #[test]
484 fn chunk_data_exact_chunk_size() {
485 let transport = Arc::new(Mutex::new(MockTransport::new()));
486 let app = EthereumApp::new(transport);
487 let hd_path = StandardHDPath::try_from("m/44'/60'/0'/0/0").unwrap();
488
489 let exact_data = vec![0u8; CHUNK_SIZE];
491 let (init, cont) = app.chunk_data(&exact_data, &hd_path);
492
493 assert_eq!(init, &exact_data);
494 assert_eq!(cont.len(), 0);
495 }
496
497 #[test]
498 fn send_chunked_data_builds_correct_apdu() {
499 let transport = Arc::new(Mutex::new(MockTransport::new()));
500 let app = EthereumApp::new(transport);
501 let hd_path = StandardHDPath::try_from("m/44'/60'/0'/0/0").unwrap();
502
503 let small_data = vec![1, 2, 3, 4, 5];
504 let (init, cont) = app.chunk_data(&small_data, &hd_path);
505
506 assert_eq!(init, &small_data);
508 assert_eq!(cont.len(), 0);
509
510 let large_data = vec![0u8; CHUNK_SIZE + 100];
512 let (init, cont) = app.chunk_data(&large_data, &hd_path);
513
514 let expected_init_size = CHUNK_SIZE - hd_path.to_bytes().len();
516 assert_eq!(init.len(), expected_init_size);
517 assert_eq!(cont.len(), large_data.len() - expected_init_size);
518
519 let mut reconstructed = Vec::new();
521 reconstructed.extend_from_slice(init);
522 reconstructed.extend_from_slice(cont);
523 assert_eq!(reconstructed, large_data);
524 }
525
526 #[test]
527 fn chunk_data_respects_hd_path_length() {
528 let transport = Arc::new(Mutex::new(MockTransport::new()));
529 let app = EthereumApp::new(transport);
530
531 use hdpath::CustomHDPath;
533
534 let short_path = CustomHDPath::try_from("m/44'/60'/0'/0").unwrap(); let long_path = StandardHDPath::try_from("m/44'/60'/0'/0/0").unwrap(); let test_data = vec![0u8; CHUNK_SIZE + 50];
539
540 let (init_short, cont_short) = app.chunk_data(&test_data, &short_path);
541 let (init_long, cont_long) = app.chunk_data(&test_data, &long_path);
542
543 let short_path_len = short_path.to_bytes().len();
545 let long_path_len = long_path.to_bytes().len();
546
547 assert!(short_path_len < long_path_len, "Short path should have fewer bytes than long path");
549
550 let expected_init_short = CHUNK_SIZE - short_path_len;
551 let expected_init_long = CHUNK_SIZE - long_path_len;
552
553 assert_eq!(init_short.len(), expected_init_short);
554 assert_eq!(init_long.len(), expected_init_long);
555
556 assert!(init_short.len() > init_long.len(), "Shorter path should allow more data in initial chunk");
558 assert!(cont_short.len() < cont_long.len(), "Shorter path should have less continuation data");
559
560 let mut reconstructed_short = Vec::new();
562 reconstructed_short.extend_from_slice(init_short);
563 reconstructed_short.extend_from_slice(cont_short);
564
565 let mut reconstructed_long = Vec::new();
566 reconstructed_long.extend_from_slice(init_long);
567 reconstructed_long.extend_from_slice(cont_long);
568
569 assert_eq!(reconstructed_short, test_data);
570 assert_eq!(reconstructed_long, test_data);
571 }
572
573 #[test]
574 fn decode_std_address() {
575 let resp = hex::decode("4104b28217096d8ad3dd25461404c3941a5196ac8f089f1be5bcb62df2ce08a71ba1ca4b879ee38217cced7ef1c9dc5c15cb804ab159503514f73559d1a1192ba1fc28354164343233663565623437333534313563393366306365353266366532633133424436413530309000000000000035140000000000000000000000000000000000000000000000000000000000000000").unwrap();
576 let parsed = AddressResponse::try_from(resp);
577 assert!(parsed.is_ok(), "{:?}", parsed);
578 let parsed = parsed.unwrap();
579 assert_eq!("0x5Ad423f5eb4735415c93f0ce52f6e2c13BD6A500".to_string(), parsed.address);
580 assert_eq!(
581 "04b28217096d8ad3dd25461404c3941a5196ac8f089f1be5bcb62df2ce08a71ba1ca4b879ee38217cced7ef1c9dc5c15cb804ab159503514f73559d1a1192ba1fc",
582 hex::encode(parsed.pubkey.serialize_uncompressed()));
583 }
584
585 #[test]
586 fn decode_std_address_2() {
587 let resp = hex::decode("4104452ae4b222d10cb80c269d0677f7165c548e49113d91b26848ae01a7732f15ff88379573411237d1a9dfb9603d2f40d7a56bf12b1bf5f6ae3b69d7bfebd45689283364363634383362344361643335313838363130323946663836613338376542633437303531373290000000000000f5f60000000000000000000000000000000000000000000000000000000000000000").unwrap();
588 let parsed = AddressResponse::try_from(resp);
589 assert!(parsed.is_ok(), "{:?}", parsed);
590 let parsed = parsed.unwrap();
591 assert_eq!("0x3d66483b4Cad3518861029Ff86a387eBc4705172".to_string(), parsed.address);
592 assert_eq!(
593 "04452ae4b222d10cb80c269d0677f7165c548e49113d91b26848ae01a7732f15ff88379573411237d1a9dfb9603d2f40d7a56bf12b1bf5f6ae3b69d7bfebd45689",
594 hex::encode(parsed.pubkey.serialize_uncompressed()));
595 }
596
597 #[test]
598 fn test_from_ledger_bytes_to_vrs_roundtrip() {
599 let original_data: [u8; ECDSA_SIGNATURE_BYTES] = [
600 0x1f, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11,
602 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21,
603 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31,
604 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, 0x41,
605 ];
606
607 let signature = SignatureBytes::from_ledger_bytes(&original_data).expect("Failed to create SignatureBytes");
608
609 assert_eq!(original_data, signature.to_ledger_bytes());
610 }
611}