@ -1,156 +0,0 @@ | |||
# Remote Signer | |||
Located under the `tools/tm-signer-harness` folder in the [Tendermint | |||
repository](https://github.com/tendermint/tendermint). | |||
The Tendermint remote signer test harness facilitates integration testing | |||
between Tendermint and remote signers such as | |||
[tkkms](https://github.com/iqlusioninc/tmkms). Such remote signers allow for signing | |||
of important Tendermint messages using | |||
[HSMs](https://en.wikipedia.org/wiki/Hardware_security_module), providing | |||
additional security. | |||
When executed, `tm-signer-harness`: | |||
1. Runs a listener (either TCP or Unix sockets). | |||
2. Waits for a connection from the remote signer. | |||
3. Upon connection from the remote signer, executes a number of automated tests | |||
to ensure compatibility. | |||
4. Upon successful validation, the harness process exits with a 0 exit code. | |||
Upon validation failure, it exits with a particular exit code related to the | |||
error. | |||
## Prerequisites | |||
Requires the same prerequisites as for building | |||
[Tendermint](https://github.com/tendermint/tendermint). | |||
## Building | |||
From the `tools/tm-signer-harness` directory in your Tendermint source | |||
repository, simply run: | |||
```bash | |||
make | |||
# To have global access to this executable | |||
make install | |||
``` | |||
## Docker Image | |||
To build a Docker image containing the `tm-signer-harness`, also from the | |||
`tools/tm-signer-harness` directory of your Tendermint source repo, simply run: | |||
```bash | |||
make docker-image | |||
``` | |||
## Running against KMS | |||
As an example of how to use `tm-signer-harness`, the following instructions show | |||
you how to execute its tests against [tkkms](https://github.com/iqlusioninc/tmkms). | |||
For this example, we will make use of the **software signing module in KMS**, as | |||
the hardware signing module requires a physical | |||
[YubiHSM](https://www.yubico.com/products/yubihsm/) device. | |||
### Step 1: Install KMS on your local machine | |||
See the [tkkms repo](https://github.com/iqlusioninc/tmkms) for details on how to set | |||
KMS up on your local machine. | |||
If you have [Rust](https://www.rust-lang.org/) installed on your local machine, | |||
you can simply install KMS by: | |||
```bash | |||
cargo install tmkms | |||
``` | |||
### Step 2: Make keys for KMS | |||
The KMS software signing module needs a key with which to sign messages. In our | |||
example, we will simply export a signing key from our local Tendermint instance. | |||
```bash | |||
# Will generate all necessary Tendermint configuration files, including: | |||
# - ~/.tendermint/config/priv_validator_key.json | |||
# - ~/.tendermint/data/priv_validator_state.json | |||
tendermint init validator | |||
# Extract the signing key from our local Tendermint instance | |||
tm-signer-harness extract_key \ # Use the "extract_key" command | |||
-tmhome ~/.tendermint \ # Where to find the Tendermint home directory | |||
-output ./signing.key # Where to write the key | |||
``` | |||
Also, because we want KMS to connect to `tm-signer-harness`, we will need to | |||
provide a secret connection key from KMS' side: | |||
```bash | |||
tmkms keygen secret_connection.key | |||
``` | |||
### Step 3: Configure and run KMS | |||
KMS needs some configuration to tell it to use the softer signing module as well | |||
as the `signing.key` file we just generated. Save the following to a file called | |||
`tmkms.toml`: | |||
```toml | |||
[[validator]] | |||
addr = "tcp://127.0.0.1:61219" # This is where we will find tm-signer-harness. | |||
chain_id = "test-chain-0XwP5E" # The Tendermint chain ID for which KMS will be signing (found in ~/.tendermint/config/genesis.json). | |||
reconnect = true # true is the default | |||
secret_key = "./secret_connection.key" # Where to find our secret connection key. | |||
[[providers.softsign]] | |||
id = "test-chain-0XwP5E" # The Tendermint chain ID for which KMS will be signing (same as validator.chain_id above). | |||
path = "./signing.key" # The signing key we extracted earlier. | |||
``` | |||
Then run KMS with this configuration: | |||
```bash | |||
tmkms start -c tmkms.toml | |||
``` | |||
This will start KMS, which will repeatedly try to connect to | |||
`tcp://127.0.0.1:61219` until it is successful. | |||
### Step 4: Run tm-signer-harness | |||
Now we get to run the signer test harness: | |||
```bash | |||
tm-signer-harness run \ # The "run" command executes the tests | |||
-addr tcp://127.0.0.1:61219 \ # The address we promised KMS earlier | |||
-tmhome ~/.tendermint # Where to find our Tendermint configuration/data files. | |||
``` | |||
If the current version of Tendermint and KMS are compatible, `tm-signer-harness` | |||
should now exit with a 0 exit code. If they are somehow not compatible, it | |||
should exit with a meaningful non-zero exit code (see the exit codes below). | |||
### Step 5: Shut down KMS | |||
Simply hit Ctrl+Break on your KMS instance (or use the `kill` command in Linux) | |||
to terminate it gracefully. | |||
## Exit Code Meanings | |||
The following list shows the various exit codes from `tm-signer-harness` and | |||
their meanings: | |||
| Exit Code | Description | | |||
| --- | --- | | |||
| 0 | Success! | | |||
| 1 | Invalid command line parameters supplied to `tm-signer-harness` | | |||
| 2 | Maximum number of accept retries reached (the `-accept-retries` parameter) | | |||
| 3 | Failed to load `${TMHOME}/config/genesis.json` | | |||
| 4 | Failed to create listener specified by `-addr` parameter | | |||
| 5 | Failed to start listener | | |||
| 6 | Interrupted by `SIGINT` (e.g. when hitting Ctrl+Break or Ctrl+C) | | |||
| 7 | Other unknown error | | |||
| 8 | Test 1 failed: public key mismatch | | |||
| 9 | Test 2 failed: signing of proposals failed | | |||
| 10 | Test 3 failed: signing of votes failed | |
@ -1,4 +0,0 @@ | |||
ARG TENDERMINT_VERSION=latest | |||
FROM tendermint/tendermint:${TENDERMINT_VERSION} | |||
COPY tm-signer-harness /usr/bin/tm-signer-harness |
@ -1,21 +0,0 @@ | |||
.PHONY: build install docker-image | |||
TENDERMINT_VERSION?=latest | |||
BUILD_TAGS?='tendermint' | |||
VERSION := $(shell git describe --always) | |||
BUILD_FLAGS = -ldflags "-X github.com/tendermint/tendermint/version.TMCoreSemVer=$(VERSION)" | |||
.DEFAULT_GOAL := build | |||
build: | |||
CGO_ENABLED=0 go build $(BUILD_FLAGS) -tags $(BUILD_TAGS) -o ../../build/tm-signer-harness main.go | |||
install: | |||
CGO_ENABLED=0 go install $(BUILD_FLAGS) -tags $(BUILD_TAGS) . | |||
docker-image: | |||
GOOS=linux GOARCH=amd64 go build $(BUILD_FLAGS) -tags $(BUILD_TAGS) -o tm-signer-harness main.go | |||
docker build \ | |||
--build-arg TENDERMINT_VERSION=$(TENDERMINT_VERSION) \ | |||
-t tendermint/tm-signer-harness:$(TENDERMINT_VERSION) . | |||
rm -rf tm-signer-harness |
@ -1,5 +0,0 @@ | |||
# tm-signer-harness | |||
See the [`tm-signer-harness` | |||
documentation](https://tendermint.com/docs/tools/remote-signer-validation.html) | |||
for more details. |
@ -1,427 +0,0 @@ | |||
package internal | |||
import ( | |||
"bytes" | |||
"context" | |||
"fmt" | |||
"net" | |||
"os" | |||
"os/signal" | |||
"time" | |||
"github.com/tendermint/tendermint/crypto/tmhash" | |||
"github.com/tendermint/tendermint/crypto/ed25519" | |||
"github.com/tendermint/tendermint/internal/state" | |||
"github.com/tendermint/tendermint/privval" | |||
"github.com/tendermint/tendermint/libs/log" | |||
tmnet "github.com/tendermint/tendermint/libs/net" | |||
tmos "github.com/tendermint/tendermint/libs/os" | |||
tmproto "github.com/tendermint/tendermint/proto/tendermint/types" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
// Test harness error codes (which act as exit codes when the test harness fails). | |||
const ( | |||
NoError int = iota // 0 | |||
ErrInvalidParameters // 1 | |||
ErrMaxAcceptRetriesReached // 2 | |||
ErrFailedToLoadGenesisFile // 3 | |||
ErrFailedToCreateListener // 4 | |||
ErrFailedToStartListener // 5 | |||
ErrInterrupted // 6 | |||
ErrOther // 7 | |||
ErrTestPublicKeyFailed // 8 | |||
ErrTestSignProposalFailed // 9 | |||
ErrTestSignVoteFailed // 10 | |||
) | |||
var voteTypes = []tmproto.SignedMsgType{tmproto.PrevoteType, tmproto.PrecommitType} | |||
// TestHarnessError allows us to keep track of which exit code should be used | |||
// when exiting the main program. | |||
type TestHarnessError struct { | |||
Code int // The exit code to return | |||
Err error // The original error | |||
Info string // Any additional information | |||
} | |||
var _ error = (*TestHarnessError)(nil) | |||
// TestHarness allows for testing of a remote signer to ensure compatibility | |||
// with this version of Tendermint. | |||
type TestHarness struct { | |||
addr string | |||
signerClient *privval.SignerClient | |||
fpv *privval.FilePV | |||
chainID string | |||
acceptRetries int | |||
logger log.Logger | |||
exitWhenComplete bool | |||
exitCode int | |||
} | |||
// TestHarnessConfig provides configuration to set up a remote signer test | |||
// harness. | |||
type TestHarnessConfig struct { | |||
BindAddr string | |||
KeyFile string | |||
StateFile string | |||
GenesisFile string | |||
AcceptDeadline time.Duration | |||
ConnDeadline time.Duration | |||
AcceptRetries int | |||
SecretConnKey ed25519.PrivKey | |||
ExitWhenComplete bool // Whether or not to call os.Exit when the harness has completed. | |||
} | |||
// timeoutError can be used to check if an error returned from the netp package | |||
// was due to a timeout. | |||
type timeoutError interface { | |||
Timeout() bool | |||
} | |||
// NewTestHarness will load Tendermint data from the given files (including | |||
// validator public/private keypairs and chain details) and create a new | |||
// harness. | |||
func NewTestHarness(ctx context.Context, logger log.Logger, cfg TestHarnessConfig) (*TestHarness, error) { | |||
keyFile := ExpandPath(cfg.KeyFile) | |||
stateFile := ExpandPath(cfg.StateFile) | |||
logger.Info("Loading private validator configuration", "keyFile", keyFile, "stateFile", stateFile) | |||
// NOTE: LoadFilePV ultimately calls os.Exit on failure. No error will be | |||
// returned if this call fails. | |||
fpv, err := privval.LoadFilePV(keyFile, stateFile) | |||
if err != nil { | |||
return nil, err | |||
} | |||
genesisFile := ExpandPath(cfg.GenesisFile) | |||
logger.Info("Loading chain ID from genesis file", "genesisFile", genesisFile) | |||
st, err := state.MakeGenesisDocFromFile(genesisFile) | |||
if err != nil { | |||
return nil, newTestHarnessError(ErrFailedToLoadGenesisFile, err, genesisFile) | |||
} | |||
logger.Info("Loaded genesis file", "chainID", st.ChainID) | |||
spv, err := newTestHarnessListener(logger, cfg) | |||
if err != nil { | |||
return nil, newTestHarnessError(ErrFailedToCreateListener, err, "") | |||
} | |||
signerClient, err := privval.NewSignerClient(ctx, spv, st.ChainID) | |||
if err != nil { | |||
return nil, newTestHarnessError(ErrFailedToCreateListener, err, "") | |||
} | |||
return &TestHarness{ | |||
addr: cfg.BindAddr, | |||
signerClient: signerClient, | |||
fpv: fpv, | |||
chainID: st.ChainID, | |||
acceptRetries: cfg.AcceptRetries, | |||
logger: logger, | |||
exitWhenComplete: cfg.ExitWhenComplete, | |||
exitCode: 0, | |||
}, nil | |||
} | |||
// Run will execute the tests associated with this test harness. The intention | |||
// here is to call this from one's `main` function, as the way it succeeds or | |||
// fails at present is to call os.Exit() with an exit code related to the error | |||
// that caused the tests to fail, or exit code 0 on success. | |||
func (th *TestHarness) Run() { | |||
c := make(chan os.Signal, 1) | |||
signal.Notify(c, os.Interrupt) | |||
go func() { | |||
for sig := range c { | |||
th.logger.Info("Caught interrupt, terminating...", "sig", sig) | |||
th.Shutdown(newTestHarnessError(ErrInterrupted, nil, "")) | |||
} | |||
}() | |||
th.logger.Info("Starting test harness") | |||
accepted := false | |||
var startErr error | |||
for acceptRetries := th.acceptRetries; acceptRetries > 0; acceptRetries-- { | |||
th.logger.Info("Attempting to accept incoming connection", "acceptRetries", acceptRetries) | |||
if err := th.signerClient.WaitForConnection(10 * time.Millisecond); err != nil { | |||
// if it wasn't a timeout error | |||
if _, ok := err.(timeoutError); !ok { | |||
th.logger.Error("Failed to start listener", "err", err) | |||
th.Shutdown(newTestHarnessError(ErrFailedToStartListener, err, "")) | |||
// we need the return statements in case this is being run | |||
// from a unit test - otherwise this function will just die | |||
// when os.Exit is called | |||
return | |||
} | |||
startErr = err | |||
} else { | |||
th.logger.Info("Accepted external connection") | |||
accepted = true | |||
break | |||
} | |||
} | |||
if !accepted { | |||
th.logger.Error("Maximum accept retries reached", "acceptRetries", th.acceptRetries) | |||
th.Shutdown(newTestHarnessError(ErrMaxAcceptRetriesReached, startErr, "")) | |||
return | |||
} | |||
// Run the tests | |||
if err := th.TestPublicKey(); err != nil { | |||
th.Shutdown(err) | |||
return | |||
} | |||
if err := th.TestSignProposal(); err != nil { | |||
th.Shutdown(err) | |||
return | |||
} | |||
if err := th.TestSignVote(); err != nil { | |||
th.Shutdown(err) | |||
return | |||
} | |||
th.logger.Info("SUCCESS! All tests passed.") | |||
th.Shutdown(nil) | |||
} | |||
// TestPublicKey just validates that we can (1) fetch the public key from the | |||
// remote signer, and (2) it matches the public key we've configured for our | |||
// local Tendermint version. | |||
func (th *TestHarness) TestPublicKey() error { | |||
th.logger.Info("TEST: Public key of remote signer") | |||
fpvk, err := th.fpv.GetPubKey(context.Background()) | |||
if err != nil { | |||
return err | |||
} | |||
th.logger.Info("Local", "pubKey", fpvk) | |||
sck, err := th.signerClient.GetPubKey(context.Background()) | |||
if err != nil { | |||
return err | |||
} | |||
th.logger.Info("Remote", "pubKey", sck) | |||
if !bytes.Equal(fpvk.Bytes(), sck.Bytes()) { | |||
th.logger.Error("FAILED: Local and remote public keys do not match") | |||
return newTestHarnessError(ErrTestPublicKeyFailed, nil, "") | |||
} | |||
return nil | |||
} | |||
// TestSignProposal makes sure the remote signer can successfully sign | |||
// proposals. | |||
func (th *TestHarness) TestSignProposal() error { | |||
th.logger.Info("TEST: Signing of proposals") | |||
// sha256 hash of "hash" | |||
hash := tmhash.Sum([]byte("hash")) | |||
prop := &types.Proposal{ | |||
Type: tmproto.ProposalType, | |||
Height: 100, | |||
Round: 0, | |||
POLRound: -1, | |||
BlockID: types.BlockID{ | |||
Hash: hash, | |||
PartSetHeader: types.PartSetHeader{ | |||
Hash: hash, | |||
Total: 1000000, | |||
}, | |||
}, | |||
Timestamp: time.Now(), | |||
} | |||
p := prop.ToProto() | |||
propBytes := types.ProposalSignBytes(th.chainID, p) | |||
if err := th.signerClient.SignProposal(context.Background(), th.chainID, p); err != nil { | |||
th.logger.Error("FAILED: Signing of proposal", "err", err) | |||
return newTestHarnessError(ErrTestSignProposalFailed, err, "") | |||
} | |||
prop.Signature = p.Signature | |||
th.logger.Debug("Signed proposal", "prop", prop) | |||
// first check that it's a basically valid proposal | |||
if err := prop.ValidateBasic(); err != nil { | |||
th.logger.Error("FAILED: Signed proposal is invalid", "err", err) | |||
return newTestHarnessError(ErrTestSignProposalFailed, err, "") | |||
} | |||
sck, err := th.signerClient.GetPubKey(context.Background()) | |||
if err != nil { | |||
return err | |||
} | |||
// now validate the signature on the proposal | |||
if sck.VerifySignature(propBytes, prop.Signature) { | |||
th.logger.Info("Successfully validated proposal signature") | |||
} else { | |||
th.logger.Error("FAILED: Proposal signature validation failed") | |||
return newTestHarnessError(ErrTestSignProposalFailed, nil, "signature validation failed") | |||
} | |||
return nil | |||
} | |||
// TestSignVote makes sure the remote signer can successfully sign all kinds of | |||
// votes. | |||
func (th *TestHarness) TestSignVote() error { | |||
th.logger.Info("TEST: Signing of votes") | |||
for _, voteType := range voteTypes { | |||
th.logger.Info("Testing vote type", "type", voteType) | |||
hash := tmhash.Sum([]byte("hash")) | |||
vote := &types.Vote{ | |||
Type: voteType, | |||
Height: 101, | |||
Round: 0, | |||
BlockID: types.BlockID{ | |||
Hash: hash, | |||
PartSetHeader: types.PartSetHeader{ | |||
Hash: hash, | |||
Total: 1000000, | |||
}, | |||
}, | |||
ValidatorIndex: 0, | |||
ValidatorAddress: tmhash.SumTruncated([]byte("addr")), | |||
Timestamp: time.Now(), | |||
} | |||
v := vote.ToProto() | |||
voteBytes := types.VoteSignBytes(th.chainID, v) | |||
// sign the vote | |||
if err := th.signerClient.SignVote(context.Background(), th.chainID, v); err != nil { | |||
th.logger.Error("FAILED: Signing of vote", "err", err) | |||
return newTestHarnessError(ErrTestSignVoteFailed, err, fmt.Sprintf("voteType=%d", voteType)) | |||
} | |||
vote.Signature = v.Signature | |||
th.logger.Debug("Signed vote", "vote", vote) | |||
// validate the contents of the vote | |||
if err := vote.ValidateBasic(); err != nil { | |||
th.logger.Error("FAILED: Signed vote is invalid", "err", err) | |||
return newTestHarnessError(ErrTestSignVoteFailed, err, fmt.Sprintf("voteType=%d", voteType)) | |||
} | |||
sck, err := th.signerClient.GetPubKey(context.Background()) | |||
if err != nil { | |||
return err | |||
} | |||
// now validate the signature on the proposal | |||
if sck.VerifySignature(voteBytes, vote.Signature) { | |||
th.logger.Info("Successfully validated vote signature", "type", voteType) | |||
} else { | |||
th.logger.Error("FAILED: Vote signature validation failed", "type", voteType) | |||
return newTestHarnessError(ErrTestSignVoteFailed, nil, "signature validation failed") | |||
} | |||
} | |||
return nil | |||
} | |||
// Shutdown will kill the test harness and attempt to close all open sockets | |||
// gracefully. If the supplied error is nil, it is assumed that the exit code | |||
// should be 0. If err is not nil, it will exit with an exit code related to the | |||
// error. | |||
func (th *TestHarness) Shutdown(err error) { | |||
var exitCode int | |||
if err == nil { | |||
exitCode = NoError | |||
} else if therr, ok := err.(*TestHarnessError); ok { | |||
exitCode = therr.Code | |||
} else { | |||
exitCode = ErrOther | |||
} | |||
th.exitCode = exitCode | |||
// in case sc.Stop() takes too long | |||
if th.exitWhenComplete { | |||
go func() { | |||
time.Sleep(time.Duration(5) * time.Second) | |||
th.logger.Error("Forcibly exiting program after timeout") | |||
os.Exit(exitCode) | |||
}() | |||
} | |||
err = th.signerClient.Close() | |||
if err != nil { | |||
th.logger.Error("Failed to cleanly stop listener: %s", err.Error()) | |||
} | |||
if th.exitWhenComplete { | |||
os.Exit(exitCode) | |||
} | |||
} | |||
// newTestHarnessListener creates our client instance which we will use for testing. | |||
func newTestHarnessListener(logger log.Logger, cfg TestHarnessConfig) (*privval.SignerListenerEndpoint, error) { | |||
proto, addr := tmnet.ProtocolAndAddress(cfg.BindAddr) | |||
if proto == "unix" { | |||
// make sure the socket doesn't exist - if so, try to delete it | |||
if tmos.FileExists(addr) { | |||
if err := os.Remove(addr); err != nil { | |||
logger.Error("Failed to remove existing Unix domain socket", "addr", addr) | |||
return nil, err | |||
} | |||
} | |||
} | |||
ln, err := net.Listen(proto, addr) | |||
if err != nil { | |||
return nil, err | |||
} | |||
logger.Info("Listening", "proto", proto, "addr", addr) | |||
var svln net.Listener | |||
switch proto { | |||
case "unix": | |||
unixLn := privval.NewUnixListener(ln) | |||
privval.UnixListenerTimeoutAccept(cfg.AcceptDeadline)(unixLn) | |||
privval.UnixListenerTimeoutReadWrite(cfg.ConnDeadline)(unixLn) | |||
svln = unixLn | |||
case "tcp": | |||
tcpLn := privval.NewTCPListener(ln, cfg.SecretConnKey) | |||
privval.TCPListenerTimeoutAccept(cfg.AcceptDeadline)(tcpLn) | |||
privval.TCPListenerTimeoutReadWrite(cfg.ConnDeadline)(tcpLn) | |||
logger.Info("Resolved TCP address for listener", "addr", tcpLn.Addr()) | |||
svln = tcpLn | |||
default: | |||
_ = ln.Close() | |||
logger.Error("Unsupported protocol (must be unix:// or tcp://)", "proto", proto) | |||
return nil, newTestHarnessError(ErrInvalidParameters, nil, fmt.Sprintf("Unsupported protocol: %s", proto)) | |||
} | |||
return privval.NewSignerListenerEndpoint(logger, svln), nil | |||
} | |||
func newTestHarnessError(code int, err error, info string) *TestHarnessError { | |||
return &TestHarnessError{ | |||
Code: code, | |||
Err: err, | |||
Info: info, | |||
} | |||
} | |||
func (e *TestHarnessError) Error() string { | |||
var msg string | |||
switch e.Code { | |||
case ErrInvalidParameters: | |||
msg = "Invalid parameters supplied to application" | |||
case ErrMaxAcceptRetriesReached: | |||
msg = "Maximum accept retries reached" | |||
case ErrFailedToLoadGenesisFile: | |||
msg = "Failed to load genesis file" | |||
case ErrFailedToCreateListener: | |||
msg = "Failed to create listener" | |||
case ErrFailedToStartListener: | |||
msg = "Failed to start listener" | |||
case ErrInterrupted: | |||
msg = "Interrupted" | |||
case ErrTestPublicKeyFailed: | |||
msg = "Public key validation test failed" | |||
case ErrTestSignProposalFailed: | |||
msg = "Proposal signing validation test failed" | |||
case ErrTestSignVoteFailed: | |||
msg = "Vote signing validation test failed" | |||
default: | |||
msg = "Unknown error" | |||
} | |||
if len(e.Info) > 0 { | |||
msg = fmt.Sprintf("%s: %s", msg, e.Info) | |||
} | |||
if e.Err != nil { | |||
msg = fmt.Sprintf("%s (original error: %s)", msg, e.Err.Error()) | |||
} | |||
return msg | |||
} |
@ -1,202 +0,0 @@ | |||
package internal | |||
import ( | |||
"context" | |||
"fmt" | |||
"os" | |||
"testing" | |||
"time" | |||
"github.com/stretchr/testify/assert" | |||
"github.com/stretchr/testify/require" | |||
"github.com/tendermint/tendermint/crypto" | |||
"github.com/tendermint/tendermint/crypto/ed25519" | |||
tmjson "github.com/tendermint/tendermint/libs/json" | |||
"github.com/tendermint/tendermint/libs/log" | |||
tmrand "github.com/tendermint/tendermint/libs/rand" | |||
"github.com/tendermint/tendermint/privval" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
const ( | |||
defaultConnDeadline = 100 | |||
) | |||
func TestRemoteSignerTestHarnessMaxAcceptRetriesReached(t *testing.T) { | |||
ctx, cancel := context.WithCancel(context.Background()) | |||
defer cancel() | |||
cfg := makeConfig(t, 1, 2) | |||
defer cleanup(cfg) | |||
th, err := NewTestHarness(ctx, log.TestingLogger(), cfg) | |||
require.NoError(t, err) | |||
th.Run() | |||
assert.Equal(t, ErrMaxAcceptRetriesReached, th.exitCode) | |||
} | |||
func TestRemoteSignerTestHarnessSuccessfulRun(t *testing.T) { | |||
ctx, cancel := context.WithCancel(context.Background()) | |||
defer cancel() | |||
harnessTest( | |||
ctx, | |||
t, | |||
func(th *TestHarness) *privval.SignerServer { | |||
return newMockSignerServer(t, th, th.fpv.Key.PrivKey, false, false) | |||
}, | |||
NoError, | |||
) | |||
} | |||
func TestRemoteSignerPublicKeyCheckFailed(t *testing.T) { | |||
ctx, cancel := context.WithCancel(context.Background()) | |||
defer cancel() | |||
harnessTest( | |||
ctx, | |||
t, | |||
func(th *TestHarness) *privval.SignerServer { | |||
return newMockSignerServer(t, th, ed25519.GenPrivKey(), false, false) | |||
}, | |||
ErrTestPublicKeyFailed, | |||
) | |||
} | |||
func TestRemoteSignerProposalSigningFailed(t *testing.T) { | |||
ctx, cancel := context.WithCancel(context.Background()) | |||
defer cancel() | |||
harnessTest( | |||
ctx, | |||
t, | |||
func(th *TestHarness) *privval.SignerServer { | |||
return newMockSignerServer(t, th, th.fpv.Key.PrivKey, true, false) | |||
}, | |||
ErrTestSignProposalFailed, | |||
) | |||
} | |||
func TestRemoteSignerVoteSigningFailed(t *testing.T) { | |||
ctx, cancel := context.WithCancel(context.Background()) | |||
defer cancel() | |||
harnessTest( | |||
ctx, | |||
t, | |||
func(th *TestHarness) *privval.SignerServer { | |||
return newMockSignerServer(t, th, th.fpv.Key.PrivKey, false, true) | |||
}, | |||
ErrTestSignVoteFailed, | |||
) | |||
} | |||
func newMockSignerServer( | |||
t *testing.T, | |||
th *TestHarness, | |||
privKey crypto.PrivKey, | |||
breakProposalSigning bool, | |||
breakVoteSigning bool, | |||
) *privval.SignerServer { | |||
mockPV := types.NewMockPVWithParams(privKey, breakProposalSigning, breakVoteSigning) | |||
dialerEndpoint := privval.NewSignerDialerEndpoint( | |||
th.logger, | |||
privval.DialTCPFn( | |||
th.addr, | |||
time.Duration(defaultConnDeadline)*time.Millisecond, | |||
ed25519.GenPrivKey(), | |||
), | |||
) | |||
return privval.NewSignerServer(dialerEndpoint, th.chainID, mockPV) | |||
} | |||
// For running relatively standard tests. | |||
func harnessTest( | |||
ctx context.Context, | |||
t *testing.T, | |||
signerServerMaker func(th *TestHarness) *privval.SignerServer, | |||
expectedExitCode int, | |||
) { | |||
cfg := makeConfig(t, 100, 3) | |||
defer cleanup(cfg) | |||
th, err := NewTestHarness(ctx, log.TestingLogger(), cfg) | |||
require.NoError(t, err) | |||
donec := make(chan struct{}) | |||
go func() { | |||
defer close(donec) | |||
th.Run() | |||
}() | |||
ss := signerServerMaker(th) | |||
require.NoError(t, ss.Start(ctx)) | |||
assert.True(t, ss.IsRunning()) | |||
defer ss.Stop() //nolint:errcheck // ignore for tests | |||
<-donec | |||
assert.Equal(t, expectedExitCode, th.exitCode) | |||
} | |||
func makeConfig(t *testing.T, acceptDeadline, acceptRetries int) TestHarnessConfig { | |||
t.Helper() | |||
const keyFilename = "tm-testharness-keyfile" | |||
const stateFilename = "tm-testharness-statefile" | |||
pvFile, err := privval.GenFilePV(keyFilename, stateFilename, types.ABCIPubKeyTypeEd25519) | |||
if err != nil { | |||
panic(err) | |||
} | |||
pvGenDoc := types.GenesisDoc{ | |||
ChainID: fmt.Sprintf("test-chain-%v", tmrand.Str(6)), | |||
GenesisTime: time.Now(), | |||
ConsensusParams: types.DefaultConsensusParams(), | |||
Validators: []types.GenesisValidator{ | |||
{ | |||
Address: pvFile.Key.Address, | |||
PubKey: pvFile.Key.PubKey, | |||
Power: 10, | |||
}, | |||
}, | |||
} | |||
keyFileContents, err := tmjson.Marshal(pvFile.Key) | |||
require.NoError(t, err) | |||
stateFileContents, err := tmjson.Marshal(pvFile.LastSignState) | |||
require.NoError(t, err) | |||
genesisFileContents, err := tmjson.Marshal(pvGenDoc) | |||
require.NoError(t, err) | |||
return TestHarnessConfig{ | |||
BindAddr: privval.GetFreeLocalhostAddrPort(), | |||
KeyFile: makeTempFile(keyFilename, keyFileContents), | |||
StateFile: makeTempFile(stateFilename, stateFileContents), | |||
GenesisFile: makeTempFile("tm-testharness-genesisfile", genesisFileContents), | |||
AcceptDeadline: time.Duration(acceptDeadline) * time.Millisecond, | |||
ConnDeadline: time.Duration(defaultConnDeadline) * time.Millisecond, | |||
AcceptRetries: acceptRetries, | |||
SecretConnKey: ed25519.GenPrivKey(), | |||
ExitWhenComplete: false, | |||
} | |||
} | |||
func cleanup(cfg TestHarnessConfig) { | |||
os.Remove(cfg.KeyFile) | |||
os.Remove(cfg.StateFile) | |||
os.Remove(cfg.GenesisFile) | |||
} | |||
func makeTempFile(name string, content []byte) string { | |||
tempFile, err := os.CreateTemp("", fmt.Sprintf("%s-*", name)) | |||
if err != nil { | |||
panic(err) | |||
} | |||
if _, err := tempFile.Write(content); err != nil { | |||
tempFile.Close() | |||
panic(err) | |||
} | |||
if err := tempFile.Close(); err != nil { | |||
panic(err) | |||
} | |||
return tempFile.Name() | |||
} |
@ -1,25 +0,0 @@ | |||
package internal | |||
import ( | |||
"os/user" | |||
"path/filepath" | |||
"strings" | |||
) | |||
// ExpandPath will check if the given path begins with a "~" symbol, and if so, | |||
// will expand it to become the user's home directory. If it fails to expand the | |||
// path it will automatically return the original path itself. | |||
func ExpandPath(path string) string { | |||
usr, err := user.Current() | |||
if err != nil { | |||
return path | |||
} | |||
if path == "~" { | |||
return usr.HomeDir | |||
} else if strings.HasPrefix(path, "~/") { | |||
return filepath.Join(usr.HomeDir, path[2:]) | |||
} | |||
return path | |||
} |
@ -1,203 +0,0 @@ | |||
package main | |||
import ( | |||
"context" | |||
"flag" | |||
"fmt" | |||
"os" | |||
"path/filepath" | |||
"time" | |||
"github.com/tendermint/tendermint/crypto/ed25519" | |||
"github.com/tendermint/tendermint/libs/log" | |||
"github.com/tendermint/tendermint/privval" | |||
"github.com/tendermint/tendermint/tools/tm-signer-harness/internal" | |||
"github.com/tendermint/tendermint/version" | |||
) | |||
const ( | |||
defaultAcceptRetries = 100 | |||
defaultBindAddr = "tcp://127.0.0.1:0" | |||
defaultAcceptDeadline = 1 | |||
defaultConnDeadline = 3 | |||
defaultExtractKeyOutput = "./signing.key" | |||
) | |||
var defaultTMHome string | |||
var logger = log.MustNewDefaultLogger(log.LogFormatPlain, log.LogLevelInfo, false) | |||
// Command line flags | |||
var ( | |||
flagAcceptRetries int | |||
flagBindAddr string | |||
flagTMHome string | |||
flagKeyOutputPath string | |||
) | |||
// Command line commands | |||
var ( | |||
rootCmd *flag.FlagSet | |||
runCmd *flag.FlagSet | |||
extractKeyCmd *flag.FlagSet | |||
versionCmd *flag.FlagSet | |||
) | |||
func init() { | |||
rootCmd = flag.NewFlagSet("root", flag.ExitOnError) | |||
rootCmd.Usage = func() { | |||
fmt.Println(`Remote signer test harness for Tendermint. | |||
Usage: | |||
tm-signer-harness <command> [flags] | |||
Available Commands: | |||
extract_key Extracts a signing key from a local Tendermint instance | |||
help Help on the available commands | |||
run Runs the test harness | |||
version Display version information and exit | |||
Use "tm-signer-harness help <command>" for more information about that command.`) | |||
fmt.Println("") | |||
} | |||
hd, err := os.UserHomeDir() | |||
if err != nil { | |||
fmt.Println("The UserHomeDir is not defined, setting the default TM Home PATH to \"~/.tendermint\"") | |||
defaultTMHome = "~/.tendermint" | |||
} else { | |||
defaultTMHome = fmt.Sprintf("%s/.tendermint", hd) | |||
} | |||
runCmd = flag.NewFlagSet("run", flag.ExitOnError) | |||
runCmd.IntVar(&flagAcceptRetries, | |||
"accept-retries", | |||
defaultAcceptRetries, | |||
"The number of attempts to listen for incoming connections") | |||
runCmd.StringVar(&flagBindAddr, "addr", defaultBindAddr, "Bind to this address for the testing") | |||
runCmd.StringVar(&flagTMHome, "tmhome", defaultTMHome, "Path to the Tendermint home directory") | |||
runCmd.Usage = func() { | |||
fmt.Println(`Runs the remote signer test harness for Tendermint. | |||
Usage: | |||
tm-signer-harness run [flags] | |||
Flags:`) | |||
runCmd.PrintDefaults() | |||
fmt.Println("") | |||
} | |||
extractKeyCmd = flag.NewFlagSet("extract_key", flag.ExitOnError) | |||
extractKeyCmd.StringVar(&flagKeyOutputPath, | |||
"output", | |||
defaultExtractKeyOutput, | |||
"Path to which signing key should be written") | |||
extractKeyCmd.StringVar(&flagTMHome, "tmhome", defaultTMHome, "Path to the Tendermint home directory") | |||
extractKeyCmd.Usage = func() { | |||
fmt.Println(`Extracts a signing key from a local Tendermint instance for use in the remote | |||
signer under test. | |||
Usage: | |||
tm-signer-harness extract_key [flags] | |||
Flags:`) | |||
extractKeyCmd.PrintDefaults() | |||
fmt.Println("") | |||
} | |||
versionCmd = flag.NewFlagSet("version", flag.ExitOnError) | |||
versionCmd.Usage = func() { | |||
fmt.Println(` | |||
Prints the Tendermint version for which this remote signer harness was built. | |||
Usage: | |||
tm-signer-harness version`) | |||
fmt.Println("") | |||
} | |||
} | |||
func runTestHarness(ctx context.Context, acceptRetries int, bindAddr, tmhome string) { | |||
tmhome = internal.ExpandPath(tmhome) | |||
cfg := internal.TestHarnessConfig{ | |||
BindAddr: bindAddr, | |||
KeyFile: filepath.Join(tmhome, "config", "priv_validator_key.json"), | |||
StateFile: filepath.Join(tmhome, "data", "priv_validator_state.json"), | |||
GenesisFile: filepath.Join(tmhome, "config", "genesis.json"), | |||
AcceptDeadline: time.Duration(defaultAcceptDeadline) * time.Second, | |||
AcceptRetries: acceptRetries, | |||
ConnDeadline: time.Duration(defaultConnDeadline) * time.Second, | |||
SecretConnKey: ed25519.GenPrivKey(), | |||
ExitWhenComplete: true, | |||
} | |||
harness, err := internal.NewTestHarness(ctx, logger, cfg) | |||
if err != nil { | |||
logger.Error(err.Error()) | |||
if therr, ok := err.(*internal.TestHarnessError); ok { | |||
os.Exit(therr.Code) | |||
} | |||
os.Exit(internal.ErrOther) | |||
} | |||
harness.Run() | |||
} | |||
func extractKey(tmhome, outputPath string) { | |||
keyFile := filepath.Join(internal.ExpandPath(tmhome), "config", "priv_validator_key.json") | |||
stateFile := filepath.Join(internal.ExpandPath(tmhome), "data", "priv_validator_state.json") | |||
fpv, err := privval.LoadFilePV(keyFile, stateFile) | |||
if err != nil { | |||
logger.Error("Can't load file pv", "err", err) | |||
os.Exit(1) | |||
} | |||
pkb := []byte(fpv.Key.PrivKey.(ed25519.PrivKey)) | |||
if err := os.WriteFile(internal.ExpandPath(outputPath), pkb[:32], 0600); err != nil { | |||
logger.Info("Failed to write private key", "output", outputPath, "err", err) | |||
os.Exit(1) | |||
} | |||
logger.Info("Successfully wrote private key", "output", outputPath) | |||
} | |||
func main() { | |||
ctx, cancel := context.WithCancel(context.Background()) | |||
defer cancel() | |||
if err := rootCmd.Parse(os.Args[1:]); err != nil { | |||
fmt.Printf("Error parsing flags: %v\n", err) | |||
os.Exit(1) | |||
} | |||
if rootCmd.NArg() == 0 || (rootCmd.NArg() == 1 && rootCmd.Arg(0) == "help") { | |||
rootCmd.Usage() | |||
os.Exit(0) | |||
} | |||
switch rootCmd.Arg(0) { | |||
case "help": | |||
switch rootCmd.Arg(1) { | |||
case "run": | |||
runCmd.Usage() | |||
case "extract_key": | |||
extractKeyCmd.Usage() | |||
case "version": | |||
versionCmd.Usage() | |||
default: | |||
fmt.Printf("Unrecognized command: %s\n", rootCmd.Arg(1)) | |||
os.Exit(1) | |||
} | |||
case "run": | |||
if err := runCmd.Parse(os.Args[2:]); err != nil { | |||
fmt.Printf("Error parsing flags: %v\n", err) | |||
os.Exit(1) | |||
} | |||
runTestHarness(ctx, flagAcceptRetries, flagBindAddr, flagTMHome) | |||
case "extract_key": | |||
if err := extractKeyCmd.Parse(os.Args[2:]); err != nil { | |||
fmt.Printf("Error parsing flags: %v\n", err) | |||
os.Exit(1) | |||
} | |||
extractKey(flagTMHome, flagKeyOutputPath) | |||
case "version": | |||
fmt.Println(version.TMVersion) | |||
default: | |||
fmt.Printf("Unrecognized command: %s\n", flag.Arg(0)) | |||
os.Exit(1) | |||
} | |||
} |