From 3d5bf2c1f479209ee47f6dbdbab5e85005838280 Mon Sep 17 00:00:00 2001 From: Tomer Date: Tue, 23 Dec 2025 18:19:48 -0500 Subject: [PATCH 1/3] feat: add SEP-53 message signing and verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add support for signing and verifying arbitrary messages per SEP-53. New commands: - `stellar message sign` - Sign a message using SEP-53 specification - `stellar message verify` - Verify a SEP-53 signed message Features: - Sign messages with local keys, seed phrases, or secure store - Support for both UTF-8 text and binary (base64) input - Signature output in base64 format - Identity lookup for signing keys Implementation follows the SEP-53 specification: 1. Prepend "Stellar Signed Message:\n" prefix to message 2. SHA-256 hash the prefixed payload 3. ed25519 sign the hash Closes #2345 πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cmd/soroban-cli/src/commands/message/mod.rs | 47 +++ cmd/soroban-cli/src/commands/message/sign.rs | 305 ++++++++++++++++++ .../src/commands/message/verify.rs | 233 +++++++++++++ cmd/soroban-cli/src/commands/mod.rs | 9 + 4 files changed, 594 insertions(+) create mode 100644 cmd/soroban-cli/src/commands/message/mod.rs create mode 100644 cmd/soroban-cli/src/commands/message/sign.rs create mode 100644 cmd/soroban-cli/src/commands/message/verify.rs diff --git a/cmd/soroban-cli/src/commands/message/mod.rs b/cmd/soroban-cli/src/commands/message/mod.rs new file mode 100644 index 000000000..23133c432 --- /dev/null +++ b/cmd/soroban-cli/src/commands/message/mod.rs @@ -0,0 +1,47 @@ +use crate::commands::global; + +pub mod sign; +pub mod verify; + +/// The prefix used for SEP-53 message signing. +/// See: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0053.md +pub const SEP53_PREFIX: &str = "Stellar Signed Message:\n"; + +#[derive(Debug, clap::Subcommand)] +pub enum Cmd { + /// Sign an arbitrary message using SEP-53 + /// + /// Signs a message following the SEP-53 specification for arbitrary message signing. + /// The message is prefixed with "Stellar Signed Message:\n", hashed with SHA-256, + /// and signed with the ed25519 private key. + /// + /// Example: stellar message sign "Hello, World!" --sign-with-key alice + Sign(sign::Cmd), + + /// Verify a SEP-53 signed message + /// + /// Verifies that a signature was produced by the holder of the private key + /// corresponding to the given public key, following the SEP-53 specification. + /// + /// Example: stellar message verify "Hello, World!" --signature --public-key GABC... + Verify(verify::Cmd), +} + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Sign(#[from] sign::Error), + + #[error(transparent)] + Verify(#[from] verify::Error), +} + +impl Cmd { + pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { + match self { + Cmd::Sign(cmd) => cmd.run(global_args).await?, + Cmd::Verify(cmd) => cmd.run()?, + } + Ok(()) + } +} diff --git a/cmd/soroban-cli/src/commands/message/sign.rs b/cmd/soroban-cli/src/commands/message/sign.rs new file mode 100644 index 000000000..01cc4bfe7 --- /dev/null +++ b/cmd/soroban-cli/src/commands/message/sign.rs @@ -0,0 +1,305 @@ +use std::io::{self, Read}; + +use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; +use clap::{arg, Parser}; +use ed25519_dalek::Signer as _; +use sha2::{Digest, Sha256}; + +use crate::{ + commands::global, + config::{locator, secret}, + signer::{self, SecureStoreEntry}, +}; + +use super::SEP53_PREFIX; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Locator(#[from] locator::Error), + + #[error(transparent)] + Secret(#[from] secret::Error), + + #[error(transparent)] + Signer(#[from] signer::Error), + + #[error(transparent)] + Io(#[from] io::Error), + + #[error(transparent)] + Base64(#[from] base64::DecodeError), + + #[error(transparent)] + StrKey(#[from] stellar_strkey::DecodeError), + + #[error(transparent)] + Ed25519(#[from] ed25519_dalek::SignatureError), + + #[error("No signing key provided. Use --sign-with-key")] + NoSigningKey, + + #[error("Ledger signing of arbitrary messages is not yet supported")] + LedgerNotSupported, +} + +#[derive(Debug, Parser, Clone)] +#[group(skip)] +pub struct Cmd { + /// The message to sign. If not provided, reads from stdin. + #[arg()] + pub message: Option, + + /// Treat the message as base64-encoded binary data + #[arg(long)] + pub base64: bool, + + /// Sign with a local key. Can be an identity (--sign-with-key alice), + /// a secret key (--sign-with-key SC36...), or a seed phrase + /// (--sign-with-key "kite urban..."). + #[arg(long, env = "STELLAR_SIGN_WITH_KEY")] + pub sign_with_key: Option, + + /// If using a seed phrase to sign, sets which hierarchical deterministic + /// path to use, e.g. `m/44'/148'/{hd_path}`. Example: `--hd-path 1`. Default: `0` + #[arg(long)] + pub hd_path: Option, + + /// Sign with a Ledger hardware wallet + #[arg(long, conflicts_with = "sign_with_key", env = "STELLAR_SIGN_WITH_LEDGER")] + pub sign_with_ledger: bool, + + #[command(flatten)] + pub locator: locator::Args, +} + +/// Output format for signed messages +#[derive(serde::Serialize)] +struct SignedMessageOutput { + /// The public key (address) that signed the message + signer: String, + /// The original message (as provided or base64 if binary) + message: String, + /// The base64-encoded signature + signature: String, +} + +impl Cmd { + pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { + // Get the message bytes + let message_bytes = self.get_message_bytes()?; + + // Create the SEP-53 payload: prefix + message + let mut payload = Vec::with_capacity(SEP53_PREFIX.len() + message_bytes.len()); + payload.extend_from_slice(SEP53_PREFIX.as_bytes()); + payload.extend_from_slice(&message_bytes); + + // Hash the payload with SHA-256 + let hash: [u8; 32] = Sha256::digest(&payload).into(); + + // Get the signer and sign + let (public_key, signature) = self.sign_hash(hash)?; + + // Encode signature as base64 + let signature_base64 = BASE64.encode(signature.to_bytes()); + + // Output the result + let output = SignedMessageOutput { + signer: public_key.to_string(), + message: if self.base64 { + BASE64.encode(&message_bytes) + } else { + String::from_utf8_lossy(&message_bytes).to_string() + }, + signature: signature_base64.clone(), + }; + + if global_args.quiet { + // In quiet mode, just output the signature + println!("{signature_base64}"); + } else { + // Output as formatted text + println!("Signer: {}", output.signer); + println!("Signature: {}", output.signature); + } + + Ok(()) + } + + fn sign_hash( + &self, + hash: [u8; 32], + ) -> Result<(stellar_strkey::ed25519::PublicKey, ed25519_dalek::Signature), Error> { + if self.sign_with_ledger { + // Ledger doesn't support signing arbitrary messages yet + return Err(Error::LedgerNotSupported); + } + + let key_or_name = self.sign_with_key.as_deref().ok_or(Error::NoSigningKey)?; + let secret = self.locator.get_secret_key(key_or_name)?; + + match &secret { + secret::Secret::SecretKey { .. } | secret::Secret::SeedPhrase { .. } => { + let signing_key = secret.key_pair(self.hd_path)?; + let public_key = stellar_strkey::ed25519::PublicKey::from_payload( + signing_key.verifying_key().as_bytes(), + )?; + let signature = signing_key.sign(&hash); + Ok((public_key, signature)) + } + secret::Secret::Ledger => { + // Ledger doesn't support signing arbitrary messages yet + Err(Error::LedgerNotSupported) + } + secret::Secret::SecureStore { entry_name } => { + let entry = SecureStoreEntry { + name: entry_name.clone(), + hd_path: self.hd_path, + }; + let public_key = entry.get_public_key()?; + let signature = entry.sign_payload(hash)?; + Ok((public_key, signature)) + } + } + } + + fn get_message_bytes(&self) -> Result, Error> { + let message_str = match &self.message { + Some(msg) => msg.clone(), + None => { + // Read from stdin + let mut buffer = String::new(); + io::stdin().read_to_string(&mut buffer)?; + // Remove trailing newline if present + if buffer.ends_with('\n') { + buffer.pop(); + if buffer.ends_with('\r') { + buffer.pop(); + } + } + buffer + } + }; + + if self.base64 { + // Decode base64 input + Ok(BASE64.decode(&message_str)?) + } else { + // Use UTF-8 encoded message + Ok(message_str.into_bytes()) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::secret::Secret; + use std::str::FromStr; + + // Use a known valid test key from the codebase + const TEST_SECRET_KEY: &str = "SBF5HLRREHMS36XZNTUSKZ6FTXDZGNXOHF4EXKUL5UCWZLPBX3NGJ4BH"; + const TEST_PUBLIC_KEY: &str = "GAREAZZQWHOCBJS236KIE3AWYBVFLSBK7E5UW3ICI3TCRWQKT5LNLCEZ"; + + fn get_test_signing_key() -> ed25519_dalek::SigningKey { + let secret = Secret::from_str(TEST_SECRET_KEY).unwrap(); + secret.key_pair(None).unwrap() + } + + fn sign_message(message_bytes: &[u8], signing_key: &ed25519_dalek::SigningKey) -> String { + // Create SEP-53 payload + let mut payload = Vec::with_capacity(SEP53_PREFIX.len() + message_bytes.len()); + payload.extend_from_slice(SEP53_PREFIX.as_bytes()); + payload.extend_from_slice(message_bytes); + + // Hash with SHA-256 + let hash: [u8; 32] = Sha256::digest(&payload).into(); + + // Sign + let signature = signing_key.sign(&hash); + + // Return base64-encoded signature + BASE64.encode(signature.to_bytes()) + } + + fn verify_signature( + message_bytes: &[u8], + signature_base64: &str, + signing_key: &ed25519_dalek::SigningKey, + ) -> bool { + use ed25519_dalek::Verifier; + + // Create SEP-53 payload + let mut payload = Vec::with_capacity(SEP53_PREFIX.len() + message_bytes.len()); + payload.extend_from_slice(SEP53_PREFIX.as_bytes()); + payload.extend_from_slice(message_bytes); + + // Hash with SHA-256 + let hash: [u8; 32] = Sha256::digest(&payload).into(); + + // Decode signature + let signature_bytes = BASE64.decode(signature_base64).unwrap(); + let signature = ed25519_dalek::Signature::from_slice(&signature_bytes).unwrap(); + + // Verify + signing_key.verifying_key().verify(&hash, &signature).is_ok() + } + + #[test] + fn test_sign_and_verify_ascii_message() { + let signing_key = get_test_signing_key(); + + // Verify public key matches expected + let public_key = stellar_strkey::ed25519::PublicKey::from_payload( + signing_key.verifying_key().as_bytes(), + ) + .unwrap(); + assert_eq!(public_key.to_string(), TEST_PUBLIC_KEY); + + // Sign and verify + let message = "Hello, World!"; + let signature = sign_message(message.as_bytes(), &signing_key); + assert!(verify_signature(message.as_bytes(), &signature, &signing_key)); + } + + #[test] + fn test_sign_and_verify_utf8_message() { + let signing_key = get_test_signing_key(); + + // Sign and verify Japanese text + let message = "γ“γ‚“γ«γ‘γ―γ€δΈ–η•ŒοΌ"; + let signature = sign_message(message.as_bytes(), &signing_key); + assert!(verify_signature(message.as_bytes(), &signature, &signing_key)); + } + + #[test] + fn test_sign_and_verify_binary_message() { + let signing_key = get_test_signing_key(); + + // Sign and verify binary data + let message_base64 = "2zZDP1sa1BVBfLP7TeeMk3sUbaxAkUhBhDiNdrksaFo="; + let message_bytes = BASE64.decode(message_base64).unwrap(); + let signature = sign_message(&message_bytes, &signing_key); + assert!(verify_signature(&message_bytes, &signature, &signing_key)); + } + + #[test] + fn test_sep53_prefix_is_correct() { + // Verify the SEP-53 prefix is as specified + assert_eq!(SEP53_PREFIX, "Stellar Signed Message:\n"); + } + + #[test] + fn test_wrong_signature_fails_verification() { + let signing_key = get_test_signing_key(); + + let message1 = "Hello, World!"; + let message2 = "Goodbye, World!"; + + // Sign message1 + let signature = sign_message(message1.as_bytes(), &signing_key); + + // Verify fails with different message + assert!(!verify_signature(message2.as_bytes(), &signature, &signing_key)); + } +} diff --git a/cmd/soroban-cli/src/commands/message/verify.rs b/cmd/soroban-cli/src/commands/message/verify.rs new file mode 100644 index 000000000..f23867750 --- /dev/null +++ b/cmd/soroban-cli/src/commands/message/verify.rs @@ -0,0 +1,233 @@ +use std::io::{self, Read}; + +use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; +use clap::{arg, Parser}; +use ed25519_dalek::{Signature, Verifier, VerifyingKey}; +use sha2::{Digest, Sha256}; + +use crate::config::{locator, secret}; + +use super::SEP53_PREFIX; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Locator(#[from] locator::Error), + + #[error(transparent)] + Secret(#[from] secret::Error), + + #[error(transparent)] + Io(#[from] io::Error), + + #[error(transparent)] + Base64(#[from] base64::DecodeError), + + #[error(transparent)] + StrKey(#[from] stellar_strkey::DecodeError), + + #[error(transparent)] + Ed25519(#[from] ed25519_dalek::SignatureError), + + #[error("Signature verification failed")] + VerificationFailed, + + #[error("Invalid signature length: expected 64 bytes, got {0}")] + InvalidSignatureLength(usize), +} + +#[derive(Debug, Parser, Clone)] +#[group(skip)] +pub struct Cmd { + /// The message to verify. If not provided, reads from stdin. + #[arg()] + pub message: Option, + + /// The base64-encoded signature to verify + #[arg(long, short = 's')] + pub signature: String, + + /// The public key to verify against. + /// Can be a Stellar public key (G...) or an identity name. + #[arg(long, short = 'p')] + pub public_key: String, + + /// Treat the message as base64-encoded binary data + #[arg(long)] + pub base64: bool, + + #[command(flatten)] + pub locator: locator::Args, +} + +impl Cmd { + pub fn run(&self) -> Result<(), Error> { + // Get the message bytes + let message_bytes = self.get_message_bytes()?; + + // Create the SEP-53 payload: prefix + message + let mut payload = Vec::with_capacity(SEP53_PREFIX.len() + message_bytes.len()); + payload.extend_from_slice(SEP53_PREFIX.as_bytes()); + payload.extend_from_slice(&message_bytes); + + // Hash the payload with SHA-256 + let hash: [u8; 32] = Sha256::digest(&payload).into(); + + // Decode the signature + let signature_bytes = BASE64.decode(&self.signature)?; + if signature_bytes.len() != 64 { + return Err(Error::InvalidSignatureLength(signature_bytes.len())); + } + let signature = Signature::from_slice(&signature_bytes)?; + + // Get the public key + let public_key_bytes = self.get_public_key_bytes()?; + let verifying_key = VerifyingKey::from_bytes(&public_key_bytes)?; + + // Verify the signature + match verifying_key.verify(&hash, &signature) { + Ok(()) => { + let public_key = stellar_strkey::ed25519::PublicKey(public_key_bytes); + println!("Signature valid"); + println!("Signer: {public_key}"); + Ok(()) + } + Err(_) => { + eprintln!("Signature invalid"); + Err(Error::VerificationFailed) + } + } + } + + fn get_message_bytes(&self) -> Result, Error> { + let message_str = match &self.message { + Some(msg) => msg.clone(), + None => { + // Read from stdin + let mut buffer = String::new(); + io::stdin().read_to_string(&mut buffer)?; + // Remove trailing newline if present + if buffer.ends_with('\n') { + buffer.pop(); + if buffer.ends_with('\r') { + buffer.pop(); + } + } + buffer + } + }; + + if self.base64 { + // Decode base64 input + Ok(BASE64.decode(&message_str)?) + } else { + // Use UTF-8 encoded message + Ok(message_str.into_bytes()) + } + } + + fn get_public_key_bytes(&self) -> Result<[u8; 32], Error> { + // First, try to parse as a Stellar public key directly + if let Ok(pk) = stellar_strkey::ed25519::PublicKey::from_string(&self.public_key) { + return Ok(pk.0); + } + + // Otherwise, try to look it up as an identity + let secret = self.locator.get_secret_key(&self.public_key)?; + let pk = secret.public_key(None)?; + Ok(pk.0) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // Test vectors from SEP-53 + const TEST_PUBLIC_KEY: &str = "GBXFXNDLV4LSWA4VB7YIL5GBD7BVNR22SGBTDKMO2SBZZHDXSKZYCP7L"; + + // Test case 1: ASCII message + const TEST_MESSAGE_1: &str = "Hello, World!"; + const TEST_SIGNATURE_1: &str = + "fO5dbYhXUhBMhe6kId/cuVq/AfEnHRHEvsP8vXh03M1uLpi5e46yO2Q8rEBzu3feXQewcQE5GArp88u6ePK6BA=="; + + // Test case 2: Japanese text (UTF-8) + const TEST_MESSAGE_2: &str = "γ“γ‚“γ«γ‘γ―γ€δΈ–η•ŒοΌ"; + const TEST_SIGNATURE_2: &str = + "CDU265Xs8y3OWbB/56H9jPgUss5G9A0qFuTqH2zs2YDgTm+++dIfmAEceFqB7bhfN3am59lCtDXrCtwH2k1GBA=="; + + // Test case 3: Binary data (base64 encoded in test vector) + const TEST_MESSAGE_3_BASE64: &str = "2zZDP1sa1BVBfLP7TeeMk3sUbaxAkUhBhDiNdrksaFo="; + const TEST_SIGNATURE_3: &str = + "VA1+7hefNwv2NKScH6n+Sljj15kLAge+M2wE7fzFOf+L0MMbssA1mwfJZRyyrhBORQRle10X1Dxpx+UOI4EbDQ=="; + + fn verify_message( + message_bytes: &[u8], + signature_base64: &str, + public_key_str: &str, + ) -> bool { + // Create SEP-53 payload + let mut payload = Vec::with_capacity(SEP53_PREFIX.len() + message_bytes.len()); + payload.extend_from_slice(SEP53_PREFIX.as_bytes()); + payload.extend_from_slice(message_bytes); + + // Hash with SHA-256 + let hash: [u8; 32] = Sha256::digest(&payload).into(); + + // Decode signature + let signature_bytes = BASE64.decode(signature_base64).unwrap(); + let signature = Signature::from_slice(&signature_bytes).unwrap(); + + // Decode public key + let public_key = stellar_strkey::ed25519::PublicKey::from_string(public_key_str).unwrap(); + let verifying_key = VerifyingKey::from_bytes(&public_key.0).unwrap(); + + // Verify + verifying_key.verify(&hash, &signature).is_ok() + } + + #[test] + fn test_verify_ascii_message() { + assert!(verify_message( + TEST_MESSAGE_1.as_bytes(), + TEST_SIGNATURE_1, + TEST_PUBLIC_KEY + )); + } + + #[test] + fn test_verify_utf8_message() { + assert!(verify_message( + TEST_MESSAGE_2.as_bytes(), + TEST_SIGNATURE_2, + TEST_PUBLIC_KEY + )); + } + + #[test] + fn test_verify_binary_message() { + let message_bytes = BASE64.decode(TEST_MESSAGE_3_BASE64).unwrap(); + assert!(verify_message(&message_bytes, TEST_SIGNATURE_3, TEST_PUBLIC_KEY)); + } + + #[test] + fn test_verify_wrong_signature() { + // Use signature from message 2 with message 1 + assert!(!verify_message( + TEST_MESSAGE_1.as_bytes(), + TEST_SIGNATURE_2, + TEST_PUBLIC_KEY + )); + } + + #[test] + fn test_verify_wrong_public_key() { + // Use a different public key + let wrong_key = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"; + assert!(!verify_message( + TEST_MESSAGE_1.as_bytes(), + TEST_SIGNATURE_1, + wrong_key + )); + } +} diff --git a/cmd/soroban-cli/src/commands/mod.rs b/cmd/soroban-cli/src/commands/mod.rs index 4e83f13e6..2b5244617 100644 --- a/cmd/soroban-cli/src/commands/mod.rs +++ b/cmd/soroban-cli/src/commands/mod.rs @@ -18,6 +18,7 @@ pub mod fees; pub mod global; pub mod keys; pub mod ledger; +pub mod message; pub mod network; pub mod plugin; pub mod snapshot; @@ -139,6 +140,7 @@ impl Root { Cmd::Keys(id) => id.run(&self.global_args).await?, Cmd::Tx(tx) => tx.run(&self.global_args).await?, Cmd::Ledger(ledger) => ledger.run(&self.global_args).await?, + Cmd::Message(message) => message.run(&self.global_args).await?, Cmd::Cache(cache) => cache.run()?, Cmd::Env(env) => env.run(&self.global_args)?, Cmd::Fees(env) => env.run(&self.global_args).await?, @@ -227,6 +229,10 @@ pub enum Cmd { #[command(subcommand)] Ledger(ledger::Cmd), + /// Sign and verify arbitrary messages using SEP-53 + #[command(subcommand)] + Message(message::Cmd), + /// ⚠️ Deprecated, use `fees stats` instead. Fetch network feestats FeeStats(fee_stats::Cmd), @@ -289,6 +295,9 @@ pub enum Error { #[error(transparent)] Ledger(#[from] ledger::Error), + #[error(transparent)] + Message(#[from] message::Error), + #[error(transparent)] FeeStats(#[from] fee_stats::Error), From 104ab596729f147da7bb00ba925f7961629ff40d Mon Sep 17 00:00:00 2001 From: Tomer Date: Tue, 23 Dec 2025 18:37:30 -0500 Subject: [PATCH 2/3] fix: address clippy warnings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove unused `arg` import from clap - Use `if let` instead of `match` for single pattern destructuring - Add `#[allow(clippy::unused_async)]` for async fn without await (kept async for consistency with other commands) πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cmd/soroban-cli/src/commands/message/sign.rs | 26 +++++------ .../src/commands/message/verify.rs | 44 +++++++++---------- 2 files changed, 33 insertions(+), 37 deletions(-) diff --git a/cmd/soroban-cli/src/commands/message/sign.rs b/cmd/soroban-cli/src/commands/message/sign.rs index 01cc4bfe7..d277d9db2 100644 --- a/cmd/soroban-cli/src/commands/message/sign.rs +++ b/cmd/soroban-cli/src/commands/message/sign.rs @@ -1,7 +1,7 @@ use std::io::{self, Read}; use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; -use clap::{arg, Parser}; +use clap::Parser; use ed25519_dalek::Signer as _; use sha2::{Digest, Sha256}; @@ -85,6 +85,7 @@ struct SignedMessageOutput { } impl Cmd { + #[allow(clippy::unused_async)] pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { // Get the message bytes let message_bytes = self.get_message_bytes()?; @@ -164,21 +165,20 @@ impl Cmd { } fn get_message_bytes(&self) -> Result, Error> { - let message_str = match &self.message { - Some(msg) => msg.clone(), - None => { - // Read from stdin - let mut buffer = String::new(); - io::stdin().read_to_string(&mut buffer)?; - // Remove trailing newline if present - if buffer.ends_with('\n') { + let message_str = if let Some(msg) = &self.message { + msg.clone() + } else { + // Read from stdin + let mut buffer = String::new(); + io::stdin().read_to_string(&mut buffer)?; + // Remove trailing newline if present + if buffer.ends_with('\n') { + buffer.pop(); + if buffer.ends_with('\r') { buffer.pop(); - if buffer.ends_with('\r') { - buffer.pop(); - } } - buffer } + buffer }; if self.base64 { diff --git a/cmd/soroban-cli/src/commands/message/verify.rs b/cmd/soroban-cli/src/commands/message/verify.rs index f23867750..7fe8d46ba 100644 --- a/cmd/soroban-cli/src/commands/message/verify.rs +++ b/cmd/soroban-cli/src/commands/message/verify.rs @@ -1,7 +1,7 @@ use std::io::{self, Read}; use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; -use clap::{arg, Parser}; +use clap::Parser; use ed25519_dalek::{Signature, Verifier, VerifyingKey}; use sha2::{Digest, Sha256}; @@ -85,36 +85,32 @@ impl Cmd { let verifying_key = VerifyingKey::from_bytes(&public_key_bytes)?; // Verify the signature - match verifying_key.verify(&hash, &signature) { - Ok(()) => { - let public_key = stellar_strkey::ed25519::PublicKey(public_key_bytes); - println!("Signature valid"); - println!("Signer: {public_key}"); - Ok(()) - } - Err(_) => { - eprintln!("Signature invalid"); - Err(Error::VerificationFailed) - } + if verifying_key.verify(&hash, &signature).is_ok() { + let public_key = stellar_strkey::ed25519::PublicKey(public_key_bytes); + println!("Signature valid"); + println!("Signer: {public_key}"); + Ok(()) + } else { + eprintln!("Signature invalid"); + Err(Error::VerificationFailed) } } fn get_message_bytes(&self) -> Result, Error> { - let message_str = match &self.message { - Some(msg) => msg.clone(), - None => { - // Read from stdin - let mut buffer = String::new(); - io::stdin().read_to_string(&mut buffer)?; - // Remove trailing newline if present - if buffer.ends_with('\n') { + let message_str = if let Some(msg) = &self.message { + msg.clone() + } else { + // Read from stdin + let mut buffer = String::new(); + io::stdin().read_to_string(&mut buffer)?; + // Remove trailing newline if present + if buffer.ends_with('\n') { + buffer.pop(); + if buffer.ends_with('\r') { buffer.pop(); - if buffer.ends_with('\r') { - buffer.pop(); - } } - buffer } + buffer }; if self.base64 { From c0b97bf1f3685626e1af0b531b54b3e9cde75d67 Mon Sep 17 00:00:00 2001 From: Tomer Weller Date: Tue, 23 Dec 2025 18:52:19 -0500 Subject: [PATCH 3/3] Fix cargo fmt formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cmd/soroban-cli/src/commands/message/sign.rs | 29 +++++++++++++++---- .../src/commands/message/verify.rs | 12 ++++---- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/cmd/soroban-cli/src/commands/message/sign.rs b/cmd/soroban-cli/src/commands/message/sign.rs index d277d9db2..c38bb72d5 100644 --- a/cmd/soroban-cli/src/commands/message/sign.rs +++ b/cmd/soroban-cli/src/commands/message/sign.rs @@ -66,7 +66,11 @@ pub struct Cmd { pub hd_path: Option, /// Sign with a Ledger hardware wallet - #[arg(long, conflicts_with = "sign_with_key", env = "STELLAR_SIGN_WITH_LEDGER")] + #[arg( + long, + conflicts_with = "sign_with_key", + env = "STELLAR_SIGN_WITH_LEDGER" + )] pub sign_with_ledger: bool, #[command(flatten)] @@ -242,7 +246,10 @@ mod tests { let signature = ed25519_dalek::Signature::from_slice(&signature_bytes).unwrap(); // Verify - signing_key.verifying_key().verify(&hash, &signature).is_ok() + signing_key + .verifying_key() + .verify(&hash, &signature) + .is_ok() } #[test] @@ -259,7 +266,11 @@ mod tests { // Sign and verify let message = "Hello, World!"; let signature = sign_message(message.as_bytes(), &signing_key); - assert!(verify_signature(message.as_bytes(), &signature, &signing_key)); + assert!(verify_signature( + message.as_bytes(), + &signature, + &signing_key + )); } #[test] @@ -269,7 +280,11 @@ mod tests { // Sign and verify Japanese text let message = "γ“γ‚“γ«γ‘γ―γ€δΈ–η•ŒοΌ"; let signature = sign_message(message.as_bytes(), &signing_key); - assert!(verify_signature(message.as_bytes(), &signature, &signing_key)); + assert!(verify_signature( + message.as_bytes(), + &signature, + &signing_key + )); } #[test] @@ -300,6 +315,10 @@ mod tests { let signature = sign_message(message1.as_bytes(), &signing_key); // Verify fails with different message - assert!(!verify_signature(message2.as_bytes(), &signature, &signing_key)); + assert!(!verify_signature( + message2.as_bytes(), + &signature, + &signing_key + )); } } diff --git a/cmd/soroban-cli/src/commands/message/verify.rs b/cmd/soroban-cli/src/commands/message/verify.rs index 7fe8d46ba..b75884e4a 100644 --- a/cmd/soroban-cli/src/commands/message/verify.rs +++ b/cmd/soroban-cli/src/commands/message/verify.rs @@ -157,11 +157,7 @@ mod tests { const TEST_SIGNATURE_3: &str = "VA1+7hefNwv2NKScH6n+Sljj15kLAge+M2wE7fzFOf+L0MMbssA1mwfJZRyyrhBORQRle10X1Dxpx+UOI4EbDQ=="; - fn verify_message( - message_bytes: &[u8], - signature_base64: &str, - public_key_str: &str, - ) -> bool { + fn verify_message(message_bytes: &[u8], signature_base64: &str, public_key_str: &str) -> bool { // Create SEP-53 payload let mut payload = Vec::with_capacity(SEP53_PREFIX.len() + message_bytes.len()); payload.extend_from_slice(SEP53_PREFIX.as_bytes()); @@ -203,7 +199,11 @@ mod tests { #[test] fn test_verify_binary_message() { let message_bytes = BASE64.decode(TEST_MESSAGE_3_BASE64).unwrap(); - assert!(verify_message(&message_bytes, TEST_SIGNATURE_3, TEST_PUBLIC_KEY)); + assert!(verify_message( + &message_bytes, + TEST_SIGNATURE_3, + TEST_PUBLIC_KEY + )); } #[test]