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
64pub 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 signed_auths.push(auth);
98 continue;
99 };
100 let SorobanAddressCredentials { ref address, .. } = credentials;
101
102 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 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 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
224impl 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 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 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}