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
62pub 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 return Ok(auth);
102 };
103 let SorobanAddressCredentials { ref address, .. } = credentials;
104
105 let needle = match address {
108 ScAddress::Account(AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(ref a)))) => a,
109 ScAddress::Contract(Hash(c)) => {
110 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 source_key
126 } else {
127 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 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 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}