Skip to content
63 changes: 63 additions & 0 deletions FULL_HELP_DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ Anything after the `--` double dash (the "slop") is parsed as arguments to the c
- `version` — Print version information
- `plugin` — The subcommand for CLI plugins
- `ledger` — Fetch ledger information
- `message` — Sign and verify arbitrary messages using SEP-53
- `fee-stats` — ⚠️ Deprecated, use `fees stats` instead. Fetch network feestats
- `fees` — Fetch network feestats and configure CLI fee settings

Expand Down Expand Up @@ -4482,6 +4483,68 @@ Get the latest ledger sequence and information from the network
- `--network-passphrase <NETWORK_PASSPHRASE>` — Network passphrase to sign the transaction sent to the rpc server
- `-n`, `--network <NETWORK>` — Name of network to use from config

## `stellar message`

Sign and verify arbitrary messages using SEP-53

**Usage:** `stellar message <COMMAND>`

###### **Subcommands:**

- `sign` — Sign an arbitrary message using SEP-53
- `verify` — Verify a SEP-53 signed message

## `stellar message sign`

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

**Usage:** `stellar message sign [OPTIONS] --sign-with-key <SIGN_WITH_KEY> [MESSAGE]`

###### **Arguments:**

- `<MESSAGE>` — 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

###### **Options:**

- `--base64` — Treat the message as base64-encoded binary data
- `--sign-with-key <SIGN_WITH_KEY>` — 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
- `--hd-path <HD_PATH>` — 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`

###### **Options (Global):**

- `--global` — ⚠️ Deprecated: global config is always on
- `--config-dir <CONFIG_DIR>` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings

## `stellar message verify`

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 <BASE64_SIG> --public-key GABC...

**Usage:** `stellar message verify [OPTIONS] --signature <SIGNATURE> --public-key <PUBLIC_KEY> [MESSAGE]`

###### **Arguments:**

- `<MESSAGE>` — 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

###### **Options:**

- `--base64` — Treat the message as base64-encoded binary data
- `-s`, `--signature <SIGNATURE>` — The base64-encoded signature to verify
- `-p`, `--public-key <PUBLIC_KEY>` — The public key to verify the signature against. Can be an identity (--public-key alice), a public key (--public-key GDKW...)
- `--hd-path <HD_PATH>` — If public key identity is a seed phrase use this hd path, default is 0

###### **Options (Global):**

- `--global` — ⚠️ Deprecated: global config is always on
- `--config-dir <CONFIG_DIR>` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings

## `stellar fee-stats`

⚠️ Deprecated, use `fees stats` instead. Fetch network feestats
Expand Down
1 change: 1 addition & 0 deletions cmd/crates/soroban-test/tests/it/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod init;
#[cfg(feature = "it")]
mod integration;
mod log;
mod message;
mod plugin;
mod rpc_provider;
mod strkey;
Expand Down
177 changes: 177 additions & 0 deletions cmd/crates/soroban-test/tests/it/message.rs
Original file line number Diff line number Diff line change
@@ -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();
}
48 changes: 48 additions & 0 deletions cmd/soroban-cli/src/commands/message/mod.rs
Original file line number Diff line number Diff line change
@@ -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 <BASE64_SIG> --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(global_args)?,
}
Ok(())
}
}
Loading
Loading