From b2ceeaabeb3b7f98338af92a368d8f1995949224 Mon Sep 17 00:00:00 2001 From: Jonathan LEI Date: Sat, 29 Jun 2024 16:13:26 +0800 Subject: [PATCH] feat: add `LedgerStarknetApp` type for Ledger specific operations --- starknet-signers/src/ledger.rs | 91 +++++++++++++++++++++++++++++----- 1 file changed, 79 insertions(+), 12 deletions(-) diff --git a/starknet-signers/src/ledger.rs b/starknet-signers/src/ledger.rs index 8071a78f..5bffac18 100644 --- a/starknet-signers/src/ledger.rs +++ b/starknet-signers/src/ledger.rs @@ -22,12 +22,19 @@ const EIP_2645_PATH_LENGTH: usize = 6; const PUBLIC_KEY_SIZE: usize = 65; const SIGNATURE_SIZE: usize = 65; +/// Ledger app wrapper that implements the [`Signer`] trait. #[derive(Debug)] pub struct LedgerSigner { - transport: Ledger, + app: LedgerStarknetApp, derivation_path: DerivationPath, } +/// A handle for communicating with the Ledger Starknet app. +#[derive(Debug)] +pub struct LedgerStarknetApp { + transport: Ledger, +} + #[derive(Debug, thiserror::Error)] pub enum LedgerError { #[error("derivation path is empty, not prefixed with m/2645', or is not 6-level long")] @@ -77,8 +84,6 @@ impl LedgerSigner { /// /// Currently, the Ledger app only enforces the length and the first level of the path. pub async fn new(derivation_path: DerivationPath) -> Result { - let transport = Ledger::init().await?; - if !matches!(derivation_path.iter().next(), Some(&EIP_2645_PURPOSE)) || derivation_path.len() != EIP_2645_PATH_LENGTH { @@ -86,7 +91,7 @@ impl LedgerSigner { } Ok(Self { - transport, + app: LedgerStarknetApp::new().await?, derivation_path, }) } @@ -98,12 +103,57 @@ impl Signer for LedgerSigner { type SignError = LedgerError; async fn get_public_key(&self) -> Result { + self.app + .get_public_key(self.derivation_path.clone(), false) + .await + } + + async fn sign_hash(&self, hash: &Felt) -> Result { + self.app.sign_hash(self.derivation_path.clone(), hash).await + } + + fn is_interactive(&self) -> bool { + true + } +} + +impl LedgerStarknetApp { + /// Initializes the Starknet Ledger app. Attempts to find and connect to a Ledger device. The + /// device must be unlocked and have the Starknet app open. + pub async fn new() -> Result { + let transport = Ledger::init().await?; + + Ok(Self { transport }) + } + + /// Gets a public key from the app for a particular derivation path, with optional on-device + /// confirmation for extra security. + /// + /// The derivation path _must_ follow EIP-2645, i.e. having `2645'` as its "purpose" level as + /// per BIP-44, as the Ledger app does not allow other paths to be used. + /// + /// The path _must_ also be 6-level in length. An example path for Starknet would be: + /// + /// `m/2645'/1195502025'/1470455285'/0'/0'/0` + /// + /// where: + /// + /// - `2645'` is the EIP-2645 prefix + /// - `1195502025'`, decimal for `0x4741e9c9`, is the 31 lowest bits for `sha256(starknet)` + /// - `1470455285'`, decimal for `0x57a55df5`, is the 31 lowest bits for `sha256(starkli)` + /// + /// Currently, the Ledger app only enforces the length and the first level of the path. + async fn get_public_key( + &self, + derivation_path: DerivationPath, + display: bool, + ) -> Result { let response = self .transport .exchange( &GetPubKeyCommand { - display: false, - path: self.derivation_path.clone(), + display, + path: derivation_path, } .into(), ) @@ -123,13 +173,34 @@ impl Signer for LedgerSigner { Ok(VerifyingKey::from_scalar(pubkey_x)) } - async fn sign_hash(&self, hash: &Felt) -> Result { + /// Requests a signature for a **raw hash** with a certain derivation path. Currently the Ledger + /// app only supports blind signing raw hashes. + /// + /// The derivation path _must_ follow EIP-2645, i.e. having `2645'` as its "purpose" level as + /// per BIP-44, as the Ledger app does not allow other paths to be used. + /// + /// The path _must_ also be 6-level in length. An example path for Starknet would be: + /// + /// `m/2645'/1195502025'/1470455285'/0'/0'/0` + /// + /// where: + /// + /// - `2645'` is the EIP-2645 prefix + /// - `1195502025'`, decimal for `0x4741e9c9`, is the 31 lowest bits for `sha256(starknet)` + /// - `1470455285'`, decimal for `0x57a55df5`, is the 31 lowest bits for `sha256(starkli)` + /// + /// Currently, the Ledger app only enforces the length and the first level of the path. + async fn sign_hash( + &self, + derivation_path: DerivationPath, + hash: &Felt, + ) -> Result { get_apdu_data( &self .transport .exchange( &SignHashCommand1 { - path: self.derivation_path.clone(), + path: derivation_path, } .into(), ) @@ -163,10 +234,6 @@ impl Signer for LedgerSigner { Ok(signature) } - - fn is_interactive(&self) -> bool { - true - } } impl From for LedgerError {