- Go 97.4%
- Makefile 1.8%
- Dockerfile 0.8%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
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. |
||
| .agents/skills/icu-evm-signer | ||
| .forgejo/workflows | ||
| cmd/icu-evm-signer | ||
| internal | ||
| .dockerignore | ||
| .git-allowed-signers | ||
| .gitignore | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| LICENSE | ||
| Makefile | ||
| minisign.pub | ||
| README.md | ||
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.