Skip to main content

soroban_cli/signer/
ledger.rs

1use crate::xdr;
2
3pub use ledger_impl::*;
4
5#[derive(thiserror::Error, Debug)]
6pub enum Error {
7    #[error("Ledger Device keys are not allowed: additional-libs feature must be enabled")]
8    FeatureNotEnabled,
9
10    #[cfg(feature = "additional-libs")]
11    #[error(transparent)]
12    StellarLedger(#[from] stellar_ledger::Error),
13
14    #[error(transparent)]
15    TryFromSlice(#[from] std::array::TryFromSliceError),
16
17    #[error(transparent)]
18    Xdr(#[from] xdr::Error),
19
20    #[error(transparent)]
21    Validation(#[from] crate::signer::validation::Error),
22}
23
24#[cfg(feature = "additional-libs")]
25mod ledger_impl {
26    use super::Error;
27    use crate::xdr::{DecoratedSignature, Hash, Signature, SignatureHint, Transaction};
28    use ed25519_dalek::Signature as Ed25519Signature;
29    use sha2::{Digest, Sha256};
30    use stellar_ledger::{Blob as _, Exchange, LedgerSigner};
31
32    #[cfg(not(feature = "emulator-tests"))]
33    pub type LedgerType = Ledger<stellar_ledger::TransportNativeHID>;
34    #[cfg(feature = "emulator-tests")]
35    pub type LedgerType = Ledger<stellar_ledger::emulator_test_support::http_transport::Emulator>;
36
37    // Pure-data signer for Ledger identities. Mirrors `SecureStoreEntry`:
38    // holds no live transport, opens HID lazily inside each sign call so the
39    // device stays free between operations and can never collide with a
40    // concurrent transport elsewhere in the process.
41    pub struct LedgerEntry {
42        pub hd_path: u32,
43        pub public_key: Option<stellar_strkey::ed25519::PublicKey>,
44    }
45
46    impl LedgerEntry {
47        pub async fn sign_tx_hash(&self, tx_hash: [u8; 32]) -> Result<DecoratedSignature, Error> {
48            let live = new(self.hd_path).await?;
49            let key = match self.public_key {
50                Some(pk) => pk,
51                None => live.public_key().await?,
52            };
53            let sig_bytes = live
54                .signer
55                .sign_transaction_hash(live.index, &tx_hash)
56                .await?;
57            // Only the cached-pubkey path can drift; the cacheless path fetched the
58            // key live from the same device that just signed.
59            if let Some(pk) = self.public_key {
60                crate::signer::validation::verify_signature(&pk, &tx_hash, &sig_bytes)?;
61            }
62            let hint = SignatureHint(key.0[28..].try_into()?);
63            let signature = Signature(sig_bytes.try_into()?);
64            Ok(DecoratedSignature { hint, signature })
65        }
66
67        pub async fn sign_payload(&self, payload: [u8; 32]) -> Result<Ed25519Signature, Error> {
68            let live = new(self.hd_path).await?;
69            let bytes = live
70                .signer
71                .sign_transaction_hash(live.index, &payload)
72                .await?;
73            if let Some(pk) = self.public_key {
74                crate::signer::validation::verify_signature(&pk, &payload, &bytes)?;
75            }
76            Ok(Ed25519Signature::from_bytes(bytes.as_slice().try_into()?))
77        }
78    }
79
80    pub struct Ledger<T: Exchange> {
81        pub(crate) index: u32,
82        pub(crate) signer: LedgerSigner<T>,
83    }
84
85    #[cfg(not(feature = "emulator-tests"))]
86    #[allow(clippy::unused_async)]
87    pub async fn new(hd_path: u32) -> Result<Ledger<stellar_ledger::TransportNativeHID>, Error> {
88        let signer = stellar_ledger::native()?;
89        Ok(Ledger {
90            index: hd_path,
91            signer,
92        })
93    }
94
95    #[cfg(feature = "emulator-tests")]
96    pub async fn new(
97        hd_path: u32,
98    ) -> Result<Ledger<stellar_ledger::emulator_test_support::http_transport::Emulator>, Error>
99    {
100        use stellar_ledger::emulator_test_support::ledger as emulator_ledger;
101        // port from SPECULOS_PORT ENV var
102        let host_port: u16 = std::env::var("SPECULOS_PORT")
103            .expect("SPECULOS_PORT env var not set")
104            .parse()
105            .expect("port must be a number");
106        let signer = emulator_ledger(host_port).await;
107
108        Ok(Ledger {
109            index: hd_path,
110            signer,
111        })
112    }
113
114    impl<T: Exchange> Ledger<T> {
115        pub async fn sign_transaction(
116            &self,
117            tx: Transaction,
118            network_passphrase: &str,
119        ) -> Result<DecoratedSignature, Error> {
120            let network_id = Hash(Sha256::digest(network_passphrase).into());
121            let signature = self
122                .signer
123                .sign_transaction(self.index, tx, network_id)
124                .await?;
125            let key = self.public_key().await?;
126            let hint = SignatureHint(key.0[28..].try_into()?);
127            let signature = Signature(signature.try_into()?);
128            Ok(DecoratedSignature { hint, signature })
129        }
130
131        pub async fn public_key(&self) -> Result<stellar_strkey::ed25519::PublicKey, Error> {
132            Ok(self.signer.get_public_key(&self.index.into()).await?)
133        }
134    }
135}
136
137#[cfg(not(feature = "additional-libs"))]
138mod ledger_impl {
139    use super::Error;
140    use crate::xdr::{DecoratedSignature, Transaction};
141    use ed25519_dalek::Signature as Ed25519Signature;
142    use std::marker::PhantomData;
143
144    pub type LedgerType = Ledger<GenericExchange>;
145
146    pub trait Exchange {}
147    pub struct Ledger<T: Exchange> {
148        _marker: PhantomData<T>,
149    }
150
151    pub struct LedgerEntry {
152        pub hd_path: u32,
153        pub public_key: Option<stellar_strkey::ed25519::PublicKey>,
154    }
155
156    impl LedgerEntry {
157        #[allow(clippy::unused_async)]
158        pub async fn sign_tx_hash(&self, _tx_hash: [u8; 32]) -> Result<DecoratedSignature, Error> {
159            Err(Error::FeatureNotEnabled)
160        }
161
162        #[allow(clippy::unused_async)]
163        pub async fn sign_payload(&self, _payload: [u8; 32]) -> Result<Ed25519Signature, Error> {
164            Err(Error::FeatureNotEnabled)
165        }
166    }
167
168    #[allow(clippy::unused_async)]
169    pub async fn new(_hd_path: u32) -> Result<Ledger<GenericExchange>, Error> {
170        Err(Error::FeatureNotEnabled)
171    }
172
173    impl<T: Exchange> Ledger<T> {
174        #[allow(clippy::unused_async)]
175        pub async fn sign_transaction(
176            &self,
177            _tx: Transaction,
178            _network_passphrase: &str,
179        ) -> Result<DecoratedSignature, Error> {
180            Err(Error::FeatureNotEnabled)
181        }
182
183        #[allow(clippy::unused_async)]
184        pub async fn public_key(&self) -> Result<stellar_strkey::ed25519::PublicKey, Error> {
185            Err(Error::FeatureNotEnabled)
186        }
187    }
188
189    pub struct GenericExchange {}
190
191    impl Exchange for GenericExchange {}
192
193    impl GenericExchange {}
194}