Offline non-interactive EVM transaction signer for agent workflows.
  • Go 97.4%
  • Makefile 1.8%
  • Dockerfile 0.8%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
dev 2f7f5868e8 Add signed release pipeline
Rename the standalone signer consistently to icu-evm-signer while keeping the
existing two-commit history. Release builds use Go 1.25.14, embed the tag
version, run Linux, macOS, and Windows sequentially, and publish one signed
checksum manifest with the public key.
2026-09-14 23:43:18 -04:00
.agents/skills/icu-evm-signer Add signed release pipeline 2026-09-14 23:43:18 -04:00
.forgejo/workflows Add signed release pipeline 2026-09-14 23:43:18 -04:00
cmd/icu-evm-signer Add signed release pipeline 2026-09-14 23:43:18 -04:00
internal Add signed release pipeline 2026-09-14 23:43:18 -04:00
.dockerignore Add signed release pipeline 2026-09-14 23:43:18 -04:00
.git-allowed-signers Add signed release pipeline 2026-09-14 23:43:18 -04:00
.gitignore Add signed release pipeline 2026-09-14 23:43:18 -04:00
Dockerfile Add signed release pipeline 2026-09-14 23:43:18 -04:00
go.mod Add signed release pipeline 2026-09-14 23:43:18 -04:00
go.sum Add signed release pipeline 2026-09-14 23:43:18 -04:00
LICENSE Initial commit 2026-08-30 00:40:17 -04:00
Makefile Add signed release pipeline 2026-09-14 23:43:18 -04:00
minisign.pub Add signed release pipeline 2026-09-14 23:43:18 -04:00
README.md Add signed release pipeline 2026-09-14 23:43:18 -04:00

ICU EVM Signer

A non-interactive, offline EVM transaction signer for agent workflows. It reads a complete unsigned transaction, an encrypted Web3 keystore, and a protected secret reference; it returns signed transaction bytes and a decoded preview. It has no RPC client and cannot broadcast.

Build

go build -o icu-evm-signer ./cmd/icu-evm-signer

Store credentials

Each keystore must have its own unique secret. Keep both files outside Git in an owner-controlled directory with mode 0700. The keystore and secret must be regular files owned by the signing user with mode 0400 or 0600.

Create a one-line secret file without putting the value in shell history:

install -d -m 0700 /secure/path/account
umask 077
IFS= read -r -s -p "Keystore secret: " signer_secret
printf '\n'
printf '%s' "$signer_secret" > /secure/path/account/secret
unset signer_secret
chmod 0600 /secure/path/account/secret

Never place the secret in an argument, environment variable, .env file, prompt, log, agent message, or RPC/MCP input. Agents may reference credential paths but must not read their contents.

For an existing protected KEY=value file, use --secret-env-file PATH --secret-env-name KEY; parsing stays inside the signer.

Verify a keystore-secret pairing without signing:

icu-evm-signer verify \
  --keystore /secure/path/account/keystore.json \
  --secret-file /secure/path/account/secret

Sign a transaction

Pass one complete JSON object through stdin or --tx-file:

{
  "type": 2,
  "chainId": 369,
  "from": "0x...",
  "nonce": 7,
  "to": "0x...",
  "valueWei": "0",
  "gas": 150000,
  "data": "0x...",
  "maxFeePerGasWei": "600000000000",
  "maxPriorityFeePerGasWei": "1000000000",
  "accessList": []
}

Types 0, 1, and 2 are supported. Wei values are unsigned decimal strings. to may be null for contract creation. The requested from must match the selected keystore.

icu-evm-signer sign \
  --keystore /secure/path/account/keystore.json \
  --secret-file /secure/path/account/secret \
  < unsigned-tx.json

An inherited descriptor keeps the secret path out of process arguments:

icu-evm-signer sign \
  --keystore /secure/path/account/keystore.json \
  --secret-fd 3 \
  3</secure/path/account/secret \
  < unsigned-tx.json

Success returns one JSON object containing rawTx, txHash, and a transaction preview. Only rawTx should cross into a gas-gated broadcast tool. Receipt status and canonical block membership must be checked before reporting execution.

Exit code 2 means invalid usage or transaction input, 3 means credential access or decryption failed, and 4 means signing or encoding failed.

Sign an EIP-191 message

sign-message signs the input bytes exactly as an Ethereum personal_sign message and returns the signer address, signature, and prefixed message hash.

icu-evm-signer sign-message \
  --keystore /secure/path/account/keystore.json \
  --secret-fd 3 \
  --message-file challenge.txt \
  3</secure/path/account/secret

Container

docker build -t icu-evm-signer .
docker run --rm --network none --read-only \
  --user "$(id -u):$(id -g)" \
  --mount type=bind,src=/secure/path/account,dst=/run/keys,readonly \
  icu-evm-signer sign \
  --keystore /run/keys/keystore.json \
  --secret-file /run/keys/secret \
  < unsigned-tx.json

Development

make check

The gate runs formatting, vet, static analysis, vulnerability scanning, unit and race tests, a native build, a container build, and a binary smoke test.

License

MIT — see LICENSE.