soroban_cli/commands/message/
sign.rs1use std::io::{self, Read};
2
3use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
4use clap::Parser;
5use sha2::{Digest, Sha256};
6
7use crate::{
8 commands::global,
9 config::{locator, secret},
10 print::Print,
11 signer::{self, Signer},
12};
13
14use super::SEP53_PREFIX;
15
16#[derive(thiserror::Error, Debug)]
17pub enum Error {
18 #[error(transparent)]
19 Locator(#[from] locator::Error),
20
21 #[error(transparent)]
22 Secret(#[from] secret::Error),
23
24 #[error(transparent)]
25 Signer(#[from] signer::Error),
26
27 #[error(transparent)]
28 Io(#[from] io::Error),
29
30 #[error(transparent)]
31 Base64(#[from] base64::DecodeError),
32
33 #[error(transparent)]
34 StrKey(#[from] stellar_strkey::DecodeError),
35
36 #[error(transparent)]
37 Ed25519(#[from] ed25519_dalek::SignatureError),
38
39 #[error("No signing key provided. Use --sign-with-key")]
40 NoSigningKey,
41
42 #[error("Ledger signing of arbitrary messages is not yet supported")]
43 LedgerNotSupported,
44}
45
46#[derive(Debug, Parser, Clone)]
47#[group(skip)]
48pub struct Cmd {
49 #[arg()]
52 pub message: Option<String>,
53
54 #[arg(long)]
56 pub base64: bool,
57
58 #[arg(long, env = "STELLAR_SIGN_WITH_KEY")]
61 pub sign_with_key: String,
62
63 #[arg(long)]
64 pub hd_path: Option<usize>,
66
67 #[command(flatten)]
68 pub locator: locator::Args,
69}
70
71impl Cmd {
72 pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
73 let print = Print::new(global_args.quiet);
74
75 let message_bytes = self.get_message_bytes()?;
77
78 let key_or_name = &self.sign_with_key;
80 let secret = self.locator.get_secret_key(key_or_name)?;
81 let signer = secret.signer(self.hd_path, print.clone()).await?;
82 let public_key = signer.get_public_key()?;
83
84 let signature_base64 = sep_53_sign(&message_bytes, signer)?;
86
87 print.infoln(format!("Signer: {public_key}"));
88 let message_display = if self.base64 {
89 BASE64.encode(&message_bytes)
90 } else {
91 String::from_utf8_lossy(&message_bytes).to_string()
92 };
93 print.infoln(format!("Message: {message_display}"));
94 println!("{signature_base64}");
95 Ok(())
96 }
97
98 fn get_message_bytes(&self) -> Result<Vec<u8>, Error> {
99 let message_str = if let Some(msg) = &self.message {
100 msg.clone()
101 } else {
102 let mut buffer = String::new();
104 io::stdin().read_to_string(&mut buffer)?;
105 if buffer.ends_with('\n') {
107 buffer.pop();
108 if buffer.ends_with('\r') {
109 buffer.pop();
110 }
111 }
112 buffer
113 };
114
115 if self.base64 {
116 Ok(BASE64.decode(&message_str)?)
118 } else {
119 Ok(message_str.into_bytes())
121 }
122 }
123}
124
125fn sep_53_sign(message_bytes: &[u8], signer: Signer) -> Result<String, Error> {
129 let mut payload = Vec::with_capacity(SEP53_PREFIX.len() + message_bytes.len());
131 payload.extend_from_slice(SEP53_PREFIX.as_bytes());
132 payload.extend_from_slice(message_bytes);
133 let hash: [u8; 32] = Sha256::digest(&payload).into();
134
135 let signature = signer.sign_payload(hash)?;
136
137 Ok(BASE64.encode(signature.to_bytes()))
138}
139
140#[cfg(test)]
141mod tests {
142 use std::str::FromStr;
143
144 use super::*;
145 use crate::{config::secret::Secret, utils::into_signing_key};
146
147 const TEST_SECRET_KEY: &str = "SAKICEVQLYWGSOJS4WW7HZJWAHZVEEBS527LHK5V4MLJALYKICQCJXMW";
149
150 fn setup_locator() -> locator::Args {
151 let temp_dir = tempfile::tempdir().unwrap();
152 locator::Args {
153 global: false,
154 config_dir: Some(temp_dir.path().to_path_buf()),
155 }
156 }
157
158 fn build_signer_for_test_key() -> Signer {
159 let secret = Secret::from_str(TEST_SECRET_KEY).unwrap();
160 let private_key = secret.private_key(None).unwrap();
161 let signing_key = into_signing_key(&private_key);
162 Signer {
163 kind: signer::SignerKind::Local(signer::LocalKey { key: signing_key }),
164 print: Print::new(true),
165 }
166 }
167
168 #[test]
169 fn test_sign_simple() {
170 let message = "Hello, World!".to_string();
172 let expected_signature = "fO5dbYhXUhBMhe6kId/cuVq/AfEnHRHEvsP8vXh03M1uLpi5e46yO2Q8rEBzu3feXQewcQE5GArp88u6ePK6BA==";
173
174 let locator = setup_locator();
175 let cmd = super::Cmd {
176 message: Some(message),
177 base64: false,
178 sign_with_key: TEST_SECRET_KEY.to_string(),
179 hd_path: None,
180 locator: locator.clone(),
181 };
182 let signer = build_signer_for_test_key();
183
184 let message_bytes = cmd.get_message_bytes().unwrap();
185 let signature_base64 = sep_53_sign(&message_bytes, signer).unwrap();
186
187 assert_eq!(signature_base64, expected_signature);
188 }
189
190 #[test]
191 fn test_sign_japanese() {
192 let message = "こんにちは、世界!".to_string();
194 let expected_signature = "CDU265Xs8y3OWbB/56H9jPgUss5G9A0qFuTqH2zs2YDgTm+++dIfmAEceFqB7bhfN3am59lCtDXrCtwH2k1GBA==";
195
196 let locator = setup_locator();
197 let cmd = super::Cmd {
198 message: Some(message),
199 base64: false,
200 sign_with_key: TEST_SECRET_KEY.to_string(),
201 hd_path: None,
202 locator: locator.clone(),
203 };
204 let signer = build_signer_for_test_key();
205
206 let message_bytes = cmd.get_message_bytes().unwrap();
207 let signature_base64 = sep_53_sign(&message_bytes, signer).unwrap();
208
209 assert_eq!(signature_base64, expected_signature);
210 }
211
212 #[test]
213 fn test_sign_base64() {
214 let message = "2zZDP1sa1BVBfLP7TeeMk3sUbaxAkUhBhDiNdrksaFo=".to_string();
216 let expected_signature = "VA1+7hefNwv2NKScH6n+Sljj15kLAge+M2wE7fzFOf+L0MMbssA1mwfJZRyyrhBORQRle10X1Dxpx+UOI4EbDQ==";
217
218 let locator = setup_locator();
219 let cmd = super::Cmd {
220 message: Some(message),
221 base64: true,
222 sign_with_key: TEST_SECRET_KEY.to_string(),
223 hd_path: None,
224 locator: locator.clone(),
225 };
226 let signer = build_signer_for_test_key();
227
228 let message_bytes = cmd.get_message_bytes().unwrap();
229 let signature_base64 = sep_53_sign(&message_bytes, signer).unwrap();
230
231 assert_eq!(signature_base64, expected_signature);
232 }
233}