Offline non-interactive EVM transaction signer for agent workflows.
  • Go 97.4%
  • Makefile 1.9%
  • Dockerfile 0.7%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
dev ac9b874a64
All checks were successful
qualification / linux-amd64 (push) Successful in 2m9s
qualification / darwin-amd64 (push) Successful in 16s
qualification / windows-amd64 (push) Successful in 2m51s
fix(ci): preserve published assets and add release verification
Keep existing release assets and upload only missing files. Leave published
release metadata untouched and finalize a draft only after its assets exist.

Add a manual metadata-only check for an existing release tag. Require the
expected assets, stable release and asset timestamps, and an unchanged tag
without rebuilding or downloading binaries.

Validation: fresh and repeated publishing, incomplete releases, API failures,
old-code regression, 25 verifier cases per repository, and read-only checks
of the current published releases.
2026-10-17 11:18:00 -04:00
.agents/skills/icu-evm-signer Harden offline transaction signing 2026-10-02 14:59:00 -04:00
.forgejo/workflows fix(ci): preserve published assets and add release verification 2026-10-17 11:18:00 -04:00
cmd/icu-evm-signer Harden offline transaction signing 2026-10-02 14:59:00 -04:00
internal Harden offline transaction signing 2026-10-02 14:59:00 -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 Run native CI targets in parallel 2026-10-03 00:00:27 -04:00
go.mod Harden offline transaction signing 2026-10-02 14:59:00 -04:00
go.sum Harden offline transaction signing 2026-10-02 14:59:00 -04:00
LICENSE Initial commit 2026-08-30 00:40:17 -04:00
Makefile Run native CI targets in parallel 2026-10-03 00:00:27 -04:00
minisign.pub Add signed release pipeline 2026-09-14 23:43:18 -04:00
README.md Run native CI targets in parallel 2026-10-03 00:00:27 -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

make build

Builds use Go 1.26.8 and identify local development builds as 0.1.2-dev.

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. Duplicate occurrences of the selected key are rejected, including empty entries.

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. Duplicate JSON fields, including case variants and escaped names, are rejected in the transaction and access-list entries.

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. The preview includes the complete calldata in data and all accessList entries and storage keys; empty values are "0x" and []. Compare every transaction field with the intended request before relay. 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.