Skip to main content

soroban_cli/signer/
mod.rs

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