no_std-compatible Rust toolkit for multi-chain transaction signing — thirteen networks, zero hand-written cryptography, cross-implementation KATs.
Signer turns 32-byte private keys into standards-compliant signatures for Aptos, Bitcoin, Ethereum, Solana, Cosmos, Tron, Sui, TON, Filecoin, Spark, XRP Ledger, Nostr, and Casper. It layers thin wrappers around k256 (secp256k1 ECDSA and BIP-340 Schnorr) and ed25519-dalek on a capability-split trait surface (SignDigest, optional SignMessage, chain-inherent sign_transaction); every library crate builds under no_std + alloc, private keys wrap in Zeroizing / ZeroizeOnDrop and redact in Debug, and chain outputs are pinned against independent references (RFC 6979 / 8032, BIP-137 / 340, EIP-191 / 712, @noble/curves, …).
See also
kobe— the companion HD-wallet derivation toolkit that feedsSigner::from_derivedwith BIP-39 / BIP-32 / SLIP-10 accounts.
Shell (macOS / Linux):
curl -fsSL https://sh.qntx.fun/signer | shPowerShell (Windows):
irm https://sh.qntx.fun/signer/ps | iexOr via Cargo:
cargo install signer-cli# Prefer stdin / file for keys on shared hosts (shell history / process listings)
echo "$KEY" | signer evm sign-message -k - -m "Hello, Ethereum!" # EIP-191
echo "$KEY" | signer btc sign-message -k - -m "Hello, Bitcoin!" # BIP-137
echo "$KEY" | signer sui sign-tx -k - -t "0000..." # BLAKE2b intent
echo "$KEY" | signer cosmos sign-tx -k - -t "<SignDoc hex>" # ADR-036 input
echo "$KEY" | signer xrpl sign-tx -k - -t "<tx fields hex>" # STX\0 + SHA-512/2 + DER
echo "$KEY" | signer nostr sign-digest -k - -x "5e6ea04f..." # NIP-01 event id
echo "$KEY" | signer casper sign-digest -k - -x "<32-byte deploy hash>" --algo secp256k1
signer casper sign-digest -k @./key.hex -x "<digest>" --algo ed25519
echo "$KEY" | signer arweave sign-digest -k - -x "<32-byte SHA-256(deep-hash)>"
signer arweave sign-payload -k "$KEY" -d "<deep-hash hex>"
signer evm address -k @./key.hex # EIP-55 (this key only)
# Agent-friendly JSON (global flag before the chain subcommand)
signer --json evm sign-message -k - -m "test"
# Self-upgrade (sh.qntx.fun install path; same as re-running the install script)
signer upgrade # install latest if newer (`update` is an alias)
signer upgrade --check # report only
signer upgrade --force # reinstall even when up to date
signer --json upgrade --checkCLI verbs match the library: sign-digest, sign-message, sign-tx, address. Chains without a canonical personal-message scheme omit sign-message. Private keys accept hex, - (stdin), or @path. JSON sign results include a stable scheme field (ecdsa_recoverable, ed25519, schnorr, …). Cargo installs under .cargo/bin are not overwritten; the command prints a cargo install signer-cli --force hint instead.
# Default is `std` + `mainstream` (btc, evm, svm). Opt into more chains or presets explicitly.
signer = { version = "3", features = ["std", "mainstream"] }
# All chains
signer = { version = "3", features = ["std", "all-chains"] }
# Single chain + optional kobe HD bridge
signer = { version = "3", features = ["std", "evm", "kobe"] }
kobe = { version = "3.2", features = ["std", "evm"] }kobe may resolve k256 0.13 while signer uses k256 0.14; the bridge is 32-byte secrets only, not shared signing-key types. See SECURITY.md and docs/KAT_MATRIX.md.
use signer::evm::{SignDigest, SignMessage, Signer};
let signer = Signer::from_hex(
"4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318",
)?;
let raw = signer.sign_digest(&[0x42u8; 32])?; // v = 0 | 1 (raw parity → RLP)
let msg = signer.sign_message(b"hello")?; // v = 27 | 28 (EIP-191)
println!("Address: {}", signer.address()); // identity of this key only (not HD)
println!("Signature: {}", msg.to_hex());
println!("Scheme: {}", msg.scheme()); // ecdsa_recoverableSignOutput is a discriminated enum — Ecdsa { signature, v }, EcdsaDer, Ed25519, Ed25519WithPubkey, Schnorr — so callers pattern-match wire shape instead of juggling optional metadata. v semantics are documented per producer (raw parity, EIP-191, BIP-137 ranges); they collide across chains, so verifiers must already know the scheme.
Chains without a canonical off-chain message scheme do not implement SignMessage. Build the domain preimage externally and pass it to the chain’s inherent sign_transaction (not a shared trait — byte semantics differ by protocol):
// Cosmos: build ADR-036 StdSignDoc bytes externally (e.g. via kobe / app layer)
use signer::cosmos::Signer;
let signer = Signer::from_hex("4c0883a6...")?;
let signature = signer.sign_transaction(sign_doc_bytes)?;Bitcoin message signing selects the BIP-137 header for the target address type; the default matches Bitcoin Core signmessage (compressed P2PKH):
use signer::btc::{BitcoinMessageAddressType, SignMessage, Signer};
let signer = Signer::from_hex("4c0883a6...")?;
let compressed = signer.sign_message(b"Hi")?; // v = 31 | 32
let bech32 = signer.sign_message_with(BitcoinMessageAddressType::SegwitBech32, b"Hi")?;L0 signer-primitives curve engines + SignDigest / SignOutput / v_encoding
L1 signer-{chain} protocol preimage + wire (EIP-191, BIP-137, intent, …)
L2 signer-cli ops UX (sign-digest / sign-message / sign-tx)
── kobe (companion) HD only — FromDerived over 32-byte secret material
Two key materials (secp256k1 scalar, Ed25519 seed) and three schemes (ECDSA, BIP-340, Ed25519) cover all thirteen chains. Framing is product value, not noise: digests and headers are where multi-chain correctness lives.
Enable the kobe feature to construct signers from kobe accounts:
use kobe::Wallet;
use kobe::evm::Deriver;
use signer::evm::{FromDerived, Signer};
let wallet = Wallet::from_mnemonic(
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
None,
)?;
let account = Deriver::new(&wallet).derive(0)?;
let signer = Signer::from_derived(&account)?;
let sig = signer.sign_message(b"hello")?;| Chain | Crate | Curve | Sighash / digest | Off-chain message |
|---|---|---|---|---|
| Bitcoin | signer-btc |
secp256k1 | double-SHA-256 | BIP-137 (four header variants) |
| Ethereum | signer-evm |
secp256k1 | Keccak-256 | EIP-191, EIP-712 |
| Cosmos | signer-cosmos |
secp256k1 | SHA-256 | ADR-036 StdSignDoc (external) |
| Tron | signer-tron |
secp256k1 | SHA-256 (raw_data txID) |
TRON prefix, wire v = 27/28 |
| Filecoin | signer-fil |
secp256k1 | BLAKE2b-256 over CID bytes | use sign-tx / digest |
| Spark | signer-spark |
secp256k1 | double-SHA-256 | BIP-137 (compressed P2PKH) |
| TON | signer-ton |
Ed25519 | raw | caller-owned preimage |
| XRP Ledger | signer-xrpl |
secp256k1 | STX\0 + SHA-512-half, DER |
none (no canonical personal-message) |
| Solana | signer-svm |
Ed25519 | raw | raw Ed25519 (nacl.sign.detached) |
| Sui | signer-sui |
Ed25519 | BLAKE2b-256 intent + BCS | PersonalMessage intent |
| Aptos | signer-aptos |
Ed25519 | SHA3-256 domain + BCS | use sign-tx / sign_raw |
| Nostr | signer-nostr |
Schnorr BIP-340 | SHA-256 (NIP-01 event id) | raw BIP-340 (optional) |
| Casper | signer-casper |
secp256k1 / Ed25519 | deploy digest (caller BLAKE2b) | dual-curve digest / raw bytes |
| Arweave | signer-arweave |
secp256k1 ECDSA | deep-hash → SHA-256 | format=2 recoverable 65B; owner empty |
* Address helpers on chain crates are identity of this key only (not multi-path HD). Full derivation lives in kobe.
- 14 chains — Aptos, Bitcoin, Casper, Arweave (ECDSA), Ethereum, Solana, Cosmos, Tron, Sui, TON, Filecoin, Spark, XRP Ledger, Nostr
- Mature crypto dependencies —
k256for secp256k1 ECDSA and BIP-340 Schnorr,ed25519-dalekfor Ed25519; hashing viasha2/sha3/blake2/ripemd; encoding viabech32/bs58 - Capability-split traits — mandatory
SignDigest::sign_digest(&[u8; 32]→SignOutput); optionalSignMessage,ExtractSignableBytes,EncodeSignedTransaction; protocolsign_transactionis inherent per chain (no false universal trait) - Scheme-honest semantics — ECDSA digests are prehashes; Ed25519 / BIP-340 treat 32-byte inputs as messages where applicable (documented on the trait)
- Discriminated
SignOutput— wire variants +scheme()/ CLI JSONscheme;voffsets live inv_encoding(EIP-191, BIP-137, …) - Cross-implementation KATs — RFC 6979, RFC 8032, BIP-340 CSV, EIP-712 Mail, BIP-137 headers, recover/verify round-trips — strength graded in
docs/KAT_MATRIX.md no_std+alloc— library crates compile onthumbv7m-none-eabiunder CI- Security hardened —
ZeroizeOnDrop/Zeroizing, redactedDebug, CLI key ingress via stdin or@path - Kobe integration — optional
kobefeature:FromDerivedover kobe account types - Strict linting — Clippy
pedantic+nursery+correctness(deny),rust_2018_idiomsdeny, zero warnings on nightly;just check-namesbans legacy dual names
See crates/README.md for the full crate table, dependency graph, and feature flag reference.
See CONTRIBUTING.md for development setup, the chain sign contract, PR expectations, and the release checklist.
This library has not been independently audited. Use at your own risk. Full policy: SECURITY.md.
- Private keys wrapped in
zeroize— wiped on drop; secret exports returnZeroizing<…> Debugfor secret-bearing types redacts material ([REDACTED]); do not rely on{:?}for secrets- CLI prefers
-k -/-k @pathover argv on shared hosts - Random generation uses OS CSPRNG via
getrandom; prefertry_random()where entropy can fail SignDigest/SignMessagerequireSend + Syncfor async executors- No key material is logged or persisted by the workspace
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0)
- MIT License (LICENSE-MIT or https://opensource.org/licenses/MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project shall be dual-licensed as above, without any additional terms or conditions.