kms (github.com/cosmos/kms) is an external remote signer for the Cosmos
ecosystem. It is conceptually similar to tmkms,
but implemented entirely in Go and building directly on top of the CometBFT
libraries. It dials out to one or more validator nodes, authenticates each
connection with either CometBFT's SecretConnection protocol or the libp2p Noise
transport (selected per-validator via the address scheme), and serves Ed25519
consensus signing requests (votes, proposals, and vote extensions) with
mandatory per-chain double-sign protection.
The longer-term goal is to be a single remote-signing service for the Cosmos stack: CometBFT consensus (privval) signing today, plus remote signing for IBC relaying and attestation.
| Scope | Status |
|---|---|
| Ed25519 consensus signing (votes, proposals, vote extensions) | Supported |
softsign key backend (file-based, in-memory Ed25519) |
Supported |
pkcs11 key backend (HSM / token, Ed25519) |
Supported |
cometp2p transport (TCP + SecretConnection) |
Supported |
| Multi-chain, multi-validator support | Supported |
| Double-sign protection (reuses CometBFT FilePV state machine) | Supported |
| Dial-out + automatic exponential-backoff reconnect | Supported |
awskms key backend (AWS KMS, Ed25519) |
Supported |
| libp2p transport (Noise) | Supported |
| Account / raw-bytes / ECDSA signing | Planned |
| ML-DSA / eth_secp256k1 key types | Planned |
- Dial-out model.
kmsdials out to the validator's privval listener (priv_validator_laddrinconfig.toml) rather than listening itself. This removes the need to expose any port on the KMS host. - SecretConnection authentication. Each connection is authenticated and
encrypted using CometBFT's SecretConnection protocol.
kmsuses a dedicated Ed25519 identity key (distinct from the consensus signing key) to authenticate itself to the validator. - Request serving. Once connected, the KMS handles
PubKey,SignVote,SignProposal, andPingrequests usingprivval.DefaultValidationRequestHandler. - Double-sign protection. Signing is delegated to CometBFT's
FilePVstate machine, which persists the last-signed height/round/step to a per-chain state file and refuses to sign any regression. This survives process restarts. - Automatic reconnect. When a connection drops (validator restart, network
hiccup),
kmsreconnects with capped exponential backoff (200 ms initial, 10 s ceiling) without any manual intervention.
Requires Go 1.25 or newer. Build via the Makefile (the binary is written to
build/kms):
make build # build/kms
make install # install to GOBINkms init --home ~/.kmsThis creates:
~/.kms/kms.toml— a stub configuration file.~/.kms/identity.json— a fresh Ed25519 identity key for the SecretConnection.
Open ~/.kms/kms.toml and fill in the real values (see the
example config below).
Copy or symlink the priv_validator_key.json for each chain to the path you
set as key_file in [[providers.softsign]].
In the validator's config.toml enable the remote signer listener:
priv_validator_laddr = "tcp://0.0.0.0:26659"The address must be reachable from the host running kms, and it must
match the addr you set in [[validator]].
kms start --home ~/.kmskms will dial the validator and begin serving signing requests. It logs
each connection and any signing errors to stdout.
Declares one blockchain. You need exactly one [[chain]] block per chain you
want to sign for.
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The chain-id string (e.g. cosmoshub-4). |
state_file |
string | no | Path to the double-sign state file. Defaults to <home>/state/<id>.json. Relative paths are resolved against --home. |
Declares one outbound connection to a validator node. A single chain can have
multiple [[validator]] blocks (e.g. primary + backup nodes).
| Field | Type | Required | Description |
|---|---|---|---|
chain_id |
string | yes | Must match a declared [[chain]].id. |
addr |
string | yes | Address of the validator's privval listener. Use tcp://host:port for the standard SecretConnection transport, or noise://<validator-peer-id>@host:port for the libp2p Noise transport (see libp2p Noise transport). |
identity_key |
string | yes | Path to the Ed25519 identity key file used to authenticate the SecretConnection. Relative paths are resolved against --home. Use the file generated by kms init. |
reconnect |
bool | no | Whether to reconnect automatically after a dropped connection. Defaults to true. |
Binds a file-based Ed25519 private key to one or more chains.
| Field | Type | Required | Description |
|---|---|---|---|
chain_ids |
list of strings | yes | Chain IDs this key is used to sign for. Each chain may only have one softsign provider. |
key_file |
string | yes | Path to the key file. Accepts either a CometBFT priv_validator_key.json (typed JSON with a "priv_key" field) or a file containing the raw base64-encoded 64-byte Ed25519 private key. |
Binds an Ed25519 key stored on a PKCS#11 token or HSM to one or more chains. The
private key never leaves the token: signing is performed on-device via CKM_EDDSA.
| Field | Type | Required | Description |
|---|---|---|---|
chain_ids |
list of strings | yes | Chain IDs this key is used to sign for. Each chain may only have one backend (softsign or pkcs11). |
module |
string | yes | Path to the PKCS#11 module shared library (e.g. /usr/lib/softhsm/libsofthsm2.so). Relative paths are resolved against --home. |
token_label |
string | one of token_label/slot | CKA_LABEL of the token to use. |
slot |
integer | one of token_label/slot | Slot number of the token to use. Mutually exclusive with token_label. |
key_label |
string | at least one of key_label/key_id | CKA_LABEL of the key object. |
key_id |
string (hex) | at least one of key_label/key_id | Hex-encoded CKA_ID of the key object. |
pin |
string | exactly one PIN source | User PIN, inline. |
pin_env |
string | exactly one PIN source | Name of an environment variable holding the user PIN. Preferred over inline. |
pin_file |
string | exactly one PIN source | Path to a file containing the user PIN (trailing whitespace trimmed). Relative paths resolved against --home. |
algorithm |
string | no | Key algorithm. Defaults to ed25519 (the only supported value today). |
Provision the key with your HSM tooling before starting kms; the KMS only
uses an existing key, it does not generate or import keys. The key must be an
Ed25519 (CKK_EC_EDWARDS) signing key. Example using pkcs11-tool with SoftHSM2:
softhsm2-util --init-token --free --label comet --pin 1234 --so-pin 4321
pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so --login --pin 1234 \
--keypairgen --key-type EC:edwards25519 --label validator --id 01Example provider block (PIN supplied via environment, keeping it out of the config file):
[[providers.pkcs11]]
chain_ids = ["cosmoshub-4"]
module = "/usr/lib/softhsm/libsofthsm2.so"
token_label = "comet"
key_label = "validator"
key_id = "01"
pin_env = "KMS_PIN"
# algorithm defaults to "ed25519"Binds an Ed25519 key stored in AWS KMS to one or more chains. The private key
never leaves KMS: signing is performed by the KMS Sign API using the
ECC_NIST_EDWARDS25519 key spec and the ED25519_SHA_512 (PureEd25519)
algorithm. AWS credentials are resolved by the standard AWS default credential
chain (environment variables, shared config/credentials files, SSO, or an
IAM instance/container role) — no secrets are placed in the kms config.
| Field | Type | Required | Description |
|---|---|---|---|
chain_ids |
list of strings | yes | Chain IDs this key is used to sign for. Each chain may only have one backend (softsign, pkcs11, or awskms). |
key_id |
string | yes | KMS key identifier: a key id, full key ARN, or an alias (alias/<name>). |
region |
string | no | AWS region of the key. Falls back to the AWS default chain (e.g. AWS_REGION) when omitted. |
profile |
string | no | Shared-config profile name to use. Falls back to the AWS default chain when omitted. |
endpoint |
string | no | Custom KMS endpoint URL. Intended for LocalStack / testing; leave unset for real AWS. |
algorithm |
string | no | Key algorithm. Defaults to ed25519 (the only supported value today). |
Provision the key with your AWS tooling before starting kms; the KMS only
uses an existing key, it does not create or import keys. The key must be an
asymmetric ECC_NIST_EDWARDS25519 key with key usage SIGN_VERIFY. Example
using the AWS CLI:
aws kms create-key --key-spec ECC_NIST_EDWARDS25519 --key-usage SIGN_VERIFY
aws kms create-alias --alias-name alias/validator --target-key-id <key-id>Example provider block (region pinned, credentials from an IAM role):
[[providers.awskms]]
chain_ids = ["cosmoshub-4"]
key_id = "alias/validator"
region = "us-east-1"
# algorithm defaults to "ed25519"# ~/.kms/kms.toml
[[chain]]
id = "cosmoshub-4"
# state_file defaults to <home>/state/cosmoshub-4.json when omitted
[[validator]]
chain_id = "cosmoshub-4"
addr = "tcp://10.0.0.1:26659"
identity_key = "identity.json" # relative to --home
[[providers.softsign]]
chain_ids = ["cosmoshub-4"]
key_file = "/secrets/priv_validator_key.json"Multi-chain example:
[[chain]]
id = "cosmoshub-4"
[[chain]]
id = "osmosis-1"
[[validator]]
chain_id = "cosmoshub-4"
addr = "tcp://10.0.0.1:26659"
identity_key = "identity.json"
[[validator]]
chain_id = "osmosis-1"
addr = "tcp://10.0.0.2:26659"
identity_key = "identity.json"
[[providers.softsign]]
chain_ids = ["cosmoshub-4"]
key_file = "/secrets/cosmoshub_priv_validator_key.json"
[[providers.softsign]]
chain_ids = ["osmosis-1"]
key_file = "/secrets/osmosis_priv_validator_key.json"The libp2p Noise transport is an alternative to the default SecretConnection
(tcp://) channel. Both sides use the same TCP port and listener — the address
scheme (noise:// vs tcp://) is what selects which handshake is performed.
No libp2p switch, host, or gossip network is involved; it is a direct TCP
connection secured by the Noise_XX handshake.
The key difference from SecretConnection is pinned peer IDs on both sides.
SecretConnection uses an ephemeral, unpinned key on the validator's listener,
which means the KMS cannot verify it is talking to the right validator. With the
Noise transport, each side asserts a stable libp2p identity derived from its
existing keys (the validator's node key, the KMS's identity.json), and each
side refuses any connection from an unexpected peer.
KMS peer ID (give this to the validator operator so they can pin it in
priv_validator_laddr):
kms peer-id --home ~/.kmsThis reads identity.json from <home> and prints the corresponding libp2p
peer ID.
Validator peer ID (give this to the KMS operator so they can pin it in
[[validator]].addr):
cometbft show-node-id --libp2p --home ~/.cometbftThis prints the libp2p peer ID derived from the validator's node key.
Set addr to a noise:// URI that embeds the validator's peer ID:
[[validator]]
chain_id = "cosmoshub-4"
addr = "noise://12D3KooW...validatorPeerID...@10.0.0.1:26659"
identity_key = "identity.json" # reused as the KMS's libp2p identityThe identity_key field serves double duty: it authenticates the
SecretConnection channel when tcp:// is used, and it provides the KMS's
libp2p identity (peer ID) when noise:// is used. No additional key file is
needed.
Set priv_validator_laddr in the validator's config.toml to a noise://
URI that embeds the KMS peer ID:
priv_validator_laddr = "noise://12D3KooW...kmsPeerID...@0.0.0.0:26659"The validator uses its node key (node_key.json) as its Noise identity.
Any incoming connection whose authenticated peer ID does not match the pinned
KMS peer ID is rejected before any signing request is served.
Both sides must configure the other's peer ID:
- The KMS encodes the validator's peer ID in the
noise://address it dials — the handshake fails immediately if the remote key does not match. - The validator encodes the KMS's peer ID in
priv_validator_laddr— any connection from a different peer is dropped.
There is no way to disable peer-ID pinning when using noise://; it is
enforced unconditionally.
Ed25519 and secp256k1 keys are supported. Consensus keys and node keys in CometBFT are Ed25519, so no extra setup is needed.
- softsign is NOT for production custody. The private key is loaded from
disk and held in process memory in plaintext for the lifetime of the process.
Use the
pkcs11backend (or theawskmsbackend) for production environments where the key must never leave secure hardware. - The identity key is not the consensus signing key.
identity.jsonauthenticates the SecretConnection channel; it does not sign consensus messages and does not need to be protected to the same degree as thepriv_validator_key.json. - Double-sign protection is per state file. The state file records the
highest height/round/step that has been signed.
kmsrefuses to sign any message that would regress this high-water mark. Never run twokmsinstances against the same validator with different (or missing) state files — doing so removes the double-sign protection. - Validator listener exposure. The validator's
priv_validator_laddrbinds a TCP port. Ensure it is not reachable from untrusted networks (use a firewall or a private VLAN between the validator and the KMS host).
make test # go test ./... -count=1
make test-race # with the race detectorkms/
├── cmd/
│ └── kms/ # Binary entrypoint; CLI subcommands (version, init, start, peer-id)
│ ├── main.go
│ └── main_test.go
└── internal/
├── version/ # Version string; overridable at link time via -ldflags
├── config/ # TOML config types (config.go) and validation (validate.go)
├── identity/ # Identity key load/generate (wraps CometBFT p2p.NodeKey)
├── backend/ # backend.Signer interface
│ ├── softsign/ # File-based Ed25519 backend
│ ├── pkcs11/ # PKCS#11 / HSM Ed25519 backend (+ pkcs11test helpers)
│ └── awskms/ # AWS KMS Ed25519 backend
├── signer/ # ChainSigner: double-sign protection + PrivValidator impl
│ ├── chain_signer.go
│ └── privkey_adapter.go
├── manager/ # Manager: supervised dial-out connections with backoff
│ ├── manager.go
│ └── dialer.go
└── app/ # Wiring: Build() assembles Manager from a validated Config