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 |
file 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.yaml— a stub configuration file.~/.kms/identity.json— a fresh Ed25519 identity key for the SecretConnection.
Open ~/.kms/kms.yaml 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 a keys entry (the default file backend).
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 a validators entry.
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 chains entry 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 validators entries (e.g. primary + backup nodes).
| Field | Type | Required | Description |
|---|---|---|---|
chain_id |
string | yes | Must match a declared chains[].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. |
A flat list of signing keys. Each entry binds one key to one or more chains via
chain_ids and selects its custodian via backend. The remaining fields depend
on the chosen backend — fields belonging to other backends are ignored. A chain
must be backed by exactly one key.
Fields common to every key:
| Field | Type | Required | Description |
|---|---|---|---|
chain_ids |
list of strings | yes | Chain IDs this key signs for. Each must match a declared chains[].id, and each chain may be backed by only one key. |
backend |
string | no | Custodian: file (default), pkcs11, or awskms. |
algorithm |
string | no | Key algorithm. Defaults to ed25519 (the only supported value today). Consumed by the pkcs11 and awskms backends. |
A file-based Ed25519 private key loaded into memory.
| Field | Type | Required | Description |
|---|---|---|---|
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. Relative paths are resolved against --home. |
An Ed25519 key on a PKCS#11 token or HSM. The private key never leaves the
token: signing is performed on-device via CKM_EDDSA.
| Field | Type | Required | Description |
|---|---|---|---|
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. |
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 key (PIN supplied via environment, keeping it out of the config file):
keys:
- chain_ids: [cosmoshub-4]
backend: pkcs11
module: /usr/lib/softhsm/libsofthsm2.so
token_label: comet
key_label: validator
key_id: "01"
pin_env: KMS_PIN
# algorithm defaults to "ed25519"An Ed25519 key stored in AWS KMS. 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 |
|---|---|---|---|
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. |
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 key (region pinned, credentials from an IAM role):
keys:
- chain_ids: [cosmoshub-4]
backend: awskms
key_id: alias/validator
region: us-east-1
# algorithm defaults to "ed25519"Adding a grpc block enables a TLS gRPC SignerService that runs
alongside the privval dial-out signer. Both transports are served from a
single kms start process; you can also run grpc-only (no chains or
validators entries required when there is no privval workload).
- Sign. Only
RecoverableMessagepayloads are supported. The service computesSHA-256(message)and returns aRecoverableMessageSignaturewith fieldsr,s, andv(a 65-byte recoverable ECDSA signature;vis the 0/1 recovery id). All otherSignpayload types returnUnimplemented. - GetKey / GetKeys. Return
{ id, pubkey, algo }for one or all registered keys.pubkeyis the 33-byte compressed secp256k1 public key;algoissecp256k1.
The server performs no caller authentication or authorization: any client
that can reach the listener may use any configured key. Access must be
restricted entirely by transport and network controls — TLS is mandatory (set
tls_cert and tls_key in the grpc block), and the listener should be
reachable only from trusted callers (e.g. via network policy).
| Field | Type | Required | Description |
|---|---|---|---|
listen |
string | yes | host:port the gRPC server binds to (e.g. 0.0.0.0:9090). |
tls_cert |
string | yes | Path to the TLS server certificate file. |
tls_key |
string | yes | Path to the TLS server private key file. |
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | Logical key identifier returned by GetKey/GetKeys. |
key_file |
string | yes | Path to the hex-encoded 32-byte secp256k1 private key file. |
grpc:
listen: 0.0.0.0:9090
tls_cert: server.crt
tls_key: server.key
keys:
- id: attestor-1
key_file: attestor_secp256k1.hex # hex-encoded 32-byte secp256k1 key# ~/.kms/kms.yaml
chains:
- id: cosmoshub-4
# state_file defaults to <home>/state/cosmoshub-4.json when omitted
validators:
- chain_id: cosmoshub-4
addr: tcp://10.0.0.1:26659
identity_key: identity.json # relative to --home
keys:
- chain_ids: [cosmoshub-4]
backend: file
key_file: /secrets/priv_validator_key.jsonMulti-chain example:
chains:
- id: cosmoshub-4
- id: osmosis-1
validators:
- chain_id: cosmoshub-4
addr: tcp://10.0.0.1:26659
identity_key: identity.json
- chain_id: osmosis-1
addr: tcp://10.0.0.2:26659
identity_key: identity.json
keys:
- chain_ids: [cosmoshub-4]
backend: file
key_file: /secrets/cosmoshub_priv_validator_key.json
- chain_ids: [osmosis-1]
backend: file
key_file: /secrets/osmosis_priv_validator_key.jsonThe 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
validators[].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:
validators:
- 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.
- the file backend 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 detectorPublic packages (importable by external consumers, e.g. an IBC relayer):
kms/
├── config/ # YAML config types (Config, Key, Backend) + validation; embeddable
├── signing/ # Public signing contracts: Backend (consensus) and Signer/Key (service)
│ ├── file/ # File-based Ed25519 / secp256k1 keys
│ ├── pkcs11/ # PKCS#11 / HSM Ed25519 keys (+ pkcs11test helpers)
│ └── awskms/ # AWS KMS Ed25519 keys
└── gen/signerservice/ # Generated SignerService protobuf/gRPC types
Internal packages (the kms daemon; not part of the public API):
kms/
├── cmd/
│ └── kms/ # Binary entrypoint; CLI subcommands (version, init, start, peer-id)
└── internal/
├── version/ # Version string; overridable at link time via -ldflags
├── identity/ # Identity key load/generate (wraps CometBFT p2p.NodeKey)
├── signer/ # ChainSigner: double-sign protection + PrivValidator impl (consumes signing.Backend)
├── signerservice/ # SignerService gRPC server (consumes signing.Signer/Key)
├── manager/ # Manager: supervised dial-out connections with backoff
└── app/ # Wiring: Build() assembles Manager from a validated Config