soroban_cli/commands/message/
verify.rs1use std::io::{self, Read};
2
3use crate::{
4 commands::global,
5 config::{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 Ed25519(#[from] ed25519_dalek::SignatureError),
34
35 #[error(transparent)]
36 Address(#[from] crate::config::address::Error),
37
38 #[error("Signature verification failed")]
39 VerificationFailed,
40
41 #[error("Invalid signature length: expected 64 bytes, got {0}")]
42 InvalidSignatureLength(usize),
43}
44
45#[derive(Debug, Parser, Clone)]
46#[group(skip)]
47pub struct Cmd {
48 #[arg()]
51 pub message: Option<String>,
52
53 #[arg(long)]
55 pub base64: bool,
56
57 #[arg(long, short = 's')]
59 pub signature: String,
60
61 #[arg(long, short = 'p')]
64 pub public_key: String,
65
66 #[arg(long)]
68 pub hd_path: Option<usize>,
69
70 #[command(flatten)]
71 pub locator: locator::Args,
72}
73
74impl Cmd {
75 pub fn run(&self, global_args: &global::Args) -> Result<(), Error> {
76 let print = Print::new(global_args.quiet);
77
78 let message_bytes = self.get_message_bytes()?;
80 let mut payload = Vec::with_capacity(SEP53_PREFIX.len() + message_bytes.len());
81 payload.extend_from_slice(SEP53_PREFIX.as_bytes());
82 payload.extend_from_slice(&message_bytes);
83
84 let hash: [u8; 32] = Sha256::digest(&payload).into();
86
87 let signature_bytes = BASE64.decode(&self.signature)?;
89 if signature_bytes.len() != 64 {
90 return Err(Error::InvalidSignatureLength(signature_bytes.len()));
91 }
92 let signature = Signature::from_slice(&signature_bytes)?;
93
94 let public_key = self.get_public_key()?;
96 print.infoln(format!("Verifying signature against: {public_key}"));
97 let verifying_key = VerifyingKey::from_bytes(&public_key.0)?;
98
99 if verifying_key.verify(&hash, &signature).is_ok() {
101 print.checkln("Signature valid");
102 Ok(())
103 } else {
104 print.errorln("Signature invalid");
105 Err(Error::VerificationFailed)
106 }
107 }
108
109 fn get_message_bytes(&self) -> Result<Vec<u8>, Error> {
110 let message_str = if let Some(msg) = &self.message {
111 msg.clone()
112 } else {
113 let mut buffer = String::new();
115 io::stdin().read_to_string(&mut buffer)?;
116 if buffer.ends_with('\n') {
118 buffer.pop();
119 if buffer.ends_with('\r') {
120 buffer.pop();
121 }
122 }
123 buffer
124 };
125
126 if self.base64 {
127 Ok(BASE64.decode(&message_str)?)
129 } else {
130 Ok(message_str.into_bytes())
132 }
133 }
134
135 fn get_public_key(&self) -> Result<stellar_strkey::ed25519::PublicKey, Error> {
136 if let Ok(pk) = stellar_strkey::ed25519::PublicKey::from_string(&self.public_key) {
138 return Ok(pk);
139 }
140
141 let account = self
143 .locator
144 .read_key(&self.public_key)?
145 .muxed_account(self.hd_path)
146 .map_err(crate::config::address::Error::from)?;
147 let bytes = match account {
148 soroban_sdk::xdr::MuxedAccount::Ed25519(uint256) => uint256.0,
149 soroban_sdk::xdr::MuxedAccount::MuxedEd25519(muxed_account) => muxed_account.ed25519.0,
150 };
151 Ok(stellar_strkey::ed25519::PublicKey(bytes))
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 const TEST_PUBLIC_KEY: &str = "GBXFXNDLV4LSWA4VB7YIL5GBD7BVNR22SGBTDKMO2SBZZHDXSKZYCP7L";
161 const FALSE_PUBLIC_KEY: &str = "GAREAZZQWHOCBJS236KIE3AWYBVFLSBK7E5UW3ICI3TCRWQKT5LNLCEZ";
162 const FALSE_SIGNATURE: &str =
163 "+F//cUINZgTe4vZNXOEJTchDgEYlvy+iGFH3P65KeVhoyZgAsmGRRYAQLVqgY9J3PAlHPbSSeU5advhswmAfDg==";
164
165 fn setup_locator() -> locator::Args {
166 let temp_dir = tempfile::tempdir().unwrap();
167 locator::Args {
168 global: false,
169 config_dir: Some(temp_dir.path().to_path_buf()),
170 }
171 }
172
173 fn global_args() -> global::Args {
174 global::Args {
175 quiet: true,
176 ..Default::default()
177 }
178 }
179
180 #[test]
181 fn test_verify_simple() {
182 let message = "Hello, World!".to_string();
184 let signature = "fO5dbYhXUhBMhe6kId/cuVq/AfEnHRHEvsP8vXh03M1uLpi5e46yO2Q8rEBzu3feXQewcQE5GArp88u6ePK6BA==";
185
186 let global = global_args();
187 let locator = setup_locator();
188 let cmd = super::Cmd {
189 message: Some(message),
190 base64: false,
191 signature: signature.to_string(),
192 public_key: TEST_PUBLIC_KEY.to_string(),
193 hd_path: None,
194 locator: locator.clone(),
195 };
196 let successful = cmd.run(&global);
197 assert!(successful.is_ok());
198 }
199
200 #[test]
201 fn test_verify_japanese() {
202 let message = "こんにちは、世界!".to_string();
204 let signature = "CDU265Xs8y3OWbB/56H9jPgUss5G9A0qFuTqH2zs2YDgTm+++dIfmAEceFqB7bhfN3am59lCtDXrCtwH2k1GBA==";
205
206 let global = global_args();
207 let locator = setup_locator();
208 let cmd = super::Cmd {
209 message: Some(message),
210 base64: false,
211 signature: signature.to_string(),
212 public_key: TEST_PUBLIC_KEY.to_string(),
213 hd_path: None,
214 locator: locator.clone(),
215 };
216 let successful = cmd.run(&global);
217 assert!(successful.is_ok());
218 }
219
220 #[test]
221 fn test_verify_base64() {
222 let message = "2zZDP1sa1BVBfLP7TeeMk3sUbaxAkUhBhDiNdrksaFo=".to_string();
224 let signature = "VA1+7hefNwv2NKScH6n+Sljj15kLAge+M2wE7fzFOf+L0MMbssA1mwfJZRyyrhBORQRle10X1Dxpx+UOI4EbDQ==";
225
226 let global = global_args();
227 let locator = setup_locator();
228 let cmd = super::Cmd {
229 message: Some(message),
230 base64: true,
231 signature: signature.to_string(),
232 public_key: TEST_PUBLIC_KEY.to_string(),
233 hd_path: None,
234 locator: locator.clone(),
235 };
236 let successful = cmd.run(&global);
237 assert!(successful.is_ok());
238 }
239
240 #[test]
241 fn test_verify_bad_signature_errors() {
242 let message = "Hello, World!".to_string();
243
244 let global = global_args();
245 let locator = setup_locator();
246 let cmd = super::Cmd {
247 message: Some(message),
248 base64: false,
249 signature: FALSE_SIGNATURE.to_string(),
250 public_key: TEST_PUBLIC_KEY.to_string(),
251 hd_path: None,
252 locator: locator.clone(),
253 };
254 let successful = cmd.run(&global);
255 assert!(successful.is_err());
256 }
257
258 #[test]
259 fn test_verify_bad_pubkey_errors() {
260 let message = "Hello, World!".to_string();
261 let signature = "fO5dbYhXUhBMhe6kId/cuVq/AfEnHRHEvsP8vXh03M1uLpi5e46yO2Q8rEBzu3feXQewcQE5GArp88u6ePK6BA==";
262
263 let global = global_args();
264 let locator = setup_locator();
265 let cmd = super::Cmd {
266 message: Some(message),
267 base64: false,
268 signature: signature.to_string(),
269 public_key: FALSE_PUBLIC_KEY.to_string(),
270 hd_path: None,
271 locator: locator.clone(),
272 };
273 let successful = cmd.run(&global);
274 assert!(successful.is_err());
275 }
276}