soroban_cli/
signer.rs

1use ed25519_dalek::ed25519::signature::Signer as _;
2use keyring::StellarEntry;
3use sha2::{Digest, Sha256};
4
5use crate::xdr::{
6    self, AccountId, DecoratedSignature, Hash, HashIdPreimage, HashIdPreimageSorobanAuthorization,
7    InvokeHostFunctionOp, Limits, Operation, OperationBody, PublicKey, ScAddress, ScMap, ScSymbol,
8    ScVal, Signature, SignatureHint, SorobanAddressCredentials, SorobanAuthorizationEntry,
9    SorobanAuthorizedFunction, SorobanCredentials, Transaction, TransactionEnvelope,
10    TransactionV1Envelope, Uint256, VecM, WriteXdr,
11};
12use stellar_ledger::{Blob as _, Exchange, LedgerSigner};
13
14use crate::{config::network::Network, print::Print, utils::transaction_hash};
15
16pub mod keyring;
17pub mod secure_store;
18
19#[derive(thiserror::Error, Debug)]
20pub enum Error {
21    #[error("Contract addresses are not supported to sign auth entries {address}")]
22    ContractAddressAreNotSupported { address: String },
23    #[error(transparent)]
24    Ed25519(#[from] ed25519_dalek::SignatureError),
25    #[error("Missing signing key for account {address}")]
26    MissingSignerForAddress { address: String },
27    #[error(transparent)]
28    TryFromSlice(#[from] std::array::TryFromSliceError),
29    #[error("User cancelled signing, perhaps need to add -y")]
30    UserCancelledSigning,
31    #[error(transparent)]
32    Ledger(#[from] stellar_ledger::Error),
33    #[error(transparent)]
34    Xdr(#[from] xdr::Error),
35    #[error("Only Transaction envelope V1 type is supported")]
36    UnsupportedTransactionEnvelopeType,
37    #[error(transparent)]
38    Url(#[from] url::ParseError),
39    #[error(transparent)]
40    Open(#[from] std::io::Error),
41    #[error("Returning a signature from Lab is not yet supported; Transaction can be found and submitted in lab")]
42    ReturningSignatureFromLab,
43    #[error(transparent)]
44    Keyring(#[from] keyring::Error),
45}
46
47fn requires_auth(txn: &Transaction) -> Option<xdr::Operation> {
48    let [op @ Operation {
49        body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp { auth, .. }),
50        ..
51    }] = txn.operations.as_slice()
52    else {
53        return None;
54    };
55    matches!(
56        auth.first().map(|x| &x.root_invocation.function),
57        Some(&SorobanAuthorizedFunction::ContractFn(_))
58    )
59    .then(move || op.clone())
60}
61
62// Use the given source_key and signers, to sign all SorobanAuthorizationEntry's in the given
63// transaction. If unable to sign, return an error.
64pub fn sign_soroban_authorizations(
65    raw: &Transaction,
66    source_key: &ed25519_dalek::SigningKey,
67    signers: &[ed25519_dalek::SigningKey],
68    signature_expiration_ledger: u32,
69    network_passphrase: &str,
70) -> Result<Option<Transaction>, Error> {
71    let mut tx = raw.clone();
72    let Some(mut op) = requires_auth(&tx) else {
73        return Ok(None);
74    };
75
76    let Operation {
77        body: OperationBody::InvokeHostFunction(ref mut body),
78        ..
79    } = op
80    else {
81        return Ok(None);
82    };
83
84    let network_id = Hash(Sha256::digest(network_passphrase.as_bytes()).into());
85
86    let verification_key = source_key.verifying_key();
87    let source_address = verification_key.as_bytes();
88
89    let signed_auths = body
90        .auth
91        .as_slice()
92        .iter()
93        .map(|raw_auth| {
94            let mut auth = raw_auth.clone();
95            let SorobanAuthorizationEntry {
96                credentials: SorobanCredentials::Address(ref mut credentials),
97                ..
98            } = auth
99            else {
100                // Doesn't need special signing
101                return Ok(auth);
102            };
103            let SorobanAddressCredentials { ref address, .. } = credentials;
104
105            // See if we have a signer for this authorizationEntry
106            // If not, then we Error
107            let needle = match address {
108                ScAddress::Account(AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(ref a)))) => a,
109                ScAddress::Contract(Hash(c)) => {
110                    // This address is for a contract. This means we're using a custom
111                    // smart-contract account. Currently the CLI doesn't support that yet.
112                    return Err(Error::MissingSignerForAddress {
113                        address: stellar_strkey::Strkey::Contract(stellar_strkey::Contract(*c))
114                            .to_string(),
115                    });
116                }
117            };
118            let signer = if let Some(s) = signers
119                .iter()
120                .find(|s| needle == s.verifying_key().as_bytes())
121            {
122                s
123            } else if needle == source_address {
124                // This is the source address, so we can sign it
125                source_key
126            } else {
127                // We don't have a signer for this address
128                return Err(Error::MissingSignerForAddress {
129                    address: stellar_strkey::Strkey::PublicKeyEd25519(
130                        stellar_strkey::ed25519::PublicKey(*needle),
131                    )
132                    .to_string(),
133                });
134            };
135
136            sign_soroban_authorization_entry(
137                raw_auth,
138                signer,
139                signature_expiration_ledger,
140                &network_id,
141            )
142        })
143        .collect::<Result<Vec<_>, Error>>()?;
144
145    body.auth = signed_auths.try_into()?;
146    tx.operations = vec![op].try_into()?;
147    Ok(Some(tx))
148}
149
150fn sign_soroban_authorization_entry(
151    raw: &SorobanAuthorizationEntry,
152    signer: &ed25519_dalek::SigningKey,
153    signature_expiration_ledger: u32,
154    network_id: &Hash,
155) -> Result<SorobanAuthorizationEntry, Error> {
156    let mut auth = raw.clone();
157    let SorobanAuthorizationEntry {
158        credentials: SorobanCredentials::Address(ref mut credentials),
159        ..
160    } = auth
161    else {
162        // Doesn't need special signing
163        return Ok(auth);
164    };
165    let SorobanAddressCredentials { nonce, .. } = credentials;
166
167    let preimage = HashIdPreimage::SorobanAuthorization(HashIdPreimageSorobanAuthorization {
168        network_id: network_id.clone(),
169        invocation: auth.root_invocation.clone(),
170        nonce: *nonce,
171        signature_expiration_ledger,
172    })
173    .to_xdr(Limits::none())?;
174
175    let payload = Sha256::digest(preimage);
176    let signature = signer.sign(&payload);
177
178    let map = ScMap::sorted_from(vec![
179        (
180            ScVal::Symbol(ScSymbol("public_key".try_into()?)),
181            ScVal::Bytes(
182                signer
183                    .verifying_key()
184                    .to_bytes()
185                    .to_vec()
186                    .try_into()
187                    .map_err(Error::Xdr)?,
188            ),
189        ),
190        (
191            ScVal::Symbol(ScSymbol("signature".try_into()?)),
192            ScVal::Bytes(
193                signature
194                    .to_bytes()
195                    .to_vec()
196                    .try_into()
197                    .map_err(Error::Xdr)?,
198            ),
199        ),
200    ])
201    .map_err(Error::Xdr)?;
202    credentials.signature = ScVal::Vec(Some(
203        vec![ScVal::Map(Some(map))].try_into().map_err(Error::Xdr)?,
204    ));
205    credentials.signature_expiration_ledger = signature_expiration_ledger;
206    auth.credentials = SorobanCredentials::Address(credentials.clone());
207    Ok(auth)
208}
209
210pub struct Signer {
211    pub kind: SignerKind,
212    pub print: Print,
213}
214
215#[allow(clippy::module_name_repetitions, clippy::large_enum_variant)]
216pub enum SignerKind {
217    Local(LocalKey),
218    #[cfg(not(feature = "emulator-tests"))]
219    Ledger(Ledger<stellar_ledger::TransportNativeHID>),
220    #[cfg(feature = "emulator-tests")]
221    Ledger(Ledger<stellar_ledger::emulator_test_support::http_transport::Emulator>),
222    Lab,
223    SecureStore(SecureStoreEntry),
224}
225
226impl Signer {
227    pub async fn sign_tx(
228        &self,
229        tx: Transaction,
230        network: &Network,
231    ) -> Result<TransactionEnvelope, Error> {
232        let tx_env = TransactionEnvelope::Tx(TransactionV1Envelope {
233            tx,
234            signatures: VecM::default(),
235        });
236        self.sign_tx_env(&tx_env, network).await
237    }
238
239    pub async fn sign_tx_env(
240        &self,
241        tx_env: &TransactionEnvelope,
242        network: &Network,
243    ) -> Result<TransactionEnvelope, Error> {
244        match &tx_env {
245            TransactionEnvelope::Tx(TransactionV1Envelope { tx, signatures }) => {
246                let tx_hash = transaction_hash(tx, &network.network_passphrase)?;
247                self.print
248                    .infoln(format!("Signing transaction: {}", hex::encode(tx_hash),));
249                let decorated_signature = match &self.kind {
250                    SignerKind::Local(key) => key.sign_tx_hash(tx_hash)?,
251                    SignerKind::Lab => Lab::sign_tx_env(tx_env, network, &self.print)?,
252                    SignerKind::Ledger(ledger) => ledger.sign_transaction_hash(&tx_hash).await?,
253                    SignerKind::SecureStore(entry) => entry.sign_tx_hash(tx_hash)?,
254                };
255                let mut sigs = signatures.clone().into_vec();
256                sigs.push(decorated_signature);
257                Ok(TransactionEnvelope::Tx(TransactionV1Envelope {
258                    tx: tx.clone(),
259                    signatures: sigs.try_into()?,
260                }))
261            }
262            _ => Err(Error::UnsupportedTransactionEnvelopeType),
263        }
264    }
265}
266
267pub struct LocalKey {
268    pub key: ed25519_dalek::SigningKey,
269}
270
271#[allow(dead_code)]
272pub struct Ledger<T: Exchange> {
273    pub(crate) index: u32,
274    pub(crate) signer: LedgerSigner<T>,
275}
276
277impl<T: Exchange> Ledger<T> {
278    pub async fn sign_transaction_hash(
279        &self,
280        tx_hash: &[u8; 32],
281    ) -> Result<DecoratedSignature, Error> {
282        let key = self.public_key().await?;
283        let hint = SignatureHint(key.0[28..].try_into()?);
284        let signature = Signature(
285            self.signer
286                .sign_transaction_hash(self.index, tx_hash)
287                .await?
288                .try_into()?,
289        );
290        Ok(DecoratedSignature { hint, signature })
291    }
292
293    pub async fn sign_transaction(
294        &self,
295        tx: Transaction,
296        network_passphrase: &str,
297    ) -> Result<DecoratedSignature, Error> {
298        let network_id = Hash(Sha256::digest(network_passphrase).into());
299        let signature = self
300            .signer
301            .sign_transaction(self.index, tx, network_id)
302            .await?;
303        let key = self.public_key().await?;
304        let hint = SignatureHint(key.0[28..].try_into()?);
305        let signature = Signature(signature.try_into()?);
306        Ok(DecoratedSignature { hint, signature })
307    }
308
309    pub async fn public_key(&self) -> Result<stellar_strkey::ed25519::PublicKey, Error> {
310        Ok(self.signer.get_public_key(&self.index.into()).await?)
311    }
312}
313
314#[cfg(not(feature = "emulator-tests"))]
315pub async fn ledger(hd_path: u32) -> Result<Ledger<stellar_ledger::TransportNativeHID>, Error> {
316    let signer = stellar_ledger::native()?;
317    Ok(Ledger {
318        index: hd_path,
319        signer,
320    })
321}
322
323#[cfg(feature = "emulator-tests")]
324pub async fn ledger(
325    hd_path: u32,
326) -> Result<Ledger<stellar_ledger::emulator_test_support::http_transport::Emulator>, Error> {
327    use stellar_ledger::emulator_test_support::ledger as emulator_ledger;
328    // port from SPECULOS_PORT ENV var
329    let host_port: u16 = std::env::var("SPECULOS_PORT")
330        .expect("SPECULOS_PORT env var not set")
331        .parse()
332        .expect("port must be a number");
333    let signer = emulator_ledger(host_port).await;
334
335    Ok(Ledger {
336        index: hd_path,
337        signer,
338    })
339}
340
341impl LocalKey {
342    pub fn sign_tx_hash(&self, tx_hash: [u8; 32]) -> Result<DecoratedSignature, Error> {
343        let hint = SignatureHint(self.key.verifying_key().to_bytes()[28..].try_into()?);
344        let signature = Signature(self.key.sign(&tx_hash).to_bytes().to_vec().try_into()?);
345        Ok(DecoratedSignature { hint, signature })
346    }
347}
348
349pub struct Lab;
350
351impl Lab {
352    const URL: &str = "https://lab.stellar.org/transaction/cli-sign";
353
354    pub fn sign_tx_env(
355        tx_env: &TransactionEnvelope,
356        network: &Network,
357        printer: &Print,
358    ) -> Result<DecoratedSignature, Error> {
359        let xdr = tx_env.to_xdr_base64(Limits::none())?;
360
361        let mut url = url::Url::parse(Self::URL)?;
362        url.query_pairs_mut()
363            .append_pair("networkPassphrase", &network.network_passphrase)
364            .append_pair("xdr", &xdr);
365        let url = url.to_string();
366
367        printer.globeln(format!("Opening lab to sign transaction: {url}"));
368        open::that(url)?;
369
370        Err(Error::ReturningSignatureFromLab)
371    }
372}
373
374pub struct SecureStoreEntry {
375    pub name: String,
376    pub hd_path: Option<usize>,
377}
378
379impl SecureStoreEntry {
380    pub fn sign_tx_hash(&self, tx_hash: [u8; 32]) -> Result<DecoratedSignature, Error> {
381        let entry = StellarEntry::new(&self.name)?;
382        let hint = SignatureHint(entry.get_public_key(self.hd_path)?.0[28..].try_into()?);
383        let signed_tx_hash = entry.sign_data(&tx_hash, self.hd_path)?;
384        let signature = Signature(signed_tx_hash.clone().try_into()?);
385        Ok(DecoratedSignature { hint, signature })
386    }
387}