Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/rpc-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ jobs:
run:
gnome-keyring-daemon --components=secrets --daemonize --unlock <<< 'foobar'
- run: cargo build --features additional-libs
- run: cargo build --package stellar-signer-test-ed25519
- run: rustup target add wasm32v1-none
- run: make build-test-wasms
- run:
Expand Down
18 changes: 18 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ members = [
"cmd/crates/soroban-test/tests/fixtures/test-wasms/*",
"cmd/crates/soroban-test/tests/fixtures/hello",
"cmd/crates/soroban-test/tests/fixtures/bye",
"cmd/crates/soroban-test/tests/fixtures/signer-ed25519",
]
default-members = [
"cmd/soroban-cli",
Expand Down
3 changes: 3 additions & 0 deletions FULL_HELP_DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1321,6 +1321,9 @@ Add a new identity (keypair, ledger, OS specific secure store)
- `--ledger` — Derive the address from a connected Ledger hardware wallet at `m/44'/148'/N'`, where `N` defaults to 0 and can be set with `--hd-path`. Persists the derived public key (and `--hd-path`, when provided) so later commands work without the device
- `--overwrite` — Overwrite existing identity if it already exists. When combined with --secure-store, also replaces the existing Secure Store entry
- `--hd-path <HD_PATH>` — When importing a seed phrase, which `hd_path` to derive the key at. Persisted on the identity so later commands derive the same account without re-passing the flag. Not valid with `--public-key` or a raw secret key
- `--smart-account <SMART_ACCOUNT>` — Add a smart account (contract address, C...). Its auth entries are signed by the `stellar-signer-<NAME>` plugin named with `--signer-plugin`
- `--signer-plugin <NAME>` — Name of the signer plugin for `--smart-account`, run as `stellar-signer-<NAME>` from your PATH
- `--plugin-arg <KEY=VALUE>` — Argument for the signer plugin, as KEY=VALUE. Can be repeated

## `stellar keys public-key`

Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ install:
build-fixtures:
cargo install --force --locked --path ./cmd/crates/soroban-test/tests/fixtures/hello --root ./target --debug --quiet
cargo install --force --locked --path ./cmd/crates/soroban-test/tests/fixtures/bye --root ./target --debug --quiet
cargo install --force --locked --path ./cmd/crates/soroban-test/tests/fixtures/signer-ed25519 --root ./target --debug --quiet

# regenerate the example lib in `cmd/crates/soroban-spec-typsecript/fixtures/ts`
build-snapshot: typescript-bindings-fixtures
Expand Down
14 changes: 14 additions & 0 deletions cmd/crates/soroban-test/tests/fixtures/signer-ed25519/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "stellar-signer-test-ed25519"
version = "30.0.0"
edition = "2021"
publish = false

# Test signer plugin for the `test_ed25519_account` smart account.

[dependencies]
base64 = { workspace = true }
ed25519-dalek = { workspace = true }
hex = { workspace = true }
serde_json = { workspace = true }
stellar-strkey = { workspace = true }
40 changes: 40 additions & 0 deletions cmd/crates/soroban-test/tests/fixtures/signer-ed25519/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//! Signer plugin for the `test_ed25519_account` smart account: signs the
//! request's `payload` with the identity named by `args.key`, and returns the
//! signature as an `ScVal::Bytes`.

use std::io::Read;
use std::process::Command;

use base64::Engine as _;
use ed25519_dalek::{Signer as _, SigningKey};

const SCV_BYTES: u32 = 13;

fn main() {
let mut input = String::new();
std::io::stdin().read_to_string(&mut input).unwrap();
let request: serde_json::Value = serde_json::from_str(&input).unwrap();
assert_eq!(request["mode"], "sign_auth");

let key = request["args"]["key"]
.as_str()
.expect("args.key is required");
let stellar = std::env::var("STELLAR_BIN").expect("STELLAR_BIN is set");
let output = Command::new(stellar)
.args(["keys", "secret", key])
.output()
.unwrap();
assert!(output.status.success(), "stellar keys secret {key} failed");
let secret = String::from_utf8(output.stdout).unwrap();
let secret = stellar_strkey::ed25519::PrivateKey::from_string(secret.trim()).unwrap();

let payload = hex::decode(request["payload"].as_str().unwrap()).unwrap();
let signature = SigningKey::from_bytes(&secret.0).sign(&payload).to_bytes();

// XDR for ScVal::Bytes(signature)
let mut xdr = Vec::with_capacity(72);
xdr.extend_from_slice(&SCV_BYTES.to_be_bytes());
xdr.extend_from_slice(&u32::try_from(signature.len()).unwrap().to_be_bytes());
xdr.extend_from_slice(&signature);
println!("{}", base64::engine::general_purpose::STANDARD.encode(xdr));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "test_ed25519_account"
version = "30.0.0"
authors = ["Stellar Development Foundation <info@stellar.org>"]
license = "Apache-2.0"
edition = "2021"
publish = false

[lib]
crate-type = ["cdylib"]
doctest = false

[dependencies]
soroban-sdk = { workspace = true }

[dev-dependencies]
soroban-sdk = { workspace = true, features = ["testutils"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#![no_std]
//! A smart account with one ed25519 signer, for testing signer plugins.
use soroban_sdk::{
auth::{Context, CustomAccountInterface},
contract, contracterror, contractimpl,
crypto::Hash,
symbol_short, BytesN, Env, Symbol, Vec,
};

const SIGNER: Symbol = symbol_short!("signer");

#[contract]
pub struct Contract;

#[contracterror]
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum Error {
NoSigner = 1,
}

#[contractimpl]
impl Contract {
pub fn __constructor(env: Env, signer: BytesN<32>) {
env.storage().instance().set(&SIGNER, &signer);
}
}

#[contractimpl]
impl CustomAccountInterface for Contract {
type Error = Error;
type Signature = BytesN<64>;

#[allow(non_snake_case)]
fn __check_auth(
env: Env,
signature_payload: Hash<32>,
signature: BytesN<64>,
_auth_contexts: Vec<Context>,
) -> Result<(), Error> {
let signer: BytesN<32> = env
.storage()
.instance()
.get(&SIGNER)
.ok_or(Error::NoSigner)?;
env.crypto()
.ed25519_verify(&signer, &signature_payload.into(), &signature);
Ok(())
}
}
1 change: 1 addition & 0 deletions cmd/crates/soroban-test/tests/it/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ mod ledger;
mod network;
mod pull;
mod secure_store;
mod smart_account;
mod snapshot;
mod token;
mod tx;
Expand Down
109 changes: 109 additions & 0 deletions cmd/crates/soroban-test/tests/it/integration/smart_account.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
use std::ffi::OsString;

use soroban_test::{AssertExt, TestEnv};

use super::util::{extend_contract, new_account, AUTH, ED25519_ACCOUNT};

/// PATH with the directory `stellar` was built into, where `cargo build
/// --package stellar-signer-test-ed25519` puts the test signer plugin.
fn path_with_test_plugin() -> OsString {
let stellar = assert_cmd::cargo::cargo_bin("stellar");
let dir = stellar.parent().unwrap().to_path_buf();
let paths = std::env::var_os("PATH").unwrap_or_default();
std::env::join_paths(std::iter::once(dir).chain(std::env::split_paths(&paths))).unwrap()
}

/// Deploy a `test_ed25519_account` signed for by identity `signer`, and save
/// it as identity `name` using `plugin`.
async fn add_smart_account(sandbox: &TestEnv, name: &str, signer: &str, plugin: &str) -> String {
let signer_pk = new_account(sandbox, signer);
let signer_pk = stellar_strkey::ed25519::PublicKey::from_string(&signer_pk).unwrap();
let account = sandbox
.new_assert_cmd("contract")
.arg("deploy")
.arg("--source=test")
.arg("--wasm")
.arg(ED25519_ACCOUNT.path())
.arg("--")
.arg("--signer")
.arg(hex::encode(signer_pk.0))
.assert()
.success()
.stdout_as_str();
extend_contract(sandbox, &account).await;
sandbox
.new_assert_cmd("keys")
.args(["add", name, "--smart-account", &account])
.args(["--signer-plugin", plugin, "--plugin-arg"])
.arg(format!("key={signer}"))
.assert()
.success();
account
}

async fn deploy_auth(sandbox: &TestEnv) -> String {
let id = sandbox
.new_assert_cmd("contract")
.arg("deploy")
.arg("--source=test")
.arg("--wasm")
.arg(AUTH.path())
.arg("--")
.arg("--addr=test")
.assert()
.success()
.stdout_as_str();
extend_contract(sandbox, &id).await;
id
}

#[tokio::test]
async fn smart_account_signs_with_plugin() {
let sandbox = &TestEnv::new();
let account = add_smart_account(sandbox, "sa", "sa-signer", "test-ed25519").await;
sandbox
.new_assert_cmd("keys")
.args(["address", "sa"])
.assert()
.success()
.stdout(format!("{account}\n"));

let id = deploy_auth(sandbox).await;
sandbox
.new_assert_cmd("contract")
.env("PATH", path_with_test_plugin())
.arg("invoke")
.arg("--source=test")
.arg("--id")
.arg(&id)
.arg("--")
.arg("do-auth")
.arg("--addr=sa")
.arg("--val=hello")
.assert()
.success()
.stdout("\"hello\"\n");
}

#[tokio::test]
async fn smart_account_plugin_not_installed() {
let sandbox = &TestEnv::new();
add_smart_account(sandbox, "sa", "sa-signer", "not-installed").await;

let id = deploy_auth(sandbox).await;
sandbox
.new_assert_cmd("contract")
.arg("invoke")
.arg("--source=test")
.arg("--id")
.arg(&id)
.arg("--")
.arg("do-auth")
.arg("--addr=sa")
.arg("--val=hello")
.assert()
.failure()
.stderr(predicates::str::contains(
"signer plugin `stellar-signer-not-installed` not found on PATH",
));
}
1 change: 1 addition & 0 deletions cmd/crates/soroban-test/tests/it/integration/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub const HELLO_WORLD: &Wasm = &Wasm::Custom("test-wasms", "test_hello_world");
pub const CONSTRUCTOR: &Wasm = &Wasm::Custom("test-wasms", "test_constructor");
pub const CUSTOM_TYPES: &Wasm = &Wasm::Custom("test-wasms", "test_custom_types");
pub const CUSTOM_ACCOUNT: &Wasm = &Wasm::Custom("test-wasms", "test_custom_account");
pub const ED25519_ACCOUNT: &Wasm = &Wasm::Custom("test-wasms", "test_ed25519_account");
pub const SWAP: &Wasm = &Wasm::Custom("test-wasms", "test_swap");

pub async fn invoke(sandbox: &TestEnv, id: &str, func: &str, data: &str) -> String {
Expand Down
13 changes: 12 additions & 1 deletion cmd/soroban-cli/src/commands/contract/arg_parsing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::commands::contract::deploy::wasm::CONSTRUCTOR_FUNCTION_NAME;
use crate::commands::txn_result::TxnResult;
use crate::config::{self, sc_address, UnresolvedScAddress};
use crate::print::Print;
use crate::signer::{self, Signer};
use crate::signer::{self, plugin::PluginSigner, Signer, SignerKind};
use crate::xdr::{
self, Hash, InvokeContractArgs, ScSpecEntry, ScSpecFunctionV0, ScSpecTypeDef, ScVal, ScVec,
};
Expand Down Expand Up @@ -584,6 +584,17 @@ fn resolve_address(
}

fn resolve_signer(addr_or_alias: &str, config: &config::Args) -> Option<Signer> {
if let Ok(config::key::Key::SmartAccount(account)) = config.locator.read_identity(addr_or_alias)
{
return Some(Signer {
kind: SignerKind::Plugin(PluginSigner {
address: account.address.0,
plugin: account.signer_plugin,
args: account.plugin_args,
}),
print: Print::new(false),
});
}
let account: config::UnresolvedMuxedAccount = addr_or_alias.parse().ok()?;
// A raw public key is matched to an identity at `--hd-path`, so the signer
// is built at that same path; an alias or secret honors it the same way.
Expand Down
Loading