diff --git a/Cargo.lock b/Cargo.lock index 7d2dc14685..e133bf3147 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2831,6 +2831,7 @@ name = "ledger" version = "0.1.0" dependencies = [ "bip32", + "k256", "ledger-transport", "ledger-transport-hid", "thiserror", diff --git a/common/ledger/Cargo.toml b/common/ledger/Cargo.toml index b5566b3737..9bea382a73 100644 --- a/common/ledger/Cargo.toml +++ b/common/ledger/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] bip32 = "0.3.0" +k256 = "0.10.4" ledger-transport = "0.10.0" ledger-transport-hid = "0.10.0" thiserror = "1" \ No newline at end of file diff --git a/common/ledger/src/addr_secp265k1.rs b/common/ledger/src/addr_secp265k1.rs new file mode 100644 index 0000000000..93fe82b8c6 --- /dev/null +++ b/common/ledger/src/addr_secp265k1.rs @@ -0,0 +1,40 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::LedgerError; +use crate::helpers::answer_bytes; +use bip32::{PublicKey, PublicKeyBytes}; +use ledger_transport::APDUAnswer; + +/// SECP265K1 address of the device. +pub struct AddrSecp265k1Response { + /// SECP265K1 public key. + pub public_key: k256::PublicKey, + /// String representation of the Cosmos address. + pub address: String, +} + +impl TryFrom>> for AddrSecp265k1Response { + type Error = LedgerError; + + fn try_from(answer: APDUAnswer>) -> Result { + let bytes = answer_bytes(&answer)?; + if bytes.len() < 33 { + return Err(Self::Error::InvalidAnswerLength { + expected: 33, + received: bytes.len(), + }); + } + + let (pub_key, addr) = bytes.split_at(33); + let public_key = k256::PublicKey::from_bytes( + PublicKeyBytes::try_from(pub_key).expect("Public key should be 33 bytes"), + )?; + let address = String::from_utf8(addr.to_vec()).unwrap(); + + Ok(AddrSecp265k1Response { + public_key, + address, + }) + } +} diff --git a/common/ledger/src/error.rs b/common/ledger/src/error.rs index 74679b6612..d871c729bd 100644 --- a/common/ledger/src/error.rs +++ b/common/ledger/src/error.rs @@ -5,6 +5,7 @@ use thiserror::Error; pub(crate) type Result = std::result::Result; +/// Ledger specific errors. #[derive(Debug, Error)] pub enum LedgerError { #[error("HID API - {0}")] @@ -21,4 +22,10 @@ pub enum LedgerError { #[error("Not enough bytes in answer. Expected at least {expected}, received {received}")] InvalidAnswerLength { expected: usize, received: usize }, + + #[error("Not enough components in derivation path. Expected {expected}, received {received}")] + InvalidDerivationPath { expected: usize, received: usize }, + + #[error("Bip32 - {0}")] + Bip32(#[from] bip32::Error), } diff --git a/common/ledger/src/adpu_answer.rs b/common/ledger/src/helpers.rs similarity index 51% rename from common/ledger/src/adpu_answer.rs rename to common/ledger/src/helpers.rs index defcedc3ca..df21167397 100644 --- a/common/ledger/src/adpu_answer.rs +++ b/common/ledger/src/helpers.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::error::{LedgerError, Result}; +use bip32::DerivationPath; use ledger_transport::{APDUAnswer, APDUErrorCode}; pub(crate) fn answer_bytes(answer: &APDUAnswer>) -> Result<&[u8]> { @@ -15,3 +16,22 @@ pub(crate) fn answer_bytes(answer: &APDUAnswer>) -> Result<&[u8]> { }), } } + +pub(crate) fn path_bytes(path: DerivationPath) -> Result<[[u8; 4]; 5]> { + let received = path.len(); + let components: Vec<[u8; 4]> = path.into_iter().map(|c| c.0.to_le_bytes()).collect(); + if components.len() != 5 { + Err(LedgerError::InvalidDerivationPath { + expected: 5, + received, + }) + } else { + Ok([ + components[0], + components[1], + components[2], + components[3], + components[4], + ]) + } +} diff --git a/common/ledger/src/lib.rs b/common/ledger/src/lib.rs index c79ba47a5b..d3119b6d86 100644 --- a/common/ledger/src/lib.rs +++ b/common/ledger/src/lib.rs @@ -1,11 +1,15 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -pub(crate) mod adpu_answer; +pub mod addr_secp265k1; pub mod error; +pub(crate) mod helpers; pub mod version; +use crate::addr_secp265k1::AddrSecp265k1Response; +use crate::helpers::path_bytes; use crate::version::VersionResponse; +use bip32::DerivationPath; use error::Result; use ledger_transport::APDUCommand; use ledger_transport_hid::hidapi::HidApi; @@ -13,21 +17,25 @@ use ledger_transport_hid::TransportNativeHID; const CLA: u8 = 0x55; const INS_GET_VERSION: u8 = 0x00; -const INS_SIGN_SECP256K1: u8 = 0x02; +const _INS_SIGN_SECP256K1: u8 = 0x02; const INS_GET_ADDR_SECP256K1: u8 = 0x04; -pub struct Ledger { +/// Manage hardware Ledger device with Cosmos specific operations, as described in the +/// specification: https://github.com/cosmos/ledger-cosmos/blob/main/docs/APDUSPEC.md +pub struct CosmosLedger { transport: TransportNativeHID, } -impl Ledger { +impl CosmosLedger { + /// Create the connection to the first Ledger device that we can find. pub fn new() -> Result { let api = HidApi::new()?; let transport = TransportNativeHID::new(&api)?; - Ok(Ledger { transport }) + Ok(CosmosLedger { transport }) } + /// Get the version of the device. pub fn get_version(&self) -> Result { let command = APDUCommand { cla: CLA, @@ -39,4 +47,38 @@ impl Ledger { let response = self.transport.exchange(&command)?; VersionResponse::try_from(response) } + + /// Get the SECP265K1 address of the device. + pub fn get_addr_secp265k1( + &self, + path: DerivationPath, + prefix: &str, + display: bool, + ) -> Result { + let display = if display { 1 } else { 0 }; + let components = path_bytes(path)?; + let data: Vec = vec![ + [prefix.len() as u8].as_slice(), + prefix.as_bytes(), + components[0].as_slice(), + components[1].as_slice(), + components[2].as_slice(), + components[3].as_slice(), + components[4].as_slice(), + ] + .into_iter() + .flatten() + .copied() + .collect(); + + let command = APDUCommand { + cla: CLA, + ins: INS_GET_ADDR_SECP256K1, + p1: display, + p2: 0, + data, + }; + let response = self.transport.exchange(&command)?; + AddrSecp265k1Response::try_from(response) + } } diff --git a/common/ledger/src/version.rs b/common/ledger/src/version.rs index ad1367185b..c2ee169958 100644 --- a/common/ledger/src/version.rs +++ b/common/ledger/src/version.rs @@ -1,39 +1,42 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::adpu_answer::answer_bytes; use crate::error::LedgerError; +use crate::helpers::answer_bytes; use ledger_transport::APDUAnswer; +/// Version and status data of the device. pub struct VersionResponse { + /// Activation status of test mode. pub test_mode: bool, + /// Major part of Cosmos application version. pub major: u8, + /// Minor part of Cosmos application version. pub minor: u8, + /// Patch part of Cosmos application version. pub patch: u8, + /// PIN locked status. pub device_locked: bool, } impl TryFrom>> for VersionResponse { - type Error = crate::error::LedgerError; + type Error = LedgerError; fn try_from(answer: APDUAnswer>) -> Result { let bytes = answer_bytes(&answer)?; - let received = bytes.len(); - let err = - |expected| -> LedgerError { Self::Error::InvalidAnswerLength { expected, received } }; - - let test_mode = *bytes.get(0).ok_or_else(|| err(0))? != 0; - let major = *bytes.get(1).ok_or_else(|| err(1))?; - let minor = *bytes.get(2).ok_or_else(|| err(2))?; - let patch = *bytes.get(3).ok_or_else(|| err(3))?; - let device_locked = *bytes.get(4).ok_or_else(|| err(4))? != 0; + if bytes.len() != 5 { + return Err(Self::Error::InvalidAnswerLength { + expected: 5, + received: bytes.len(), + }); + } Ok(VersionResponse { - test_mode, - major, - minor, - patch, - device_locked, + test_mode: bytes[0] != 0, + major: bytes[1], + minor: bytes[2], + patch: bytes[3], + device_locked: bytes[4] != 0, }) } }