soroban_cli/signer/
mod.rs

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