diff --git a/cmd/crates/soroban-test/tests/it/main.rs b/cmd/crates/soroban-test/tests/it/main.rs index 6cbf1a60d7..e96558af5e 100644 --- a/cmd/crates/soroban-test/tests/it/main.rs +++ b/cmd/crates/soroban-test/tests/it/main.rs @@ -7,6 +7,7 @@ mod init; #[cfg(feature = "it")] mod integration; mod log; +mod message; mod plugin; mod rpc_provider; mod strkey; diff --git a/cmd/crates/soroban-test/tests/it/message.rs b/cmd/crates/soroban-test/tests/it/message.rs new file mode 100644 index 0000000000..9d26f3fdff --- /dev/null +++ b/cmd/crates/soroban-test/tests/it/message.rs @@ -0,0 +1,177 @@ +use soroban_test::{AssertExt, TestEnv}; + +#[tokio::test] +async fn sep_53_sign_message_and_verify() { + let sandbox = &TestEnv::new(); + + let message = "Hello, World!"; + let expected_signature = + "fO5dbYhXUhBMhe6kId/cuVq/AfEnHRHEvsP8vXh03M1uLpi5e46yO2Q8rEBzu3feXQewcQE5GArp88u6ePK6BA=="; + let wrong_signature = + "CDU265Xs8y3OWbB/56H9jPgUss5G9A0qFuTqH2zs2YDgTm+++dIfmAEceFqB7bhfN3am59lCtDXrCtwH2k1GBA=="; + let secret_key = "SAKICEVQLYWGSOJS4WW7HZJWAHZVEEBS527LHK5V4MLJALYKICQCJXMW"; + let public_key = "GBXFXNDLV4LSWA4VB7YIL5GBD7BVNR22SGBTDKMO2SBZZHDXSKZYCP7L"; + let wrong_public_key = "GAREAZZQWHOCBJS236KIE3AWYBVFLSBK7E5UW3ICI3TCRWQKT5LNLCEZ"; + + let output = sandbox + .new_assert_cmd("message") + .args(["sign", message, "--sign-with-key", secret_key]) + .assert() + .success() + .stdout_as_str(); + assert_eq!(output.trim(), expected_signature); + + sandbox + .new_assert_cmd("message") + .args([ + "verify", + message, + "--signature", + expected_signature, + "--public-key", + public_key, + ]) + .assert() + .success(); + + // wrong signature + sandbox + .new_assert_cmd("message") + .args([ + "verify", + message, + "--signature", + wrong_signature, + "--public-key", + public_key, + ]) + .assert() + .failure(); + + // wrong public key + sandbox + .new_assert_cmd("message") + .args([ + "verify", + message, + "--signature", + expected_signature, + "--public-key", + wrong_public_key, + ]) + .assert() + .failure(); +} + +#[tokio::test] +async fn sep_53_sign_message_and_verify_stdin() { + let sandbox = &TestEnv::new(); + + let message = "Hello, World!"; + let expected_signature = + "fO5dbYhXUhBMhe6kId/cuVq/AfEnHRHEvsP8vXh03M1uLpi5e46yO2Q8rEBzu3feXQewcQE5GArp88u6ePK6BA=="; + let secret_key = "SAKICEVQLYWGSOJS4WW7HZJWAHZVEEBS527LHK5V4MLJALYKICQCJXMW"; + let public_key = "GBXFXNDLV4LSWA4VB7YIL5GBD7BVNR22SGBTDKMO2SBZZHDXSKZYCP7L"; + + // sandbox + // .new_assert_cmd("keys") + // .args(["add", alias_secret, "--secret-key", secret_key]) + // .assert() + // .success(); + // sandbox + // .new_assert_cmd("keys") + // .args(["add", alias_public, "--public-key", public_key]) + // .assert() + // .success(); + + let output = sandbox + .new_assert_cmd("message") + .write_stdin(message) + .args(["sign", "--sign-with-key", secret_key]) + .assert() + .success() + .stdout_as_str(); + assert_eq!(output.trim(), expected_signature); + + sandbox + .new_assert_cmd("message") + .write_stdin(message) + .args([ + "verify", + "--signature", + expected_signature, + "--public-key", + public_key, + ]) + .assert() + .success(); +} + +#[tokio::test] +async fn sep_53_sign_message_and_verify_with_alias() { + let sandbox = &TestEnv::new(); + + let message = "Hello, World!"; + let expected_signature = + "fO5dbYhXUhBMhe6kId/cuVq/AfEnHRHEvsP8vXh03M1uLpi5e46yO2Q8rEBzu3feXQewcQE5GArp88u6ePK6BA=="; + let public_key = "GBXFXNDLV4LSWA4VB7YIL5GBD7BVNR22SGBTDKMO2SBZZHDXSKZYCP7L"; + + // generate a new secret "alice" and a public alias "bob" of the example pubkey + sandbox + .new_assert_cmd("keys") + .args(["generate", "alice"]) + .assert() + .success(); + sandbox + .new_assert_cmd("keys") + .args(["add", "bob", "--public-key", public_key]) + .assert() + .success(); + + // since this is randomly generated, just validate the output matches for alice + let alice_signature = sandbox + .new_assert_cmd("message") + .write_stdin(message) + .args(["sign", "--sign-with-key", "alice"]) + .assert() + .success() + .stdout_as_str(); + sandbox + .new_assert_cmd("message") + .write_stdin(message) + .args([ + "verify", + "--signature", + &alice_signature, + "--public-key", + "alice", + ]) + .assert() + .success(); + + // validate a public key alias works for validation + sandbox + .new_assert_cmd("message") + .write_stdin(message) + .args([ + "verify", + "--signature", + expected_signature, + "--public-key", + "bob", + ]) + .assert() + .success(); + sandbox + .new_assert_cmd("message") + .write_stdin(message) + .args([ + "verify", + "--signature", + &alice_signature, + "--public-key", + "bob", + ]) + .assert() + .failure(); +} 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 0000000000..033d98e035 --- /dev/null +++ b/cmd/soroban-cli/src/commands/message/mod.rs @@ -0,0 +1,48 @@ +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 provided message will get 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 account public key, following the SEP-53 specification. The + /// provided message will get prefixed with "Stellar Signed Message:\n" before verification. + /// + /// Example: stellar message verify "Hello, World!" --signature --account 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(global_args)?, + } + 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 0000000000..52156cbd48 --- /dev/null +++ b/cmd/soroban-cli/src/commands/message/sign.rs @@ -0,0 +1,233 @@ +use std::io::{self, Read}; + +use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; +use clap::Parser; +use sha2::{Digest, Sha256}; + +use crate::{ + commands::global, + config::{locator, secret}, + print::Print, + signer::{self, Signer}, +}; + +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. This should **not** include + /// the SEP-53 prefix "Stellar Signed Message:\n", as it will be added automatically. + #[arg()] + pub message: Option, + + /// Treat the message as base64-encoded binary data + #[arg(long)] + pub base64: bool, + + // @dev: Ledger and Lab don't support signing arbitrary messages yet. Once they do, use `sign_with::Args` here. + /// Sign with a local key or key saved in OS secure storage. Can be an identity (--sign-with-key alice), a secret key (--sign-with-key SC36…), or a seed phrase (--sign-with-key "kite urban…"). If using seed phrase, `--hd-path` defaults to the `0` path. + #[arg(long, env = "STELLAR_SIGN_WITH_KEY")] + pub sign_with_key: String, + + #[arg(long)] + /// 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` + pub hd_path: Option, + + #[command(flatten)] + pub locator: locator::Args, +} + +impl Cmd { + pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> { + let print = Print::new(global_args.quiet); + + // Get the message bytes + let message_bytes = self.get_message_bytes()?; + + // Get the signer + let key_or_name = &self.sign_with_key; + let secret = self.locator.get_secret_key(key_or_name)?; + let signer = secret.signer(self.hd_path, print.clone()).await?; + let public_key = signer.get_public_key()?; + + // Encode signature as base64 + let signature_base64 = sep_53_sign(&message_bytes, signer)?; + + print.infoln(format!("Signer: {public_key}")); + let message_display = if self.base64 { + BASE64.encode(&message_bytes) + } else { + String::from_utf8_lossy(&message_bytes).to_string() + }; + print.infoln(format!("Message: {message_display}")); + print.println_stdout(signature_base64); + Ok(()) + } + + fn get_message_bytes(&self) -> Result, Error> { + 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(); + } + } + buffer + }; + + if self.base64 { + // Decode base64 input + Ok(BASE64.decode(&message_str)?) + } else { + // Use UTF-8 encoded message + Ok(message_str.into_bytes()) + } + } +} + +/// Sign the given message bytes with the provided signer, returning the base64-encoded signature. +/// +/// Expects the message bytes to be the raw message (without SEP-53 prefix). +fn sep_53_sign(message_bytes: &[u8], signer: Signer) -> Result { + // 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); + let hash: [u8; 32] = Sha256::digest(&payload).into(); + + let signature = signer.sign_payload(hash)?; + + Ok(BASE64.encode(signature.to_bytes())) +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::*; + use crate::{config::secret::Secret, utils::into_signing_key}; + + // Public key = GBXFXNDLV4LSWA4VB7YIL5GBD7BVNR22SGBTDKMO2SBZZHDXSKZYCP7L + const TEST_SECRET_KEY: &str = "SAKICEVQLYWGSOJS4WW7HZJWAHZVEEBS527LHK5V4MLJALYKICQCJXMW"; + + fn setup_locator() -> locator::Args { + let temp_dir = tempfile::tempdir().unwrap(); + locator::Args { + global: false, + config_dir: Some(temp_dir.path().to_path_buf()), + } + } + + fn build_signer_for_test_key() -> Signer { + let secret = Secret::from_str(TEST_SECRET_KEY).unwrap(); + let private_key = secret.private_key(None).unwrap(); + let signing_key = into_signing_key(&private_key); + Signer { + kind: signer::SignerKind::Local(signer::LocalKey { key: signing_key }), + print: Print::new(true), + } + } + + #[test] + fn test_sign_simple() { + // SEP-53 - test case 1 + let message = "Hello, World!".to_string(); + let expected_signature = "fO5dbYhXUhBMhe6kId/cuVq/AfEnHRHEvsP8vXh03M1uLpi5e46yO2Q8rEBzu3feXQewcQE5GArp88u6ePK6BA=="; + + let locator = setup_locator(); + let cmd = super::Cmd { + message: Some(message), + base64: false, + sign_with_key: TEST_SECRET_KEY.to_string(), + hd_path: None, + locator: locator.clone(), + }; + let signer = build_signer_for_test_key(); + + let message_bytes = cmd.get_message_bytes().unwrap(); + let signature_base64 = sep_53_sign(&message_bytes, signer).unwrap(); + + assert_eq!(signature_base64, expected_signature); + } + + #[test] + fn test_sign_japanese() { + // SEP-53 - test case 2 + let message = "こんにちは、世界!".to_string(); + let expected_signature = "CDU265Xs8y3OWbB/56H9jPgUss5G9A0qFuTqH2zs2YDgTm+++dIfmAEceFqB7bhfN3am59lCtDXrCtwH2k1GBA=="; + + let locator = setup_locator(); + let cmd = super::Cmd { + message: Some(message), + base64: false, + sign_with_key: TEST_SECRET_KEY.to_string(), + hd_path: None, + locator: locator.clone(), + }; + let signer = build_signer_for_test_key(); + + let message_bytes = cmd.get_message_bytes().unwrap(); + let signature_base64 = sep_53_sign(&message_bytes, signer).unwrap(); + + assert_eq!(signature_base64, expected_signature); + } + + #[test] + fn test_sign_base64() { + // SEP-53 - test case 3 + let message = "2zZDP1sa1BVBfLP7TeeMk3sUbaxAkUhBhDiNdrksaFo=".to_string(); + let expected_signature = "VA1+7hefNwv2NKScH6n+Sljj15kLAge+M2wE7fzFOf+L0MMbssA1mwfJZRyyrhBORQRle10X1Dxpx+UOI4EbDQ=="; + + let locator = setup_locator(); + let cmd = super::Cmd { + message: Some(message), + base64: true, + sign_with_key: TEST_SECRET_KEY.to_string(), + hd_path: None, + locator: locator.clone(), + }; + let signer = build_signer_for_test_key(); + + let message_bytes = cmd.get_message_bytes().unwrap(); + let signature_base64 = sep_53_sign(&message_bytes, signer).unwrap(); + + assert_eq!(signature_base64, expected_signature); + } +} 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 0000000000..beb44d0dbf --- /dev/null +++ b/cmd/soroban-cli/src/commands/message/verify.rs @@ -0,0 +1,276 @@ +use std::io::{self, Read}; + +use crate::{ + commands::global, + config::{locator, secret}, + print::Print, +}; +use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; +use clap::Parser; +use ed25519_dalek::{Signature, Verifier, VerifyingKey}; +use sha2::{Digest, Sha256}; + +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(transparent)] + Address(#[from] crate::config::address::Error), + + #[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. This should **not** include + /// the SEP-53 prefix "Stellar Signed Message:\n", as it will be added automatically. + #[arg()] + pub message: Option, + + /// Treat the message as base64-encoded binary data + #[arg(long)] + pub base64: bool, + + /// The base64-encoded signature to verify + #[arg(long, short = 's')] + pub signature: String, + + /// The public key to verify the signature against. Can be an identity (--public_key alice), + /// a public key (--public_key GDKW...). + #[arg(long, short = 'p')] + pub public_key: String, + + /// If public key identity is a seed phrase use this hd path, default is 0 + #[arg(long)] + pub hd_path: Option, + + #[command(flatten)] + pub locator: locator::Args, +} + +impl Cmd { + pub fn run(&self, global_args: &global::Args) -> Result<(), Error> { + let print = Print::new(global_args.quiet); + + // Create the SEP-53 payload: prefix + message as utf-8 byte array + let message_bytes = self.get_message_bytes()?; + 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 verifying key + let public_key = self.get_public_key()?; + print.infoln(format!("Verifying signature against: {public_key}")); + let verifying_key = VerifyingKey::from_bytes(&public_key.0)?; + + // Verify the signature + if verifying_key.verify(&hash, &signature).is_ok() { + print.checkln("Signature valid"); + Ok(()) + } else { + print.errorln("Signature invalid"); + Err(Error::VerificationFailed) + } + } + + fn get_message_bytes(&self) -> Result, Error> { + 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(); + } + } + 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(&self) -> Result { + // try to parse as stellar public key first + if let Ok(pk) = stellar_strkey::ed25519::PublicKey::from_string(&self.public_key) { + return Ok(pk); + } + + // otherwise treat as identity and resolve + let account = self + .locator + .read_key(&self.public_key)? + .muxed_account(self.hd_path) + .map_err(crate::config::address::Error::from)?; + let bytes = match account { + soroban_sdk::xdr::MuxedAccount::Ed25519(uint256) => uint256.0, + soroban_sdk::xdr::MuxedAccount::MuxedEd25519(muxed_account) => muxed_account.ed25519.0, + }; + Ok(stellar_strkey::ed25519::PublicKey(bytes)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // Public key = GBXFXNDLV4LSWA4VB7YIL5GBD7BVNR22SGBTDKMO2SBZZHDXSKZYCP7L + const TEST_PUBLIC_KEY: &str = "GBXFXNDLV4LSWA4VB7YIL5GBD7BVNR22SGBTDKMO2SBZZHDXSKZYCP7L"; + const FALSE_PUBLIC_KEY: &str = "GAREAZZQWHOCBJS236KIE3AWYBVFLSBK7E5UW3ICI3TCRWQKT5LNLCEZ"; + const FALSE_SIGNATURE: &str = + "+F//cUINZgTe4vZNXOEJTchDgEYlvy+iGFH3P65KeVhoyZgAsmGRRYAQLVqgY9J3PAlHPbSSeU5advhswmAfDg=="; + + fn setup_locator() -> locator::Args { + let temp_dir = tempfile::tempdir().unwrap(); + locator::Args { + global: false, + config_dir: Some(temp_dir.path().to_path_buf()), + } + } + + fn global_args() -> global::Args { + global::Args { + quiet: true, + ..Default::default() + } + } + + #[test] + fn test_verify_simple() { + // SEP-53 - test case 1 + let message = "Hello, World!".to_string(); + let signature = "fO5dbYhXUhBMhe6kId/cuVq/AfEnHRHEvsP8vXh03M1uLpi5e46yO2Q8rEBzu3feXQewcQE5GArp88u6ePK6BA=="; + + let global = global_args(); + let locator = setup_locator(); + let cmd = super::Cmd { + message: Some(message), + base64: false, + signature: signature.to_string(), + public_key: TEST_PUBLIC_KEY.to_string(), + hd_path: None, + locator: locator.clone(), + }; + let successful = cmd.run(&global); + assert!(successful.is_ok()); + } + + #[test] + fn test_verify_japanese() { + // SEP-53 - test case 2 + let message = "こんにちは、世界!".to_string(); + let signature = "CDU265Xs8y3OWbB/56H9jPgUss5G9A0qFuTqH2zs2YDgTm+++dIfmAEceFqB7bhfN3am59lCtDXrCtwH2k1GBA=="; + + let global = global_args(); + let locator = setup_locator(); + let cmd = super::Cmd { + message: Some(message), + base64: false, + signature: signature.to_string(), + public_key: TEST_PUBLIC_KEY.to_string(), + hd_path: None, + locator: locator.clone(), + }; + let successful = cmd.run(&global); + assert!(successful.is_ok()); + } + + #[test] + fn test_verify_base64() { + // SEP-53 - test case 3 + let message = "2zZDP1sa1BVBfLP7TeeMk3sUbaxAkUhBhDiNdrksaFo=".to_string(); + let signature = "VA1+7hefNwv2NKScH6n+Sljj15kLAge+M2wE7fzFOf+L0MMbssA1mwfJZRyyrhBORQRle10X1Dxpx+UOI4EbDQ=="; + + let global = global_args(); + let locator = setup_locator(); + let cmd = super::Cmd { + message: Some(message), + base64: true, + signature: signature.to_string(), + public_key: TEST_PUBLIC_KEY.to_string(), + hd_path: None, + locator: locator.clone(), + }; + let successful = cmd.run(&global); + assert!(successful.is_ok()); + } + + #[test] + fn test_verify_bad_signature_errors() { + let message = "Hello, World!".to_string(); + + let global = global_args(); + let locator = setup_locator(); + let cmd = super::Cmd { + message: Some(message), + base64: false, + signature: FALSE_SIGNATURE.to_string(), + public_key: TEST_PUBLIC_KEY.to_string(), + hd_path: None, + locator: locator.clone(), + }; + let successful = cmd.run(&global); + assert!(successful.is_err()); + } + + #[test] + fn test_verify_bad_pubkey_errors() { + let message = "Hello, World!".to_string(); + let signature = "fO5dbYhXUhBMhe6kId/cuVq/AfEnHRHEvsP8vXh03M1uLpi5e46yO2Q8rEBzu3feXQewcQE5GArp88u6ePK6BA=="; + + let global = global_args(); + let locator = setup_locator(); + let cmd = super::Cmd { + message: Some(message), + base64: false, + signature: signature.to_string(), + public_key: FALSE_PUBLIC_KEY.to_string(), + hd_path: None, + locator: locator.clone(), + }; + let successful = cmd.run(&global); + assert!(successful.is_err()); + } +} diff --git a/cmd/soroban-cli/src/commands/mod.rs b/cmd/soroban-cli/src/commands/mod.rs index 4e83f13e60..2b52446170 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), diff --git a/cmd/soroban-cli/src/print.rs b/cmd/soroban-cli/src/print.rs index e8c52adffc..412b9fd626 100644 --- a/cmd/soroban-cli/src/print.rs +++ b/cmd/soroban-cli/src/print.rs @@ -17,18 +17,27 @@ impl Print { Print { quiet } } + /// Print message to stderr if not in quiet mode pub fn print(&self, message: T) { if !self.quiet { eprint!("{message}"); } } + /// Print message with newline to stderr if not in quiet mode. pub fn println(&self, message: T) { if !self.quiet { eprintln!("{message}"); } } + /// Print message with newline to stdout, regardless of quiet mode. + /// + /// Use for output data that may be piped or captured by other programs. + pub fn println_stdout(&self, message: T) { + println!("{message}"); + } + pub fn clear_previous_line(&self) { if !self.quiet { if cfg!(windows) { diff --git a/cmd/soroban-cli/src/signer/mod.rs b/cmd/soroban-cli/src/signer/mod.rs index c699b30b33..a55c3a73a5 100644 --- a/cmd/soroban-cli/src/signer/mod.rs +++ b/cmd/soroban-cli/src/signer/mod.rs @@ -118,12 +118,12 @@ pub fn sign_soroban_authorizations( let mut signer: Option<&Signer> = None; for s in signers { - if needle == &s.get_public_key()? { + if needle == &s.get_public_key()?.0 { signer = Some(s); } } - if needle == &source_signer.get_public_key()? { + if needle == &source_signer.get_public_key()?.0 { signer = Some(source_signer); } @@ -181,7 +181,7 @@ fn sign_soroban_authorization_entry( let payload = Sha256::digest(preimage); let p: [u8; 32] = payload.as_slice().try_into()?; let signature = signer.sign_payload(p)?; - let public_key_vec = signer.get_public_key()?.to_vec(); + let public_key_vec = signer.get_public_key()?.0.to_vec(); let map = ScMap::sorted_from(vec![ ( @@ -263,15 +263,14 @@ impl Signer { } // when we implement this for ledger we'll need it to be async so we can await for the ledger's public key - pub fn get_public_key(&self) -> Result<[u8; 32], Error> { + pub fn get_public_key(&self) -> Result { match &self.kind { - SignerKind::Local(local_key) => Ok(*local_key.key.verifying_key().as_bytes()), + SignerKind::Local(local_key) => Ok(stellar_strkey::ed25519::PublicKey::from_payload( + local_key.key.verifying_key().as_bytes(), + )?), SignerKind::Ledger(_ledger) => todo!("ledger device is not implemented"), SignerKind::Lab => Err(Error::ReturningSignatureFromLab), - SignerKind::SecureStore(secure_store_entry) => { - let pk = secure_store_entry.get_public_key()?; - Ok(pk.0) - } + SignerKind::SecureStore(secure_store_entry) => secure_store_entry.get_public_key(), } }