Skip to content

Commit 702008d

Browse files
authored
feat: add LedgerStarknetApp type for Ledger specific operations (#621)
1 parent 838c612 commit 702008d

File tree

1 file changed

+79
-12
lines changed

1 file changed

+79
-12
lines changed

starknet-signers/src/ledger.rs

Lines changed: 79 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,19 @@ const EIP_2645_PATH_LENGTH: usize = 6;
2222
const PUBLIC_KEY_SIZE: usize = 65;
2323
const SIGNATURE_SIZE: usize = 65;
2424

25+
/// Ledger app wrapper that implements the [`Signer`] trait.
2526
#[derive(Debug)]
2627
pub struct LedgerSigner {
27-
transport: Ledger,
28+
app: LedgerStarknetApp,
2829
derivation_path: DerivationPath,
2930
}
3031

32+
/// A handle for communicating with the Ledger Starknet app.
33+
#[derive(Debug)]
34+
pub struct LedgerStarknetApp {
35+
transport: Ledger,
36+
}
37+
3138
#[derive(Debug, thiserror::Error)]
3239
pub enum LedgerError {
3340
#[error("derivation path is empty, not prefixed with m/2645', or is not 6-level long")]
@@ -77,16 +84,14 @@ impl LedgerSigner {
7784
///
7885
/// Currently, the Ledger app only enforces the length and the first level of the path.
7986
pub async fn new(derivation_path: DerivationPath) -> Result<Self, LedgerError> {
80-
let transport = Ledger::init().await?;
81-
8287
if !matches!(derivation_path.iter().next(), Some(&EIP_2645_PURPOSE))
8388
|| derivation_path.len() != EIP_2645_PATH_LENGTH
8489
{
8590
return Err(LedgerError::InvalidDerivationPath);
8691
}
8792

8893
Ok(Self {
89-
transport,
94+
app: LedgerStarknetApp::new().await?,
9095
derivation_path,
9196
})
9297
}
@@ -98,12 +103,57 @@ impl Signer for LedgerSigner {
98103
type SignError = LedgerError;
99104

100105
async fn get_public_key(&self) -> Result<VerifyingKey, Self::GetPublicKeyError> {
106+
self.app
107+
.get_public_key(self.derivation_path.clone(), false)
108+
.await
109+
}
110+
111+
async fn sign_hash(&self, hash: &Felt) -> Result<Signature, Self::SignError> {
112+
self.app.sign_hash(self.derivation_path.clone(), hash).await
113+
}
114+
115+
fn is_interactive(&self) -> bool {
116+
true
117+
}
118+
}
119+
120+
impl LedgerStarknetApp {
121+
/// Initializes the Starknet Ledger app. Attempts to find and connect to a Ledger device. The
122+
/// device must be unlocked and have the Starknet app open.
123+
pub async fn new() -> Result<Self, LedgerError> {
124+
let transport = Ledger::init().await?;
125+
126+
Ok(Self { transport })
127+
}
128+
129+
/// Gets a public key from the app for a particular derivation path, with optional on-device
130+
/// confirmation for extra security.
131+
///
132+
/// The derivation path _must_ follow EIP-2645, i.e. having `2645'` as its "purpose" level as
133+
/// per BIP-44, as the Ledger app does not allow other paths to be used.
134+
///
135+
/// The path _must_ also be 6-level in length. An example path for Starknet would be:
136+
///
137+
/// `m/2645'/1195502025'/1470455285'/0'/0'/0`
138+
///
139+
/// where:
140+
///
141+
/// - `2645'` is the EIP-2645 prefix
142+
/// - `1195502025'`, decimal for `0x4741e9c9`, is the 31 lowest bits for `sha256(starknet)`
143+
/// - `1470455285'`, decimal for `0x57a55df5`, is the 31 lowest bits for `sha256(starkli)`
144+
///
145+
/// Currently, the Ledger app only enforces the length and the first level of the path.
146+
async fn get_public_key(
147+
&self,
148+
derivation_path: DerivationPath,
149+
display: bool,
150+
) -> Result<VerifyingKey, LedgerError> {
101151
let response = self
102152
.transport
103153
.exchange(
104154
&GetPubKeyCommand {
105-
display: false,
106-
path: self.derivation_path.clone(),
155+
display,
156+
path: derivation_path,
107157
}
108158
.into(),
109159
)
@@ -123,13 +173,34 @@ impl Signer for LedgerSigner {
123173
Ok(VerifyingKey::from_scalar(pubkey_x))
124174
}
125175

126-
async fn sign_hash(&self, hash: &Felt) -> Result<Signature, Self::SignError> {
176+
/// Requests a signature for a **raw hash** with a certain derivation path. Currently the Ledger
177+
/// app only supports blind signing raw hashes.
178+
///
179+
/// The derivation path _must_ follow EIP-2645, i.e. having `2645'` as its "purpose" level as
180+
/// per BIP-44, as the Ledger app does not allow other paths to be used.
181+
///
182+
/// The path _must_ also be 6-level in length. An example path for Starknet would be:
183+
///
184+
/// `m/2645'/1195502025'/1470455285'/0'/0'/0`
185+
///
186+
/// where:
187+
///
188+
/// - `2645'` is the EIP-2645 prefix
189+
/// - `1195502025'`, decimal for `0x4741e9c9`, is the 31 lowest bits for `sha256(starknet)`
190+
/// - `1470455285'`, decimal for `0x57a55df5`, is the 31 lowest bits for `sha256(starkli)`
191+
///
192+
/// Currently, the Ledger app only enforces the length and the first level of the path.
193+
async fn sign_hash(
194+
&self,
195+
derivation_path: DerivationPath,
196+
hash: &Felt,
197+
) -> Result<Signature, LedgerError> {
127198
get_apdu_data(
128199
&self
129200
.transport
130201
.exchange(
131202
&SignHashCommand1 {
132-
path: self.derivation_path.clone(),
203+
path: derivation_path,
133204
}
134205
.into(),
135206
)
@@ -163,10 +234,6 @@ impl Signer for LedgerSigner {
163234

164235
Ok(signature)
165236
}
166-
167-
fn is_interactive(&self) -> bool {
168-
true
169-
}
170237
}
171238

172239
impl From<coins_ledger::LedgerError> for LedgerError {

0 commit comments

Comments
 (0)