soroban_cli/commands/message/
verify.rs1use std::io::{self, Read};
2
3use crate::{
4 commands::global,
5 config::{key, locator, secret},
6 print::Print,
7};
8use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
9use clap::Parser;
10use ed25519_dalek::{Signature, Verifier, VerifyingKey};
11use sha2::{Digest, Sha256};
12
13use super::SEP53_PREFIX;
14
15#[derive(thiserror::Error, Debug)]
16pub enum Error {
17 #[error(transparent)]
18 Locator(#[from] locator::Error),
19
20 #[error(transparent)]
21 Secret(#[from] secret::Error),
22
23 #[error(transparent)]
24 Io(#[from] io::Error),
25
26 #[error(transparent)]
27 Base64(#[from] base64::DecodeError),
28
29 #[error(transparent)]
30 StrKey(#[from] stellar_strkey::DecodeError),
31
32 #[error(transparent)]
33 Key(#[from] key::Error),
34
35 #[error(transparent)]
36 Ed25519(#[from] ed25519_dalek::SignatureError),
37
38 #[error(transparent)]
39 Address(#[from] crate::config::address::Error),
40
41 #[error("Signature verification failed")]
42 VerificationFailed,
43
44 #[error("Invalid signature length: expected 64 bytes, got {0}")]
45 InvalidSignatureLength(usize),
46}
47
48#[derive(Debug, Parser, Clone)]
49#[group(skip)]
50pub struct Cmd {
51 #[arg()]
54 pub message: Option<String>,
55
56 #[arg(long)]
58 pub base64: bool,
59
60 #[arg(long, short = 's')]
62 pub signature: String,
63
64 #[arg(long, short = 'p')]
67 pub public_key: String,
68
69 #[arg(long)]
71 pub hd_path: Option<u32>,
72
73 #[command(flatten)]
74 pub locator: locator::Args,
75}
76
77impl Cmd {
78 pub fn run(&self, global_args: &global::Args) -> Result<(), Error> {
79 let print = Print::new(global_args.quiet);
80
81 let message_bytes = self.get_message_bytes()?;
83 let mut payload = Vec::with_capacity(SEP53_PREFIX.len() + message_bytes.len());
84 payload.extend_from_slice(SEP53_PREFIX.as_bytes());
85 payload.extend_from_slice(&message_bytes);
86
87 let hash: [u8; 32] = Sha256::digest(&payload).into();
89
90 let signature_bytes = BASE64.decode(&self.signature)?;
92 if signature_bytes.len() != 64 {
93 return Err(Error::InvalidSignatureLength(signature_bytes.len()));
94 }
95 let signature = Signature::from_slice(&signature_bytes)?;
96
97 let public_key = self.get_public_key()?;
99 print.infoln(format!("Verifying signature against: {public_key}"));
100 let verifying_key = VerifyingKey::from_bytes(&public_key.0)?;
101
102 if verifying_key.verify(&hash, &signature).is_ok() {
104 print.checkln("Signature valid");
105 Ok(())
106 } else {
107 print.errorln("Signature invalid");
108 Err(Error::VerificationFailed)
109 }
110 }
111
112 fn get_message_bytes(&self) -> Result<Vec<u8>, Error> {
113 let message_str = if let Some(msg) = &self.message {
114 msg.clone()
115 } else {
116 let mut buffer = String::new();
118 io::stdin().read_to_string(&mut buffer)?;
119 if buffer.ends_with('\n') {
121 buffer.pop();
122 if buffer.ends_with('\r') {
123 buffer.pop();
124 }
125 }
126 buffer
127 };
128
129 if self.base64 {
130 Ok(BASE64.decode(&message_str)?)
132 } else {
133 Ok(message_str.into_bytes())
135 }
136 }
137
138 fn get_public_key(&self) -> Result<stellar_strkey::ed25519::PublicKey, Error> {
139 let key = match key::Key::parse_public_only(&self.public_key) {
142 Ok(key) => key,
143 Err(err @ key::Error::PublicKeyExpected) => return Err(Error::Key(err)),
144 Err(_) => self.locator.read_key(&self.public_key)?,
145 };
146 let account = key
147 .muxed_account(self.hd_path)
148 .map_err(crate::config::address::Error::from)?;
149 let bytes = match account {
150 soroban_sdk::xdr::MuxedAccount::Ed25519(uint256) => uint256.0,
151 soroban_sdk::xdr::MuxedAccount::MuxedEd25519(muxed_account) => muxed_account.ed25519.0,
152 };
153 Ok(stellar_strkey::ed25519::PublicKey(bytes))
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 const TEST_PUBLIC_KEY: &str = "GBXFXNDLV4LSWA4VB7YIL5GBD7BVNR22SGBTDKMO2SBZZHDXSKZYCP7L";
163 const FALSE_PUBLIC_KEY: &str = "GAREAZZQWHOCBJS236KIE3AWYBVFLSBK7E5UW3ICI3TCRWQKT5LNLCEZ";
164 const FALSE_SIGNATURE: &str =
165 "+F//cUINZgTe4vZNXOEJTchDgEYlvy+iGFH3P65KeVhoyZgAsmGRRYAQLVqgY9J3PAlHPbSSeU5advhswmAfDg==";
166
167 fn setup_locator() -> locator::Args {
168 let temp_dir = tempfile::tempdir().unwrap();
169 locator::Args {
170 config_dir: Some(temp_dir.path().to_path_buf()),
171 }
172 }
173
174 fn global_args() -> global::Args {
175 global::Args {
176 quiet: true,
177 ..Default::default()
178 }
179 }
180
181 #[test]
182 fn test_verify_simple() {
183 let message = "Hello, World!".to_string();
185 let signature = "fO5dbYhXUhBMhe6kId/cuVq/AfEnHRHEvsP8vXh03M1uLpi5e46yO2Q8rEBzu3feXQewcQE5GArp88u6ePK6BA==";
186
187 let global = global_args();
188 let locator = setup_locator();
189 let cmd = super::Cmd {
190 message: Some(message),
191 base64: false,
192 signature: signature.to_string(),
193 public_key: TEST_PUBLIC_KEY.to_string(),
194 hd_path: None,
195 locator: locator.clone(),
196 };
197 let successful = cmd.run(&global);
198 assert!(successful.is_ok());
199 }
200
201 #[test]
202 fn test_verify_japanese() {
203 let message = "こんにちは、世界!".to_string();
205 let signature = "CDU265Xs8y3OWbB/56H9jPgUss5G9A0qFuTqH2zs2YDgTm+++dIfmAEceFqB7bhfN3am59lCtDXrCtwH2k1GBA==";
206
207 let global = global_args();
208 let locator = setup_locator();
209 let cmd = super::Cmd {
210 message: Some(message),
211 base64: false,
212 signature: signature.to_string(),
213 public_key: TEST_PUBLIC_KEY.to_string(),
214 hd_path: None,
215 locator: locator.clone(),
216 };
217 let successful = cmd.run(&global);
218 assert!(successful.is_ok());
219 }
220
221 #[test]
222 fn test_verify_base64() {
223 let message = "2zZDP1sa1BVBfLP7TeeMk3sUbaxAkUhBhDiNdrksaFo=".to_string();
225 let signature = "VA1+7hefNwv2NKScH6n+Sljj15kLAge+M2wE7fzFOf+L0MMbssA1mwfJZRyyrhBORQRle10X1Dxpx+UOI4EbDQ==";
226
227 let global = global_args();
228 let locator = setup_locator();
229 let cmd = super::Cmd {
230 message: Some(message),
231 base64: true,
232 signature: signature.to_string(),
233 public_key: TEST_PUBLIC_KEY.to_string(),
234 hd_path: None,
235 locator: locator.clone(),
236 };
237 let successful = cmd.run(&global);
238 assert!(successful.is_ok());
239 }
240
241 #[test]
242 fn test_verify_bad_signature_errors() {
243 let message = "Hello, World!".to_string();
244
245 let global = global_args();
246 let locator = setup_locator();
247 let cmd = super::Cmd {
248 message: Some(message),
249 base64: false,
250 signature: FALSE_SIGNATURE.to_string(),
251 public_key: TEST_PUBLIC_KEY.to_string(),
252 hd_path: None,
253 locator: locator.clone(),
254 };
255 let successful = cmd.run(&global);
256 assert!(successful.is_err());
257 }
258
259 #[test]
260 fn test_verify_bad_pubkey_errors() {
261 let message = "Hello, World!".to_string();
262 let signature = "fO5dbYhXUhBMhe6kId/cuVq/AfEnHRHEvsP8vXh03M1uLpi5e46yO2Q8rEBzu3feXQewcQE5GArp88u6ePK6BA==";
263
264 let global = global_args();
265 let locator = setup_locator();
266 let cmd = super::Cmd {
267 message: Some(message),
268 base64: false,
269 signature: signature.to_string(),
270 public_key: FALSE_PUBLIC_KEY.to_string(),
271 hd_path: None,
272 locator: locator.clone(),
273 };
274 let successful = cmd.run(&global);
275 assert!(successful.is_err());
276 }
277
278 #[test]
279 fn test_verify_rejects_raw_secret_key_as_public_key() {
280 let secret_key = "SBF5HLRREHMS36XZNTUSKZ6FTXDZGNXOHF4EXKUL5UCWZLPBX3NGJ4BH";
281 let cmd = super::Cmd {
282 message: Some("Hello, World!".to_string()),
283 base64: false,
284 signature: "fO5dbYhXUhBMhe6kId/cuVq/AfEnHRHEvsP8vXh03M1uLpi5e46yO2Q8rEBzu3feXQewcQE5GArp88u6ePK6BA==".to_string(),
285 public_key: secret_key.to_string(),
286 hd_path: None,
287 locator: setup_locator(),
288 };
289 let err = cmd.run(&global_args()).unwrap_err();
290 assert!(matches!(
291 err,
292 Error::Key(crate::config::key::Error::PublicKeyExpected)
293 ));
294 }
295
296 #[test]
297 fn test_verify_rejects_raw_seed_phrase_as_public_key() {
298 let seed_phrase =
299 "depth decade power loud smile spatial sign movie judge february rate broccoli";
300 let cmd = super::Cmd {
301 message: Some("Hello, World!".to_string()),
302 base64: false,
303 signature: "fO5dbYhXUhBMhe6kId/cuVq/AfEnHRHEvsP8vXh03M1uLpi5e46yO2Q8rEBzu3feXQewcQE5GArp88u6ePK6BA==".to_string(),
304 public_key: seed_phrase.to_string(),
305 hd_path: None,
306 locator: setup_locator(),
307 };
308 let err = cmd.run(&global_args()).unwrap_err();
309 assert!(matches!(
310 err,
311 Error::Key(crate::config::key::Error::PublicKeyExpected)
312 ));
313 }
314
315 #[test]
316 fn test_verify_rejects_secure_store_as_public_key() {
317 let cmd = super::Cmd {
318 message: Some("Hello, World!".to_string()),
319 base64: false,
320 signature: "fO5dbYhXUhBMhe6kId/cuVq/AfEnHRHEvsP8vXh03M1uLpi5e46yO2Q8rEBzu3feXQewcQE5GArp88u6ePK6BA==".to_string(),
321 public_key: "secure_store:org.stellar.cli-alice".to_string(),
322 hd_path: None,
323 locator: setup_locator(),
324 };
325 let err = cmd.run(&global_args()).unwrap_err();
326 assert!(matches!(
327 err,
328 Error::Key(crate::config::key::Error::PublicKeyExpected)
329 ));
330 }
331}