Skip to content

Commit

Permalink
feat: add LedgerStarknetApp type for Ledger specific operations
Browse files Browse the repository at this point in the history
  • Loading branch information
xJonathanLEI committed Jun 29, 2024
1 parent 838c612 commit b2ceeaa
Showing 1 changed file with 79 additions and 12 deletions.
91 changes: 79 additions & 12 deletions starknet-signers/src/ledger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -77,16 +84,14 @@ 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<Self, LedgerError> {
let transport = Ledger::init().await?;

if !matches!(derivation_path.iter().next(), Some(&EIP_2645_PURPOSE))
|| derivation_path.len() != EIP_2645_PATH_LENGTH
{
return Err(LedgerError::InvalidDerivationPath);
}

Ok(Self {
transport,
app: LedgerStarknetApp::new().await?,
derivation_path,
})
}
Expand All @@ -98,12 +103,57 @@ impl Signer for LedgerSigner {
type SignError = LedgerError;

async fn get_public_key(&self) -> Result<VerifyingKey, Self::GetPublicKeyError> {
self.app
.get_public_key(self.derivation_path.clone(), false)
.await
}

async fn sign_hash(&self, hash: &Felt) -> Result<Signature, Self::SignError> {
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<Self, LedgerError> {
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<VerifyingKey, LedgerError> {
let response = self
.transport
.exchange(
&GetPubKeyCommand {
display: false,
path: self.derivation_path.clone(),
display,
path: derivation_path,
}
.into(),
)
Expand All @@ -123,13 +173,34 @@ impl Signer for LedgerSigner {
Ok(VerifyingKey::from_scalar(pubkey_x))
}

async fn sign_hash(&self, hash: &Felt) -> Result<Signature, Self::SignError> {
/// 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<Signature, LedgerError> {
get_apdu_data(
&self
.transport
.exchange(
&SignHashCommand1 {
path: self.derivation_path.clone(),
path: derivation_path,
}
.into(),
)
Expand Down Expand Up @@ -163,10 +234,6 @@ impl Signer for LedgerSigner {

Ok(signature)
}

fn is_interactive(&self) -> bool {
true
}
}

impl From<coins_ledger::LedgerError> for LedgerError {
Expand Down

0 comments on commit b2ceeaa

Please sign in to comment.