publish threshold signer code

This commit is contained in:
Roman Shtylman 2020-10-13 17:16:35 -07:00
parent cb67dad18d
commit 8ce1083293
No known key found for this signature in database
GPG Key ID: 72ED6A839D752787
21 changed files with 2265 additions and 31 deletions

View File

@ -2,11 +2,14 @@ GOLINT:=$(shell go list -f {{.Target}} golang.org/x/lint/golint)
all: build
build: build/signer
build: build/signer build/key2shares
build/signer: cmd/signer/main.go $(wildcard internal/**/*.go)
CGO_ENABLED=0 go build -o ./build/signer ${gobuild_flags} ./cmd/signer
build/key2shares: cmd/key2shares/main.go $(wildcard internal/**/*.go)
CGO_ENABLED=0 go build -o ./build/key2shares ${gobuild_flags} ./cmd/key2shares
lint: tools
@$(GOLINT) -set_exit_status ./...

View File

@ -1,26 +1,61 @@
# Tendermint Validator
A lightweight single key tendermint validator for sentry nodes.
A [multi-party-computation](https://en.wikipedia.org/wiki/Secure_multi-party_computation) signing service for Tendermint nodes using threshold Ed25519 signatures.
## Design
A lightweight alternative to using a full node instance for validating blocks. The validator is able to connect to any number of sentry nodes and will sign blocks provided by the nodes. The validator maintains a watermark file to protect against double signing.
Validator operators for tendermint chains balance operational and risk tradeoffs to avoid penalties via slashing for liveliness faults or double signing blocks.
Traditional high-availability systems where the keys exist on hot spares risk double signing if there are failover detection bugs. Low-availability systems, or manual failover, risk downtime if manual intervention cannot respond in a timely manner.
Multi-party computation using threshold signatures is able to provide high-availability while maintaining high security and avoiding double signing via failover detection bugs.
## Pre-requisites
Before starting, please make sure to fully understand node and validator requirements and operation for your particular network and chain.
Before starting, please make sure to fully understand node and validator requirements and operation for your particular network and chain. Operation of the mpc tendermint validator assumes prior and detailed knowledge of these aspects.
## Setup
_The security of any key material is outside the scope of this guide. At a minimum we recommend performing key material steps on airgapped computers and using your audited security procedures._
### Setup Validator Instance
### Create Shares
Configure the instance with a [toml](https://github.com/toml-lang/toml) file. Below is a sample configuration.
Use the `key2shares` utility to split a single validator key into participant shares. Prior to splitting a key, decide on the total number of share holders and how many (threshold) are required to produce a valid signature.
Here we split an ed25519 validating key into 3 shares with a threshold of 2. Together, any 2 key shares can produce a valid signature but no single share can produce a valid signature for our initial key.
```bash
key2shares --total 3 --threshold 2 /path/to/priv_validator_key.json
```
`key2shares` writes the shares to the current working directory.
The above example would create 3 output files:
```
private_share_1.json
private_share_2.json
private_share_3.json
```
The `.json` files contain the private shares and new private RSA keys, one per party, as well as the public RSA keys of the other cosigners.
_The RSA keys are generated by key2shares and used to secure party-to-party communication._
### Setup Validator Instances
Each private share is installed to a separate tendermint mpc validator instance.
_The specifics of instance setup and security are outside the scope of this guide, but we recommend practices like geographic distribution of instances and limiting physical and remote instance access as appropriate._
Each instance has a [toml](https://github.com/toml-lang/toml) configuration file. Below is a sample file corresponding to instance `1`.
```toml
# Path to priv validator key json file
key_file = "/path/to/priv_validator_key.json"
mode = "mpc"
# Each validator instance has its own private share.
# Avoid putting more than one share per instance.
key_file = "/path/to/private_share_1.json"
# The state directory stores watermarks for double signing protection.
# Each validator instance maintains a watermark.
@ -29,8 +64,28 @@ state_dir = "/path/to/state/dir"
# The network chain id for your p2p nodes
chain_id = "chain-id-here"
# The required number of participant share signatures.
# This must match the `--threshold` value specified during key2shares
cosigner_threshold = 2
# IP address and port for receiving communication from other validator instances.
# The validator instances must communicate during the signing process.
cosigner_listen_address = "tcp://0.0.0.0:1234"
# Each validator peer appears in a `cosigner` section.
# This sample file is for validator ID 1, so we configure sections for peers 2 and 3.
[[cosigner]]
# The ID of this peer, these must match the key IDs.
id = 2
# The IP address and port for communication with this peer
remote_address = "tcp://2.2.2.2:1234"
[[cosigner]]
id = 3
remote_address = "tcp://3.3.3.3:1234"
# Configure any number of p2p network nodes.
# We recommend at least 2 nodes for redundancy.
# We recommend at least 2 nodes per cosigner for redundancy.
[[node]]
address = "tcp://<node-a ip>:1234"
@ -38,11 +93,13 @@ address = "tcp://<node-a ip>:1234"
address = "tcp://<node-b ip>:1234"
```
Configuration for instances `2` and `3` would be similar. The `cosigner` sections would contain the respective peers, and the `node` sections would contain nodes for the cosigners.
## Configure p2p network nodes
Validators are not directly connected to the p2p network nor do they store chain and application state. They rely on nodes to receive blocks from the p2p network, make signing requests, and relay the signed blocks back to the p2p network.
Mpc validators are not directly connected to the p2p network nor do they store chain and application state. They rely on nodes to receive blocks from the p2p network, make signing requests, and relay the signed blocks back to the p2p network.
To make a node available as a relay for a validator, find the `priv_validator_laddr` (or equivalent) configuration item in your node's configuration file. Change this value, to accept connections on an IP address and port of your choosing.
To make a node available as a relay for an mpc validator, find the `priv_validator_laddr` (or equivalent) configuration item in your node's configuration file. Change this value, to accept connections on an IP address and port of your choosing.
```diff
# TCP or UNIX socket address for Tendermint to listen on for
@ -57,7 +114,7 @@ _We recommend hosting nodes on separate and isolated infrastructure from your va
## Launch validator
Once your validator instance and node is configured, you can launch the signer.
Once your validator instance and node is configured, you can launch the signer. When `threshold` signers are online, they will start signing block requests from their network nodes.
```bash
signer --config /path/to/config.toml
@ -80,3 +137,6 @@ software or this license, under any kind of legal claim.
- https://docs.tendermint.com/master/tendermint-core/validators.html
- https://hub.cosmos.network/master/validators/overview.html
- [Provably Secure Distributed Schnorr Signatures
and a (t, n) Threshold Scheme for Implicit
Certificates](http://cacr.uwaterloo.ca/techreports/2001/corr2001-13.ps)

101
cmd/key2shares/main.go Normal file
View File

@ -0,0 +1,101 @@
package main
import (
"crypto/rand"
"crypto/rsa"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"log"
"tendermint-signer/internal/signer"
amino "github.com/tendermint/go-amino"
"github.com/tendermint/tendermint/crypto/ed25519"
cryptoAmino "github.com/tendermint/tendermint/crypto/encoding/amino"
cmn "github.com/tendermint/tendermint/libs/common"
"github.com/tendermint/tendermint/privval"
tsed25519 "gitlab.com/polychainlabs/threshold-ed25519/pkg"
)
func main() {
var cdc = amino.NewCodec()
cryptoAmino.RegisterAmino(cdc)
var threshold = flag.Int("threshold", 2, "the number of shares required to produce a valid signature")
var total = flag.Int("total", 2, "the total number of shareholders")
flag.Parse()
if len(flag.Args()) != 1 {
log.Fatal("positional argument priv_validator_key.json is required")
}
keyFilePath := flag.Args()[0]
keyJSONBytes, err := ioutil.ReadFile(keyFilePath)
if err != nil {
cmn.Exit(err.Error())
}
pvKey := privval.FilePVKey{}
err = cdc.UnmarshalJSON(keyJSONBytes, &pvKey)
if err != nil {
cmn.Exit(fmt.Sprintf("Error reading PrivValidator key from %v: %v\n", keyFilePath, err))
}
privKeyBytes := [64]byte{}
// extract the raw private key bytes from the loaded key
// we need this to compute the expanded secret
switch ed25519Key := pvKey.PrivKey.(type) {
case ed25519.PrivKeyEd25519:
if len(ed25519Key) != len(privKeyBytes) {
panic("Key length inconsistency")
}
copy(privKeyBytes[:], ed25519Key[:])
break
default:
panic("Not an ed25519 private key")
}
// generate shares from secret
shares := tsed25519.DealShares(tsed25519.ExpandSecret(privKeyBytes[:32]), uint8(*threshold), uint8(*total))
// generate all rsa keys
rsaKeys := make([]*rsa.PrivateKey, len(shares))
pubkeys := make([]*rsa.PublicKey, len(shares))
for idx := range shares {
bitSize := 4096
rsaKey, err := rsa.GenerateKey(rand.Reader, bitSize)
if err != nil {
panic(err)
}
rsaKeys[idx] = rsaKey
pubkeys[idx] = &rsaKey.PublicKey
}
// write shares and keys to private share files
for idx, share := range shares {
shareID := idx + 1
privateFilename := fmt.Sprintf("private_share_%d.json", shareID)
cosignerKey := signer.CosignerKey{
PubKey: pvKey.PubKey,
ShareKey: share,
ID: shareID,
RSAKey: *rsaKeys[idx],
CosignerKeys: pubkeys,
}
jsonBytes, err := json.MarshalIndent(&cosignerKey, "", " ")
if err != nil {
panic(err)
}
err = ioutil.WriteFile(privateFilename, jsonBytes, 0644)
if err != nil {
panic(err)
}
fmt.Printf("Created Share %d\n", shareID)
}
}

View File

@ -15,6 +15,7 @@ import (
cmn "github.com/tendermint/tendermint/libs/common"
tmlog "github.com/tendermint/tendermint/libs/log"
"github.com/tendermint/tendermint/privval"
"github.com/tendermint/tendermint/types"
)
func fileExists(filename string) bool {
@ -44,6 +45,7 @@ func main() {
logger.Info(
"Tendermint Validator",
"mode", config.Mode,
"priv-key", config.PrivValKeyFile,
"priv-state-dir", config.PrivValStateDir,
)
@ -53,23 +55,119 @@ func main() {
// services to stop on shutdown
var services []cmn.Service
var pv types.PrivValidator
chainID := config.ChainID
if chainID == "" {
log.Fatal("chain_id option is required")
}
stateFile := path.Join(config.PrivValStateDir, fmt.Sprintf("%s_priv_validator_state.json", chainID))
if config.Mode == "single" {
stateFile := path.Join(config.PrivValStateDir, fmt.Sprintf("%s_priv_validator_state.json", chainID))
if !fileExists(stateFile) {
log.Fatalf("State file missing: %s\n", stateFile)
var val types.PrivValidator
if fileExists(stateFile) {
val = privval.LoadFilePV(config.PrivValKeyFile, stateFile)
} else {
logger.Info("Initializing empty state file")
val = privval.LoadFilePVEmptyState(config.PrivValKeyFile, stateFile)
}
pv = &signer.PvGuard{PrivValidator: val}
} else if config.Mode == "mpc" {
if config.CosignerThreshold == 0 {
log.Fatal("The `cosigner_threshold` option is required in `threshold` mode")
}
if config.ListenAddress == "" {
log.Fatal("The cosigner_listen_address option is required in `threshold` mode")
}
key, err := signer.LoadCosignerKey(config.PrivValKeyFile)
if err != nil {
panic(err)
}
// ok to auto initialize on disk since the cosigner share is the one that actually
// protects against double sign - this exists as a cache for the final signature
stateFile := path.Join(config.PrivValStateDir, fmt.Sprintf("%s_priv_validator_state.json", chainID))
signState, err := signer.LoadOrCreateSignState(stateFile)
if err != nil {
panic(err)
}
// state for our cosigner share
// Not automatically initialized on disk to avoid double sign risk
shareStateFile := path.Join(config.PrivValStateDir, fmt.Sprintf("%s_share_sign_state.json", chainID))
shareSignState, err := signer.LoadSignState(shareStateFile)
if err != nil {
panic(err)
}
cosigners := []signer.Cosigner{}
remoteCosigners := []signer.RemoteCosigner{}
// add ourselves as a peer so localcosigner can handle GetEphSecPart requests
peers := []signer.CosignerPeer{signer.CosignerPeer{
ID: key.ID,
PublicKey: key.RSAKey.PublicKey,
}}
for _, cosignerConfig := range config.Cosigners {
cosigner := signer.NewRemoteCosigner(cosignerConfig.ID, cosignerConfig.Address)
cosigners = append(cosigners, cosigner)
remoteCosigners = append(remoteCosigners, *cosigner)
if cosignerConfig.ID < 1 || cosignerConfig.ID > len(key.CosignerKeys) {
log.Fatalf("Unexpected cosigner ID %d", cosignerConfig.ID)
}
pubKey := key.CosignerKeys[cosignerConfig.ID-1]
peers = append(peers, signer.CosignerPeer{
ID: cosigner.GetID(),
PublicKey: *pubKey,
})
}
total := len(config.Cosigners) + 1
localCosignerConfig := signer.LocalCosignerConfig{
CosignerKey: key,
SignState: &shareSignState,
RsaKey: key.RSAKey,
Peers: peers,
Total: uint8(total),
Threshold: uint8(config.CosignerThreshold),
}
localCosigner := signer.NewLocalCosigner(localCosignerConfig)
val := signer.NewThresholdValidator(&signer.ThresholdValidatorOpt{
Pubkey: key.PubKey,
Threshold: config.CosignerThreshold,
SignState: signState,
Cosigner: localCosigner,
Peers: cosigners,
})
rpcServerConfig := signer.CosignerRpcServerConfig{
Logger: logger,
ListenAddress: config.ListenAddress,
Cosigner: localCosigner,
Peers: remoteCosigners,
}
rpcServer := signer.NewCosignerRpcServer(&rpcServerConfig)
rpcServer.Start()
services = append(services, rpcServer)
pv = &signer.PvGuard{PrivValidator: val}
} else {
log.Fatalf("Unsupported mode: %s", config.Mode)
}
val := privval.LoadFilePV(config.PrivValKeyFile, stateFile)
pv := &signer.PvGuard{PrivValidator: val}
for _, node := range config.Nodes {
dialer := net.Dialer{Timeout: 30 * time.Second}
signer := signer.NewNodeClient(node.Address, logger, config.ChainID, pv, dialer)
signer := signer.NewReconnRemoteSigner(node.Address, logger, config.ChainID, pv, dialer)
err := signer.Start()
if err != nil {

6
go.mod
View File

@ -11,11 +11,15 @@ require (
github.com/gogo/protobuf v1.2.1 // indirect
github.com/golang/protobuf v1.3.2 // indirect
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/magiconair/properties v1.8.0 // indirect
github.com/pkg/errors v0.8.1 // indirect
github.com/stretchr/testify v1.3.0 // indirect
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect
github.com/stretchr/testify v1.3.0
github.com/tendermint/go-amino v0.14.1
github.com/tendermint/tendermint v0.31.5
gitlab.com/polychainlabs/edwards25519 v0.0.0-20200206000358-2272e01758fb
gitlab.com/polychainlabs/threshold-ed25519 v0.0.0-20200221030822-1c35a36a51c1
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 // indirect
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 // indirect
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 // indirect

15
go.sum
View File

@ -41,6 +41,8 @@ github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck=
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
@ -58,6 +60,8 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ=
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@ -65,6 +69,10 @@ github.com/tendermint/go-amino v0.14.1 h1:o2WudxNfdLNBwMyl2dqOJxiro5rfrEaU0Ugs6o
github.com/tendermint/go-amino v0.14.1/go.mod h1:i/UKE5Uocn+argJJBb12qTZsCDBcAYMbR92AaJVmKso=
github.com/tendermint/tendermint v0.31.5 h1:vTet8tCq3B9/J9Yo11dNZ8pOB7NtSy++bVSfkP4KzR4=
github.com/tendermint/tendermint v0.31.5/go.mod h1:ymcPyWblXCplCPQjbOYbrF1fWnpslATMVqiGgWbZrlc=
gitlab.com/polychainlabs/edwards25519 v0.0.0-20200206000358-2272e01758fb h1:AdKE1d1TA0YYg8FDChkZP/nFOQDOMOUxLYzac/eVYUU=
gitlab.com/polychainlabs/edwards25519 v0.0.0-20200206000358-2272e01758fb/go.mod h1:3KUw8eOHmnX0bANpqw52h/JNsGJxfVIFyhIXH5zeprc=
gitlab.com/polychainlabs/threshold-ed25519 v0.0.0-20200221030822-1c35a36a51c1 h1:zdHgoRHlPqZfRMew5hX7aPZuxNURbcobivw+4gMvipk=
gitlab.com/polychainlabs/threshold-ed25519 v0.0.0-20200221030822-1c35a36a51c1/go.mod h1:jrwPQCmLa5H51H4qgUAZcigi2rz2kFLVEkGe3eNlzHw=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
@ -73,6 +81,9 @@ golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3 h1:x/bBzNauLQAlE3fLku/xy92Y8QwKX5HZymrMz2IiKFc=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367 h1:0IiAsCRByjO2QjX7ZPkw5oU9x+n1YqRL802rjC0c3Aw=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -82,6 +93,7 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwL
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -98,6 +110,9 @@ golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b h1:qMK98NmNCRVDIYFycQ5yVRkvgDUFfdP8Ip4KqmDEB7g=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7 h1:EBZoQjiKKPaLbPrbpssUfuHtwM6KV/vb4U85g/cigFY=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=

View File

@ -16,6 +16,7 @@ type CosignerConfig struct {
}
type Config struct {
Mode string `toml:"mode"`
PrivValKeyFile string `toml:"key_file"`
PrivValStateDir string `toml:"state_dir"`
ChainID string `toml:"chain_id"`
@ -28,6 +29,9 @@ type Config struct {
func LoadConfigFromFile(file string) (Config, error) {
var config Config
// default mode is mpc
config.Mode = "mpc"
reader, err := os.Open(file)
if err != nil {
return config, err

View File

@ -0,0 +1,72 @@
package signer
import "time"
// CosignerSignRequest is sent to a co-signer to obtain their signature for the SignBytes
// The SignBytes should be a serialized block
type CosignerSignRequest struct {
SignBytes []byte
}
type CosignerSignResponse struct {
EphemeralPublic []byte
Timestamp time.Time
Signature []byte
}
type CosignerGetEphemeralSecretPartRequest struct {
ID int
Height int64
Round int64
Step int8
}
type CosignerHasEphemeralSecretPartRequest struct {
ID int
Height int64
Round int64
Step int8
}
type CosignerHasEphemeralSecretPartResponse struct {
Exists bool
EphemeralSecretPublicKey []byte
}
type CosignerGetEphemeralSecretPartResponse struct {
SourceID int
SourceEphemeralSecretPublicKey []byte
EncryptedSharePart []byte
SourceSig []byte
}
type CosignerSetEphemeralSecretPartRequest struct {
SourceID int
SourceEphemeralSecretPublicKey []byte
Height int64
Round int64
Step int8
EncryptedSharePart []byte
SourceSig []byte
}
// Cosigner interface is a set of methods for an m-of-n threshold signature.
// This interface abstracts the underlying key storage and management
type Cosigner interface {
// Get the ID of the cosigner
// The ID is the shamir index: 1, 2, etc...
GetID() int
// Get the ephemeral secret part for an ephemeral share
// The ephemeral secret part is encrypted for the receiver
GetEphemeralSecretPart(req CosignerGetEphemeralSecretPartRequest) (CosignerGetEphemeralSecretPartResponse, error)
// Store an ephemeral secret share part provided by another cosigner
SetEphemeralSecretPart(req CosignerSetEphemeralSecretPartRequest) error
// Query whether the cosigner has an ehpemeral secret part set
HasEphemeralSecretPart(req CosignerHasEphemeralSecretPartRequest) (CosignerHasEphemeralSecretPartResponse, error)
// Sign the requested bytes
Sign(req CosignerSignRequest) (CosignerSignResponse, error)
}

View File

@ -0,0 +1,105 @@
package signer
import (
"crypto/rsa"
"crypto/x509"
"encoding/json"
"io/ioutil"
"github.com/tendermint/tendermint/crypto"
tmed25519 "github.com/tendermint/tendermint/crypto/ed25519"
)
// CosignerKey is a single key for an m-of-n threshold signer.
type CosignerKey struct {
PubKey crypto.PubKey `json:"pub_key"`
ShareKey []byte `json:"secret_share"`
RSAKey rsa.PrivateKey `json:"rsa_key"`
ID int `json:"id"`
CosignerKeys []*rsa.PublicKey `json:"rsa_pubs"`
}
func (cosignerKey *CosignerKey) MarshalJSON() ([]byte, error) {
type Alias CosignerKey
// marshal our private key and all public keys
privateBytes := x509.MarshalPKCS1PrivateKey(&cosignerKey.RSAKey)
rsaPubKeysBytes := make([][]byte, 0)
for _, pubKey := range cosignerKey.CosignerKeys {
publicBytes := x509.MarshalPKCS1PublicKey(pubKey)
rsaPubKeysBytes = append(rsaPubKeysBytes, publicBytes)
}
pubkey, err := cdc.MarshalBinaryBare(cosignerKey.PubKey)
if err != nil {
return nil, err
}
return json.Marshal(&struct {
RSAKey []byte `json:"rsa_key"`
Pubkey []byte `json:"pub_key"`
CosignerKeys [][]byte `json:"rsa_pubs"`
*Alias
}{
Pubkey: pubkey,
RSAKey: privateBytes,
CosignerKeys: rsaPubKeysBytes,
Alias: (*Alias)(cosignerKey),
})
}
func (cosignerKey *CosignerKey) UnmarshalJSON(data []byte) error {
type Alias CosignerKey
aux := &struct {
RSAKey []byte `json:"rsa_key"`
Pubkey []byte `json:"pub_key"`
CosignerKeys [][]byte `json:"rsa_pubs"`
*Alias
}{
Alias: (*Alias)(cosignerKey),
}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
privateKey, err := x509.ParsePKCS1PrivateKey(aux.RSAKey)
if err != nil {
return err
}
var pubkey tmed25519.PubKeyEd25519
err = cdc.UnmarshalBinaryBare(aux.Pubkey, &pubkey)
if err != nil {
return err
}
// unmarshal the public key bytes for each cosigner
cosignerKey.CosignerKeys = make([]*rsa.PublicKey, 0)
for _, bytes := range aux.CosignerKeys {
cosignerRsaPubkey, err := x509.ParsePKCS1PublicKey(bytes)
if err != nil {
return err
}
cosignerKey.CosignerKeys = append(cosignerKey.CosignerKeys, cosignerRsaPubkey)
}
cosignerKey.RSAKey = *privateKey
cosignerKey.PubKey = pubkey
return nil
}
// LoadCosignerKey loads a CosignerKey from file.
func LoadCosignerKey(file string) (CosignerKey, error) {
pvKey := CosignerKey{}
keyJSONBytes, err := ioutil.ReadFile(file)
if err != nil {
return pvKey, err
}
err = json.Unmarshal(keyJSONBytes, &pvKey)
if err != nil {
return pvKey, err
}
return pvKey, nil
}

View File

@ -0,0 +1,16 @@
package signer
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestLoadCosignerKey(test *testing.T) {
key, err := LoadCosignerKey("../../test/cosigner-key.json")
require.NoError(test, err)
require.Equal(test, key.ID, 3)
// public key from cosigner pubs array should match public key from our private key
require.Equal(test, &key.RSAKey.PublicKey, key.CosignerKeys[key.ID-1])
}

View File

@ -0,0 +1,229 @@
package signer
import (
"context"
"net"
"net/http"
"sync"
"time"
cmn "github.com/tendermint/tendermint/libs/common"
"github.com/tendermint/tendermint/libs/log"
server "github.com/tendermint/tendermint/rpc/lib/server"
rpc_types "github.com/tendermint/tendermint/rpc/lib/types"
)
type RpcSignRequest struct {
SignBytes []byte
}
type RpcSignResponse struct {
Timestamp time.Time
Signature []byte
}
type RpcGetEphemeralSecretPartRequest struct {
ID int
Height int64
Round int64
Step int8
}
type RpcGetEphemeralSecretPartResponse struct {
SourceID int
SourceEphemeralSecretPublicKey []byte
EncryptedSharePart []byte
SourceSig []byte
}
type CosignerRpcServerConfig struct {
Logger log.Logger
ListenAddress string
Cosigner Cosigner
Peers []RemoteCosigner
}
// CosignerRpcServer responds to rpc sign requests using a cosigner instance
type CosignerRpcServer struct {
cmn.BaseService
logger log.Logger
listenAddress string
listener net.Listener
cosigner Cosigner
peers []RemoteCosigner
}
// NewCosignerRpcServer instantiates a local cosigner with the specified key and sign state
func NewCosignerRpcServer(config *CosignerRpcServerConfig) *CosignerRpcServer {
cosignerRpcServer := &CosignerRpcServer{
cosigner: config.Cosigner,
listenAddress: config.ListenAddress,
peers: config.Peers,
logger: config.Logger,
}
cosignerRpcServer.BaseService = *cmn.NewBaseService(config.Logger, "CosignerRpcServer", cosignerRpcServer)
return cosignerRpcServer
}
// OnStart starts the rpm server to respond to remote CosignerSignRequests
func (rpcServer *CosignerRpcServer) OnStart() error {
proto, address := cmn.ProtocolAndAddress(rpcServer.listenAddress)
lis, err := net.Listen(proto, address)
if err != nil {
return err
}
rpcServer.listener = lis
routes := map[string]*server.RPCFunc{
"Sign": server.NewRPCFunc(rpcServer.rpcSignRequest, "arg"),
"GetEphemeralSecretPart": server.NewRPCFunc(rpcServer.rpcGetEphemeralSecretPart, "arg"),
}
mux := http.NewServeMux()
server.RegisterRPCFuncs(mux, routes, cdc, log.NewFilter(rpcServer.Logger, log.AllowError()))
tcpLogger := rpcServer.Logger.With("socket", "tcp")
tcpLogger = log.NewFilter(tcpLogger, log.AllowError())
config := server.DefaultConfig()
go func() {
defer lis.Close()
server.StartHTTPServer(lis, mux, tcpLogger, config)
}()
return nil
}
func (rpcServer *CosignerRpcServer) Addr() net.Addr {
if rpcServer.listener == nil {
return nil
}
return rpcServer.listener.Addr()
}
func (rpcServer *CosignerRpcServer) rpcSignRequest(ctx *rpc_types.Context, req RpcSignRequest) (*RpcSignResponse, error) {
response := &RpcSignResponse{}
height, round, step, err := UnpackHRS(req.SignBytes)
if err != nil {
return response, err
}
wg := sync.WaitGroup{}
wg.Add(len(rpcServer.peers))
// ping peers for our ephemeral share part
for _, peer := range rpcServer.peers {
request := func(peer RemoteCosigner) {
// need to do these requests in parallel..!!
// RPC requests are blocking
// to prevent it from hanging our process indefinitely, we use a timeout context and a goroutine
partReqCtx, partReqCtxCancel := context.WithTimeout(context.Background(), time.Second)
go func() {
partRequest := CosignerGetEphemeralSecretPartRequest{
ID: rpcServer.cosigner.GetID(),
Height: height,
Round: round,
Step: step,
}
// if we already have an ephemeral secret part for this HRS, we don't need to re-query for it
hasResp, err := rpcServer.cosigner.HasEphemeralSecretPart(CosignerHasEphemeralSecretPartRequest{
ID: peer.GetID(),
Height: height,
Round: round,
Step: step,
})
if err != nil {
rpcServer.logger.Error("HasEphemeralSecretPart req error", "error", err)
return
}
if hasResp.Exists {
partReqCtxCancel()
return
}
partResponse, err := peer.GetEphemeralSecretPart(partRequest)
if err != nil {
rpcServer.logger.Error("GetEphemeralSecretPart req error", "error", err)
return
}
// no need to contine if timed out
select {
case <-partReqCtx.Done():
return
default:
}
defer partReqCtxCancel()
// set the share part from the response
err = rpcServer.cosigner.SetEphemeralSecretPart(CosignerSetEphemeralSecretPartRequest{
SourceID: partResponse.SourceID,
SourceEphemeralSecretPublicKey: partResponse.SourceEphemeralSecretPublicKey,
EncryptedSharePart: partResponse.EncryptedSharePart,
Height: height,
Round: round,
Step: step,
SourceSig: partResponse.SourceSig,
})
if err != nil {
rpcServer.logger.Error("SetEphemeralSecretPart req error", "error", err)
}
}()
// wait for timeout or done
select {
case <-partReqCtx.Done():
}
wg.Done()
}
go request(peer)
}
wg.Wait()
// after getting any share parts we could, we sign
resp, err := rpcServer.cosigner.Sign(CosignerSignRequest{
SignBytes: req.SignBytes,
})
if err != nil {
return response, err
}
response.Timestamp = resp.Timestamp
response.Signature = resp.Signature
return response, nil
}
func (rpcServer *CosignerRpcServer) rpcGetEphemeralSecretPart(ctx *rpc_types.Context, req RpcGetEphemeralSecretPartRequest) (*RpcGetEphemeralSecretPartResponse, error) {
response := &RpcGetEphemeralSecretPartResponse{}
partResp, err := rpcServer.cosigner.GetEphemeralSecretPart(CosignerGetEphemeralSecretPartRequest{
ID: req.ID,
Height: req.Height,
Round: req.Round,
Step: req.Step,
})
if err != nil {
return response, nil
}
response.SourceID = partResp.SourceID
response.SourceEphemeralSecretPublicKey = partResp.SourceEphemeralSecretPublicKey
response.EncryptedSharePart = partResp.EncryptedSharePart
response.SourceSig = partResp.SourceSig
return response, nil
}

View File

@ -0,0 +1,106 @@
package signer
import (
"os"
"testing"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/libs/log"
"github.com/tendermint/tendermint/types"
)
type DummyCosigner struct{}
func (cosigner *DummyCosigner) GetID() int {
return 0
}
func (cosigner *DummyCosigner) Sign(signReq CosignerSignRequest) (CosignerSignResponse, error) {
return CosignerSignResponse{
Signature: []byte("foobar"),
}, nil
}
func (cosigner *DummyCosigner) GetEphemeralSecretPart(req CosignerGetEphemeralSecretPartRequest) (CosignerGetEphemeralSecretPartResponse, error) {
return CosignerGetEphemeralSecretPartResponse{
SourceID: 1,
SourceEphemeralSecretPublicKey: []byte("foo"),
EncryptedSharePart: []byte("bar"),
SourceSig: []byte("source sig"),
}, nil
}
func (cosigner *DummyCosigner) HasEphemeralSecretPart(req CosignerHasEphemeralSecretPartRequest) (CosignerHasEphemeralSecretPartResponse, error) {
return CosignerHasEphemeralSecretPartResponse{
Exists: false,
}, nil
}
func (cosigner *DummyCosigner) SetEphemeralSecretPart(req CosignerSetEphemeralSecretPartRequest) error {
return nil
}
func TestCosignerRpcServerSign(test *testing.T) {
dummyCosigner := &DummyCosigner{}
logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout))
config := CosignerRpcServerConfig{
Logger: logger,
ListenAddress: "tcp://0.0.0.0:0",
Cosigner: dummyCosigner,
}
rpcServer := NewCosignerRpcServer(&config)
rpcServer.Start()
// pack a vote into sign bytes
var vote types.CanonicalVote
vote.ChainID = "foobar"
vote.Height = 1
vote.Round = 0
vote.Type = types.PrevoteType
signBytes, err := cdc.MarshalBinaryLengthPrefixed(vote)
require.NoError(test, err)
remoteCosigner := NewRemoteCosigner(2, rpcServer.Addr().String())
resp, err := remoteCosigner.Sign(CosignerSignRequest{
SignBytes: signBytes,
})
require.NoError(test, err)
require.Equal(test, resp, CosignerSignResponse{
Signature: []byte("foobar"),
})
rpcServer.Stop()
}
func TestCosignerRpcServerGetEphemeralSecretPart(test *testing.T) {
dummyCosigner := &DummyCosigner{}
logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout))
config := CosignerRpcServerConfig{
Logger: logger,
ListenAddress: "tcp://0.0.0.0:0",
Cosigner: dummyCosigner,
}
rpcServer := NewCosignerRpcServer(&config)
rpcServer.Start()
remoteCosigner := NewRemoteCosigner(2, rpcServer.Addr().String())
resp, err := remoteCosigner.GetEphemeralSecretPart(CosignerGetEphemeralSecretPartRequest{})
require.NoError(test, err)
require.Equal(test, resp, CosignerGetEphemeralSecretPartResponse{
SourceID: 1,
SourceEphemeralSecretPublicKey: []byte("foo"),
EncryptedSharePart: []byte("bar"),
SourceSig: []byte("source sig"),
})
rpcServer.Stop()
}

View File

@ -0,0 +1,406 @@
package signer
import (
"bytes"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"errors"
"fmt"
"sync"
tm_ed25519 "github.com/tendermint/tendermint/crypto/ed25519"
"gitlab.com/polychainlabs/edwards25519"
tsed25519 "gitlab.com/polychainlabs/threshold-ed25519/pkg"
)
type HRSKey struct {
Height int64
Round int64
Step int8
}
// return true if we are less than the other key
func (hrsKey *HRSKey) Less(other HRSKey) bool {
if hrsKey.Height < other.Height {
return true
}
if hrsKey.Height > other.Height {
return false
}
// height is equal, check round
if hrsKey.Round < other.Round {
return true
}
if hrsKey.Round > other.Round {
return false
}
// round is equal, check step
if hrsKey.Step < other.Step {
return true
}
// everything is equal
return false
}
type CosignerPeer struct {
ID int
PublicKey rsa.PublicKey
}
type LocalCosignerConfig struct {
CosignerKey CosignerKey
SignState *SignState
RsaKey rsa.PrivateKey
Peers []CosignerPeer
Total uint8
Threshold uint8
}
type PeerMetadata struct {
Share []byte
EphemeralSecretPublicKey []byte
}
type HrsMetadata struct {
// need to be _total_ entries per player
Secret []byte
DealtShares []tsed25519.Scalar
Peers []PeerMetadata
}
// LocalCosigner responds to sign requests using their share key
// The cosigner maintains a watermark to avoid double-signing
//
// LocalCosigner signing is thread saafe
type LocalCosigner struct {
pubKeyBytes []byte
key CosignerKey
rsaKey rsa.PrivateKey
total uint8
threshold uint8
// stores the last sign state for a share we have fully signed
// incremented whenever we are asked to sign a share
lastSignState *SignState
// signing is thread safe
lastSignStateMutex sync.Mutex
// Height, Round, Step -> metadata
hrsMeta map[HRSKey]HrsMetadata
peers map[int]CosignerPeer
}
func NewLocalCosigner(cfg LocalCosignerConfig) *LocalCosigner {
cosigner := &LocalCosigner{
key: cfg.CosignerKey,
lastSignState: cfg.SignState,
rsaKey: cfg.RsaKey,
hrsMeta: make(map[HRSKey]HrsMetadata),
peers: make(map[int]CosignerPeer),
total: cfg.Total,
threshold: cfg.Threshold,
}
for _, peer := range cfg.Peers {
cosigner.peers[peer.ID] = peer
}
// cache the public key bytes for signing operations
switch ed25519Key := cosigner.key.PubKey.(type) {
case tm_ed25519.PubKeyEd25519:
cosigner.pubKeyBytes = make([]byte, len(ed25519Key))
copy(cosigner.pubKeyBytes[:], ed25519Key[:])
break
default:
panic("Not an ed25519 public key")
}
return cosigner
}
// GetID returns the id of the cosigner
// Implements Cosigner interface
func (cosigner *LocalCosigner) GetID() int {
return cosigner.key.ID
}
// Sign the sign request using the cosigner's share
// Return the signed bytes or an error
// Implements Cosigner interface
func (cosigner *LocalCosigner) Sign(req CosignerSignRequest) (CosignerSignResponse, error) {
cosigner.lastSignStateMutex.Lock()
defer cosigner.lastSignStateMutex.Unlock()
res := CosignerSignResponse{}
lss := cosigner.lastSignState
height, round, step, err := UnpackHRS(req.SignBytes)
if err != nil {
return res, err
}
sameHRS, err := lss.CheckHRS(height, round, step)
if err != nil {
return res, err
}
// If the HRS is the same the sign bytes may still differ by timestamp
// It is ok to re-sign a different timestamp if that is the only difference in the sign bytes
if sameHRS {
if bytes.Equal(req.SignBytes, lss.SignBytes) {
res.EphemeralPublic = lss.EphemeralPublic
res.Signature = lss.Signature
return res, nil
} else if _, ok := lss.OnlyDifferByTimestamp(req.SignBytes); !ok {
return res, errors.New("Mismatched data")
}
// saame HRS, and only differ by timestamp - ok to sign again
}
hrsKey := HRSKey{
Height: height,
Round: round,
Step: step,
}
meta, ok := cosigner.hrsMeta[hrsKey]
if !ok {
return res, errors.New("No metadata at HRS")
}
shareParts := make([]tsed25519.Scalar, 0)
publicKeys := make([]tsed25519.Element, 0)
// calculate secret and public keys
for _, peer := range meta.Peers {
if len(peer.Share) == 0 {
continue
}
shareParts = append(shareParts, peer.Share)
publicKeys = append(publicKeys, peer.EphemeralSecretPublicKey)
}
ephemeralShare := tsed25519.AddScalars(shareParts)
ephemeralPublic := tsed25519.AddElements(publicKeys)
// check bounds for ephemeral share to avoid passing out of bounds valids to SignWithShare
{
if len(ephemeralShare) != 32 {
return res, errors.New("Ephemeral share is out of bounds.")
}
var scalarBytes [32]byte
copy(scalarBytes[:], ephemeralShare)
if !edwards25519.ScMinimal(&scalarBytes) {
return res, errors.New("Ephemeral share is out of bounds.")
}
}
share := cosigner.key.ShareKey[:]
sig := tsed25519.SignWithShare(req.SignBytes, share, ephemeralShare, cosigner.pubKeyBytes, ephemeralPublic)
cosigner.lastSignState.Height = height
cosigner.lastSignState.Round = round
cosigner.lastSignState.Step = step
cosigner.lastSignState.EphemeralPublic = ephemeralPublic
cosigner.lastSignState.Signature = sig
cosigner.lastSignState.SignBytes = req.SignBytes
cosigner.lastSignState.Save()
for existingKey := range cosigner.hrsMeta {
// delete any HRS lower than our signed level
// we will not be providing parts for any lower HRS
if existingKey.Less(hrsKey) {
delete(cosigner.hrsMeta, existingKey)
}
}
res.EphemeralPublic = ephemeralPublic
res.Signature = sig
return res, nil
}
// Get the ephemeral secret part for an ephemeral share
// The ephemeral secret part is encrypted for the receiver
func (cosigner *LocalCosigner) GetEphemeralSecretPart(req CosignerGetEphemeralSecretPartRequest) (CosignerGetEphemeralSecretPartResponse, error) {
res := CosignerGetEphemeralSecretPartResponse{}
// protects the meta map
cosigner.lastSignStateMutex.Lock()
defer cosigner.lastSignStateMutex.Unlock()
hrsKey := HRSKey{
Height: req.Height,
Round: req.Round,
Step: req.Step,
}
meta, ok := cosigner.hrsMeta[hrsKey]
// generate metadata placeholder
if !ok {
secret := make([]byte, 32)
rand.Read(secret)
meta = HrsMetadata{
Secret: secret,
Peers: make([]PeerMetadata, cosigner.total),
}
// split this secret with shamirs
// !! dealt shares need to be saved because dealing produces different shares each time!
meta.DealtShares = tsed25519.DealShares(meta.Secret, cosigner.threshold, cosigner.total)
cosigner.hrsMeta[hrsKey] = meta
}
ourEphPublicKey := tsed25519.ScalarMultiplyBase(meta.Secret)
// set our values
meta.Peers[cosigner.key.ID-1].Share = meta.DealtShares[cosigner.key.ID-1]
meta.Peers[cosigner.key.ID-1].EphemeralSecretPublicKey = ourEphPublicKey
// grab the peer info for the ID being requested
peer, ok := cosigner.peers[req.ID]
if !ok {
return res, errors.New("Unknown peer ID")
}
sharePart := meta.DealtShares[req.ID-1]
// use RSA public to encrypt user's share part
encrypted, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, &peer.PublicKey, sharePart, nil)
if err != nil {
return res, err
}
res.SourceID = cosigner.key.ID
res.SourceEphemeralSecretPublicKey = ourEphPublicKey
res.EncryptedSharePart = encrypted
// sign the response payload with our private key
// cosigners can verify the signature to confirm sender validity
{
jsonBytes, err := cdc.MarshalJSON(res)
if err != nil {
return res, err
}
digest := sha256.Sum256(jsonBytes)
signature, err := rsa.SignPSS(rand.Reader, &cosigner.rsaKey, crypto.SHA256, digest[:], nil)
if err != nil {
return res, err
}
res.SourceSig = signature
}
return res, nil
}
func (cosigner *LocalCosigner) HasEphemeralSecretPart(req CosignerHasEphemeralSecretPartRequest) (CosignerHasEphemeralSecretPartResponse, error) {
res := CosignerHasEphemeralSecretPartResponse{
Exists: false,
}
// protects the meta map
cosigner.lastSignStateMutex.Lock()
defer cosigner.lastSignStateMutex.Unlock()
hrsKey := HRSKey{
Height: req.Height,
Round: req.Round,
Step: req.Step,
}
meta, ok := cosigner.hrsMeta[hrsKey]
if ok {
pub := meta.Peers[req.ID-1].EphemeralSecretPublicKey
if len(pub) > 0 {
res.Exists = true
res.EphemeralSecretPublicKey = pub
}
}
return res, nil
}
// Store an ephemeral secret share part provided by another cosigner
func (cosigner *LocalCosigner) SetEphemeralSecretPart(req CosignerSetEphemeralSecretPartRequest) error {
// Verify the source signature
{
if req.SourceSig == nil {
return errors.New("SourceSig field is required")
}
digestMsg := CosignerGetEphemeralSecretPartResponse{}
digestMsg.SourceID = req.SourceID
digestMsg.SourceEphemeralSecretPublicKey = req.SourceEphemeralSecretPublicKey
digestMsg.EncryptedSharePart = req.EncryptedSharePart
digestBytes, err := cdc.MarshalJSON(digestMsg)
if err != nil {
return err
}
digest := sha256.Sum256(digestBytes)
peer, ok := cosigner.peers[req.SourceID]
if !ok {
return fmt.Errorf("Unknown cosigner: %d", req.SourceID)
}
peerPub := peer.PublicKey
err = rsa.VerifyPSS(&peerPub, crypto.SHA256, digest[:], req.SourceSig, nil)
if err != nil {
return err
}
}
// protects the meta map
cosigner.lastSignStateMutex.Lock()
defer cosigner.lastSignStateMutex.Unlock()
hrsKey := HRSKey{
Height: req.Height,
Round: req.Round,
Step: req.Step,
}
meta, ok := cosigner.hrsMeta[hrsKey]
// generate metadata placeholder
if !ok {
secret := make([]byte, 32)
rand.Read(secret)
meta = HrsMetadata{
Secret: secret,
Peers: make([]PeerMetadata, cosigner.total),
}
meta.DealtShares = tsed25519.DealShares(meta.Secret, cosigner.threshold, cosigner.total)
cosigner.hrsMeta[hrsKey] = meta
}
// decrypt share
sharePart, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, &cosigner.rsaKey, req.EncryptedSharePart, nil)
if err != nil {
return err
}
// set slot
meta.Peers[req.SourceID-1].Share = sharePart
meta.Peers[req.SourceID-1].EphemeralSecretPublicKey = req.SourceEphemeralSecretPublicKey
return nil
}

View File

@ -0,0 +1,262 @@
package signer
import (
"crypto/rand"
"crypto/rsa"
"fmt"
"io/ioutil"
"os"
"testing"
"github.com/stretchr/testify/require"
tm_ed25519 "github.com/tendermint/tendermint/crypto/ed25519"
"github.com/tendermint/tendermint/types"
tsed25519 "gitlab.com/polychainlabs/threshold-ed25519/pkg"
)
func TestLocalCosignerGetID(test *testing.T) {
dummyPub := tm_ed25519.PubKeyEd25519{}
bitSize := 4096
rsaKey, err := rsa.GenerateKey(rand.Reader, bitSize)
require.NoError(test, err)
key := CosignerKey{
PubKey: dummyPub,
ShareKey: []byte{},
ID: 1,
}
signState := SignState{
Height: 0,
Round: 0,
Step: 0,
}
config := LocalCosignerConfig{
CosignerKey: key,
SignState: &signState,
RsaKey: *rsaKey,
Peers: []CosignerPeer{CosignerPeer{
ID: 1,
PublicKey: rsaKey.PublicKey,
}},
}
cosigner := NewLocalCosigner(config)
require.Equal(test, cosigner.GetID(), 1)
}
func TestLocalCosignerSign2of2(test *testing.T) {
// Test signing with a 2 of 2
total := uint8(2)
threshold := uint8(2)
bitSize := 4096
rsaKey1, err := rsa.GenerateKey(rand.Reader, bitSize)
require.NoError(test, err)
rsaKey2, err := rsa.GenerateKey(rand.Reader, bitSize)
require.NoError(test, err)
peers := []CosignerPeer{CosignerPeer{
ID: 1,
PublicKey: rsaKey1.PublicKey,
}, CosignerPeer{
ID: 2,
PublicKey: rsaKey2.PublicKey,
}}
privateKey := tm_ed25519.GenPrivKey()
privKeyBytes := [64]byte{}
copy(privKeyBytes[:], privateKey[:])
secretShares := tsed25519.DealShares(tsed25519.ExpandSecret(privKeyBytes[:32]), threshold, total)
key1 := CosignerKey{
PubKey: privateKey.PubKey(),
ShareKey: secretShares[0],
ID: 1,
}
stateFile1, err := ioutil.TempFile("", "state1.json")
require.NoError(test, err)
defer os.Remove(stateFile1.Name())
signState1, err := LoadOrCreateSignState(stateFile1.Name())
key2 := CosignerKey{
PubKey: privateKey.PubKey(),
ShareKey: secretShares[1],
ID: 2,
}
stateFile2, err := ioutil.TempFile("", "state2.json")
require.NoError(test, err)
defer os.Remove(stateFile2.Name())
signState2, err := LoadOrCreateSignState(stateFile2.Name())
require.NoError(test, err)
config1 := LocalCosignerConfig{
CosignerKey: key1,
SignState: &signState1,
RsaKey: *rsaKey1,
Peers: peers,
Total: total,
Threshold: threshold,
}
config2 := LocalCosignerConfig{
CosignerKey: key2,
SignState: &signState2,
RsaKey: *rsaKey2,
Peers: peers,
Total: total,
Threshold: threshold,
}
var cosigner1 Cosigner
var cosigner2 Cosigner
cosigner1 = NewLocalCosigner(config1)
cosigner2 = NewLocalCosigner(config2)
require.Equal(test, cosigner1.GetID(), 1)
require.Equal(test, cosigner2.GetID(), 2)
publicKeys := make([]tsed25519.Element, 0)
// get part 2 from cosigner 1 and give to cosigner 2
{
resp, err := cosigner1.GetEphemeralSecretPart(CosignerGetEphemeralSecretPartRequest{
ID: 2,
Height: 1,
Round: 0,
Step: 2,
})
require.NoError(test, err)
publicKeys = append(publicKeys, resp.SourceEphemeralSecretPublicKey)
err = cosigner2.SetEphemeralSecretPart(CosignerSetEphemeralSecretPartRequest{
SourceID: resp.SourceID,
Height: 1,
Round: 0,
Step: 2,
SourceEphemeralSecretPublicKey: resp.SourceEphemeralSecretPublicKey,
EncryptedSharePart: resp.EncryptedSharePart,
SourceSig: resp.SourceSig,
})
require.NoError(test, err)
}
// get part 1 from cosigner 2 and give to cosigner 1
{
resp, err := cosigner2.GetEphemeralSecretPart(CosignerGetEphemeralSecretPartRequest{
ID: 1,
Height: 1,
Round: 0,
Step: 2,
})
require.NoError(test, err)
publicKeys = append(publicKeys, resp.SourceEphemeralSecretPublicKey)
err = cosigner1.SetEphemeralSecretPart(CosignerSetEphemeralSecretPartRequest{
SourceID: resp.SourceID,
Height: 1,
Round: 0,
Step: 2,
SourceEphemeralSecretPublicKey: resp.SourceEphemeralSecretPublicKey,
EncryptedSharePart: resp.EncryptedSharePart,
SourceSig: resp.SourceSig,
})
require.NoError(test, err)
}
ephemeralPublic := tsed25519.AddElements(publicKeys)
fmt.Printf("public keys: %x\n", publicKeys)
fmt.Printf("eph pub: %x\n", ephemeralPublic)
// pack a vote into sign bytes
var vote types.CanonicalVote
vote.ChainID = "foobar"
vote.Height = 1
vote.Round = 0
vote.Type = types.PrevoteType
signBytes, err := cdc.MarshalBinaryLengthPrefixed(vote)
require.NoError(test, err)
// sign with cosigner 1
sigRes1, err := cosigner1.Sign(CosignerSignRequest{
SignBytes: signBytes,
})
require.NoError(test, err)
sigRes2, err := cosigner2.Sign(CosignerSignRequest{
SignBytes: signBytes,
})
require.NoError(test, err)
sigIds := []int{1, 2}
sigArr := [][]byte{sigRes1.Signature, sigRes2.Signature}
fmt.Printf("sig arr: %x\n", sigArr)
combinedSig := tsed25519.CombineShares(total, sigIds, sigArr)
signature := append(ephemeralPublic, combinedSig...)
fmt.Printf("signature: %x\n", signature)
require.True(test, privateKey.PubKey().VerifyBytes(signBytes, signature))
}
func TestLocalCosignerWatermark(test *testing.T) {
/*
privateKey := tm_ed25519.GenPrivKey()
privKeyBytes := [64]byte{}
copy(privKeyBytes[:], privateKey[:])
secretShares := tsed25519.DealShares(privKeyBytes[:32], 2, 2)
key1 := CosignerKey{
PubKey: privateKey.PubKey(),
ShareKey: secretShares[0],
ID: 1,
}
stateFile1, err := ioutil.TempFile("", "state1.json")
require.NoError(test, err)
defer os.Remove(stateFile1.Name())
signState1, err := LoadOrCreateSignState(stateFile1.Name())
cosigner1 := NewLocalCosigner(key1, &signState1)
ephPublicKey, ephPrivateKey, err := ed25519.GenerateKey(rand.Reader)
require.NoError(test, err)
ephShares := tsed25519.DealShares(ephPrivateKey.Seed(), 2, 2)
signReq1 := CosignerSignRequest{
EphemeralPublic: ephPublicKey,
EphemeralShareSecret: ephShares[0],
Height: 2,
Round: 0,
Step: 0,
SignBytes: []byte("Hello World!"),
}
_, err = cosigner1.Sign(signReq1)
require.NoError(test, err)
// watermark should have increased after signing
require.Equal(test, signState1.Height, int64(2))
// revert the height to a lower number and check if signing is rejected
signReq1.Height = 1
_, err = cosigner1.Sign(signReq1)
require.Error(test, err, "height regression. Got 1, last height 2")
*/
}

View File

@ -0,0 +1,86 @@
package signer
import (
"errors"
client "github.com/tendermint/tendermint/rpc/lib/client"
)
// RemoteCosigner uses tendermint rpc to request signing from a remote cosigner
type RemoteCosigner struct {
id int
address string
}
// NewRemoteCosigner returns a newly initialized RemoteCosigner
func NewRemoteCosigner(id int, address string) *RemoteCosigner {
cosigner := &RemoteCosigner{
id: id,
address: address,
}
return cosigner
}
// GetID returns the ID of the remote cosigner
// Implements the cosigner interface
func (cosigner *RemoteCosigner) GetID() int {
return cosigner.id
}
// Sign the sign request using the cosigner's share
// Return the signed bytes or an error
func (cosigner *RemoteCosigner) Sign(signReq CosignerSignRequest) (CosignerSignResponse, error) {
params := map[string]interface{}{
"arg": RpcSignRequest{
SignBytes: signReq.SignBytes,
},
}
remoteClient := client.NewJSONRPCClient(cosigner.address)
result := &CosignerSignResponse{}
_, err := remoteClient.Call("Sign", params, result)
if err != nil {
return CosignerSignResponse{}, err
}
return CosignerSignResponse{
Timestamp: result.Timestamp,
Signature: result.Signature,
}, nil
}
func (cosigner *RemoteCosigner) GetEphemeralSecretPart(req CosignerGetEphemeralSecretPartRequest) (CosignerGetEphemeralSecretPartResponse, error) {
resp := CosignerGetEphemeralSecretPartResponse{}
params := map[string]interface{}{
"arg": RpcGetEphemeralSecretPartRequest{
ID: req.ID,
Height: req.Height,
Round: req.Round,
Step: req.Step,
},
}
remoteClient := client.NewJSONRPCClient(cosigner.address)
result := &RpcGetEphemeralSecretPartResponse{}
_, err := remoteClient.Call("GetEphemeralSecretPart", params, result)
if err != nil {
return CosignerGetEphemeralSecretPartResponse{}, err
}
resp.SourceID = result.SourceID
resp.SourceEphemeralSecretPublicKey = result.SourceEphemeralSecretPublicKey
resp.EncryptedSharePart = result.EncryptedSharePart
resp.SourceSig = result.SourceSig
return resp, nil
}
func (cosigner *RemoteCosigner) HasEphemeralSecretPart(req CosignerHasEphemeralSecretPartRequest) (CosignerHasEphemeralSecretPartResponse, error) {
res := CosignerHasEphemeralSecretPartResponse{}
return res, errors.New("Not Implemented")
}
func (cosigner *RemoteCosigner) SetEphemeralSecretPart(req CosignerSetEphemeralSecretPartRequest) error {
return errors.New("Not Implemented")
}

View File

@ -0,0 +1,89 @@
package signer
import (
"fmt"
"net"
"net/http"
"os"
"testing"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/libs/log"
server "github.com/tendermint/tendermint/rpc/lib/server"
rpc_types "github.com/tendermint/tendermint/rpc/lib/types"
)
func rpcSignRequest(ctx *rpc_types.Context, req RpcSignRequest) (*RpcSignResponse, error) {
return &RpcSignResponse{Signature: []byte("hello world")}, nil
}
func rpcGetEphemeralSecretPart(ctx *rpc_types.Context, req RpcGetEphemeralSecretPartRequest) (*RpcGetEphemeralSecretPartResponse, error) {
response := &RpcGetEphemeralSecretPartResponse{
SourceID: 1,
SourceEphemeralSecretPublicKey: []byte("foo"),
EncryptedSharePart: []byte("bar"),
}
return response, nil
}
func TestRemoteCosignerSign(test *testing.T) {
lis, err := net.Listen("tcp", "0.0.0.0:0")
require.NoError(test, err)
defer lis.Close()
logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout))
serv := func() {
routes := map[string]*server.RPCFunc{
"Sign": server.NewRPCFunc(rpcSignRequest, "arg"),
"GetEphemeralSecretPart": server.NewRPCFunc(rpcGetEphemeralSecretPart, "arg"),
}
mux := http.NewServeMux()
server.RegisterRPCFuncs(mux, routes, cdc, logger)
tcpLogger := logger.With("socket", "tcp")
config := server.DefaultConfig()
server.StartHTTPServer(lis, mux, tcpLogger, config)
}
go serv()
port := lis.Addr().(*net.TCPAddr).Port
cosigner := NewRemoteCosigner(2, fmt.Sprintf("tcp://0.0.0.0:%d", port))
resp, err := cosigner.Sign(CosignerSignRequest{})
require.NoError(test, err)
require.Equal(test, resp.Signature, []byte("hello world"))
}
func TestRemoteCosignerGetEphemeralSecretPart(test *testing.T) {
lis, err := net.Listen("tcp", "0.0.0.0:0")
require.NoError(test, err)
defer lis.Close()
logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout))
serv := func() {
routes := map[string]*server.RPCFunc{
"Sign": server.NewRPCFunc(rpcSignRequest, "arg"),
"GetEphemeralSecretPart": server.NewRPCFunc(rpcGetEphemeralSecretPart, "arg"),
}
mux := http.NewServeMux()
server.RegisterRPCFuncs(mux, routes, cdc, logger)
tcpLogger := logger.With("socket", "tcp")
config := server.DefaultConfig()
server.StartHTTPServer(lis, mux, tcpLogger, config)
}
go serv()
port := lis.Addr().(*net.TCPAddr).Port
cosigner := NewRemoteCosigner(2, fmt.Sprintf("tcp://0.0.0.0:%d", port))
resp, err := cosigner.GetEphemeralSecretPart(CosignerGetEphemeralSecretPartRequest{})
require.NoError(test, err)
require.Equal(test, resp, CosignerGetEphemeralSecretPartResponse{
SourceID: 1,
SourceEphemeralSecretPublicKey: []byte("foo"),
EncryptedSharePart: []byte("bar"),
})
}

View File

@ -13,8 +13,9 @@ import (
"github.com/tendermint/tendermint/types"
)
// NodeClient dials a node responds to signature requests using its privVal.
type NodeClient struct {
// ReconnRemoteSigner dials using its dialer and responds to any
// signature requests using its privVal.
type ReconnRemoteSigner struct {
cmn.BaseService
address string
@ -25,19 +26,19 @@ type NodeClient struct {
dialer net.Dialer
}
// NewNodeClient return a NodeClient that will dial using the given
// NewReconnRemoteSigner return a ReconnRemoteSigner that will dial using the given
// dialer and respond to any signature requests over the connection
// using the given privVal.
//
// If the connection is broken, the NodeClient will attempt to reconnect.
func NewNodeClient(
// If the connection is broken, the ReconnRemoteSigner will attempt to reconnect.
func NewReconnRemoteSigner(
address string,
logger log.Logger,
chainID string,
privVal types.PrivValidator,
dialer net.Dialer,
) *NodeClient {
rs := &NodeClient{
) *ReconnRemoteSigner {
rs := &ReconnRemoteSigner{
address: address,
chainID: chainID,
privVal: privVal,
@ -50,13 +51,13 @@ func NewNodeClient(
}
// OnStart implements cmn.Service.
func (rs *NodeClient) OnStart() error {
func (rs *ReconnRemoteSigner) OnStart() error {
go rs.loop()
return nil
}
// main loop for NodeClient
func (rs *NodeClient) loop() {
// main loop for ReconnRemoteSigner
func (rs *ReconnRemoteSigner) loop() {
var conn net.Conn
for {
if !rs.IsRunning() {
@ -120,7 +121,7 @@ func (rs *NodeClient) loop() {
}
}
func (rs *NodeClient) handleRequest(req privval.RemoteSignerMsg) (privval.RemoteSignerMsg, error) {
func (rs *ReconnRemoteSigner) handleRequest(req privval.RemoteSignerMsg) (privval.RemoteSignerMsg, error) {
var res privval.RemoteSignerMsg
var err error

View File

@ -1,11 +1,13 @@
package signer
import (
"errors"
"io"
amino "github.com/tendermint/go-amino"
cryptoAmino "github.com/tendermint/tendermint/crypto/encoding/amino"
"github.com/tendermint/tendermint/privval"
"github.com/tendermint/tendermint/types"
)
var codec = amino.NewCodec()
@ -28,3 +30,18 @@ func WriteMsg(writer io.Writer, msg interface{}) (err error) {
_, err = codec.MarshalBinaryLengthPrefixedWriter(writer, msg)
return
}
// UnpackHRS deserializes sign bytes and gets the height, round, and step
func UnpackHRS(signBytes []byte) (height int64, round int64, step int8, err error) {
var proposal types.CanonicalProposal
if err := cdc.UnmarshalBinaryLengthPrefixed(signBytes, &proposal); err == nil {
return proposal.Height, proposal.Round, stepPropose, nil
}
var vote types.CanonicalVote
if err := cdc.UnmarshalBinaryLengthPrefixed(signBytes, &vote); err == nil {
return vote.Height, vote.Round, CanonicalVoteToStep(&vote), nil
}
return 0, 0, 0, errors.New("Could not UnpackHRS from sign bytes")
}

View File

@ -0,0 +1,192 @@
package signer
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"time"
cmn "github.com/tendermint/tendermint/libs/common"
"github.com/tendermint/tendermint/types"
tmtime "github.com/tendermint/tendermint/types/time"
)
const (
stepNone int8 = 0 // Used to distinguish the initial state
stepPropose int8 = 1
stepPrevote int8 = 2
stepPrecommit int8 = 3
)
func CanonicalVoteToStep(vote *types.CanonicalVote) int8 {
switch vote.Type {
case types.PrevoteType:
return stepPrevote
case types.PrecommitType:
return stepPrecommit
default:
panic("Unknown vote type")
}
}
func VoteToStep(vote *types.Vote) int8 {
switch vote.Type {
case types.PrevoteType:
return stepPrevote
case types.PrecommitType:
return stepPrecommit
default:
panic("Unknown vote type")
}
}
func ProposalToStep(_ *types.Proposal) int8 {
return stepPropose
}
// SignState stores signing information for high level watermark management.
type SignState struct {
Height int64 `json:"height"`
Round int64 `json:"round"`
Step int8 `json:"step"`
EphemeralPublic []byte `json:"ephemeral_public"`
Signature []byte `json:"signature,omitempty"`
SignBytes cmn.HexBytes `json:"signbytes,omitempty"`
filePath string
}
// Save persists the FilePvLastSignState to its filePath.
func (signState *SignState) Save() {
outFile := signState.filePath
if outFile == "" {
panic("cannot save SignState: filePath not set")
}
jsonBytes, err := cdc.MarshalJSONIndent(signState, "", " ")
if err != nil {
panic(err)
}
err = cmn.WriteFileAtomic(outFile, jsonBytes, 0600)
if err != nil {
panic(err)
}
}
// CheckHRS checks the given height, round, step (HRS) against that of the
// SignState. It returns an error if the arguments constitute a regression,
// or if they match but the SignBytes are empty.
// Returns true if the HRS matches the arguments and the SignBytes are not empty (indicating
// we have already signed for this HRS, and can reuse the existing signature).
// It panics if the HRS matches the arguments, there's a SignBytes, but no Signature.
func (signState *SignState) CheckHRS(height int64, round int64, step int8) (bool, error) {
if signState.Height > height {
return false, fmt.Errorf("height regression. Got %v, last height %v", height, signState.Height)
}
if signState.Height == height {
if signState.Round > round {
return false, fmt.Errorf("round regression at height %v. Got %v, last round %v", height, round, signState.Round)
}
if signState.Round == round {
if signState.Step > step {
return false, fmt.Errorf("step regression at height %v round %v. Got %v, last step %v", height, round, step, signState.Step)
} else if signState.Step == step {
if signState.SignBytes != nil {
if signState.Signature == nil {
panic("pv: Signature is nil but SignBytes is not!")
}
return true, nil
}
return false, errors.New("no SignBytes found")
}
}
}
return false, nil
}
// LoadSignState loads a sign state from disk.
func LoadSignState(filepath string) (SignState, error) {
state := SignState{}
stateJSONBytes, err := ioutil.ReadFile(filepath)
if err != nil {
return state, err
}
err = cdc.UnmarshalJSON(stateJSONBytes, &state)
if err != nil {
return state, err
}
state.filePath = filepath
return state, nil
}
// LoadOrCreateSignState loads the sign state from filepath
// If the sign state could not be loaded, an empty sign state is initialized
// and saved to filepath.
func LoadOrCreateSignState(filepath string) (SignState, error) {
existing, err := LoadSignState(filepath)
if err == nil {
return existing, nil
}
// There was an error loading the sign state
// Make an empty sign state and save it
state := SignState{}
state.filePath = filepath
state.Save()
return state, nil
}
// OnlyDifferByTimestamp returns true if the sign bytes of the sign state
// are the same as the new sign bytes excluding the timestamp.
func (signState *SignState) OnlyDifferByTimestamp(signBytes []byte) (time.Time, bool) {
if signState.Step == stepPropose {
return checkProposalOnlyDifferByTimestamp(signState.SignBytes, signBytes)
} else if signState.Step == stepPrevote || signState.Step == stepPrecommit {
return checkVoteOnlyDifferByTimestamp(signState.SignBytes, signBytes)
}
return time.Time{}, false
}
func checkVoteOnlyDifferByTimestamp(lastSignBytes, newSignBytes []byte) (time.Time, bool) {
var lastVote, newVote types.CanonicalVote
if err := cdc.UnmarshalBinaryLengthPrefixed(lastSignBytes, &lastVote); err != nil {
panic(fmt.Sprintf("LastSignBytes cannot be unmarshalled into vote: %v", err))
}
if err := cdc.UnmarshalBinaryLengthPrefixed(newSignBytes, &newVote); err != nil {
panic(fmt.Sprintf("signBytes cannot be unmarshalled into vote: %v", err))
}
lastTime := lastVote.Timestamp
// set the times to the same value and check equality
now := tmtime.Now()
lastVote.Timestamp = now
newVote.Timestamp = now
lastVoteBytes, _ := cdc.MarshalJSON(lastVote)
newVoteBytes, _ := cdc.MarshalJSON(newVote)
return lastTime, bytes.Equal(newVoteBytes, lastVoteBytes)
}
func checkProposalOnlyDifferByTimestamp(lastSignBytes, newSignBytes []byte) (time.Time, bool) {
var lastProposal, newProposal types.CanonicalProposal
if err := cdc.UnmarshalBinaryLengthPrefixed(lastSignBytes, &lastProposal); err != nil {
panic(fmt.Sprintf("LastSignBytes cannot be unmarshalled into proposal: %v", err))
}
if err := cdc.UnmarshalBinaryLengthPrefixed(newSignBytes, &newProposal); err != nil {
panic(fmt.Sprintf("signBytes cannot be unmarshalled into proposal: %v", err))
}
lastTime := lastProposal.Timestamp
// set the times to the same value and check equality
now := tmtime.Now()
lastProposal.Timestamp = now
newProposal.Timestamp = now
lastProposalBytes, _ := cdc.MarshalBinaryLengthPrefixed(lastProposal)
newProposalBytes, _ := cdc.MarshalBinaryLengthPrefixed(newProposal)
return lastTime, bytes.Equal(newProposalBytes, lastProposalBytes)
}

View File

@ -0,0 +1,357 @@
package signer
import (
"bytes"
"context"
"errors"
"fmt"
"sync"
"time"
amino "github.com/tendermint/go-amino"
"github.com/tendermint/tendermint/crypto"
cryptoAmino "github.com/tendermint/tendermint/crypto/encoding/amino"
"github.com/tendermint/tendermint/types"
tsed25519 "gitlab.com/polychainlabs/threshold-ed25519/pkg"
)
var cdc = amino.NewCodec()
func init() {
cryptoAmino.RegisterAmino(cdc)
}
type ThresholdValidator struct {
threshold int
pubkey crypto.PubKey
// stores the last sign state for a block we have fully signed
// Cached to respond to SignVote requests if we already have a signature
lastSignState SignState
// our own cosigner
cosigner Cosigner
// peer cosigners
peers []Cosigner
}
type ThresholdValidatorOpt struct {
Pubkey crypto.PubKey
Threshold int
SignState SignState
Cosigner Cosigner
Peers []Cosigner
}
// NewThresholdValidator creates and returns a new ThresholdValidator
func NewThresholdValidator(opt *ThresholdValidatorOpt) *ThresholdValidator {
validator := &ThresholdValidator{}
validator.cosigner = opt.Cosigner
validator.peers = opt.Peers
validator.threshold = opt.Threshold
validator.pubkey = opt.Pubkey
validator.lastSignState = opt.SignState
return validator
}
// GetPubKey returns the public key of the validator.
// Implements PrivValidator.
func (pv *ThresholdValidator) GetPubKey() crypto.PubKey {
return pv.pubkey
}
// SignVote signs a canonical representation of the vote, along with the
// chainID. Implements PrivValidator.
func (pv *ThresholdValidator) SignVote(chainID string, vote *types.Vote) error {
// round the timestamp to nearest second to reduce non-deterministic signature log messages
vote.Timestamp = vote.Timestamp.Round(time.Second)
block := &block{
Height: vote.Height,
Round: int64(vote.Round),
Step: VoteToStep(vote),
Timestamp: vote.Timestamp,
SignBytes: vote.SignBytes(chainID),
}
sig, stamp, err := pv.signBlock(chainID, block)
vote.Signature = sig
vote.Timestamp = stamp
return err
}
// SignProposal signs a canonical representation of the proposal, along with
// the chainID. Implements PrivValidator.
func (pv *ThresholdValidator) SignProposal(chainID string, proposal *types.Proposal) error {
// round the timestamp to nearest second to reduce non-deterministic signature log messages
proposal.Timestamp = proposal.Timestamp.Round(time.Second)
block := &block{
Height: proposal.Height,
Round: int64(proposal.Round),
Step: ProposalToStep(proposal),
Timestamp: proposal.Timestamp,
SignBytes: proposal.SignBytes(chainID),
}
sig, stamp, err := pv.signBlock(chainID, block)
proposal.Signature = sig
proposal.Timestamp = stamp
return err
}
type block struct {
Height int64
Round int64
Step int8
SignBytes []byte
Timestamp time.Time
}
func (pv *ThresholdValidator) signBlock(chainID string, block *block) ([]byte, time.Time, error) {
height, round, step, stamp := block.Height, block.Round, block.Step, block.Timestamp
// the block sign state for caching full block signatures
lss := pv.lastSignState
// check watermark
sameHRS, err := lss.CheckHRS(height, int64(round), step)
if err != nil {
return nil, stamp, err
}
signBytes := block.SignBytes
if sameHRS {
if bytes.Equal(signBytes, lss.SignBytes) {
return lss.Signature, block.Timestamp, nil
} else if timestamp, ok := lss.OnlyDifferByTimestamp(signBytes); ok {
return lss.Signature, timestamp, nil
}
return nil, stamp, errors.New("conflicting data")
}
total := uint8(len(pv.peers) + 1)
// destination for share signatures
shareSignatures := make([][]byte, total)
// share sigs is updated by goroutines
shareSignaturesMutex := sync.Mutex{}
wg := sync.WaitGroup{}
wg.Add(len(pv.peers))
ourID := pv.cosigner.GetID()
// have our cosigner generate ephemeral info at the current height
_, err = pv.cosigner.GetEphemeralSecretPart(CosignerGetEphemeralSecretPartRequest{
ID: ourID,
Height: height,
Round: round,
Step: step,
})
if err != nil {
return nil, stamp, err
}
// There are two layers of goroutines for each cosigner.
// The outer routine for each cosigner to dispatch signing in parallel. This outer routine
// block on the signing request completing.
// The inner routine (formed within each request goroutine), dispatches the actual signing call.
// This is to support a time out which can happen when using remote signers.
for _, cosigner := range pv.peers {
request := func(cosigner Cosigner) {
cosignerId := cosigner.GetID()
cosignerIdx := cosignerId - 1
// cosigner.Sign makes a blocking RPC request (with no timeout)
// to prevent it from hanging our process indefinitely, we use a timeout context
// and another goroutine
signCtx, signCtxCancel := context.WithTimeout(context.Background(), 4*time.Second)
go func() {
hasResp, err := pv.cosigner.HasEphemeralSecretPart(CosignerHasEphemeralSecretPartRequest{
ID: cosignerId,
Height: height,
Round: round,
Step: step,
})
// did we timeout or finish elsewhere?
select {
case <-signCtx.Done():
return
default:
}
if err != nil {
fmt.Printf("ERROR HasEphemeralSecretPart: %s\n", err)
signCtxCancel()
return
}
if !hasResp.Exists {
// if we don't already have an ephemeral secret part for the HRS, we need to get one
ephSecretResp, err := cosigner.GetEphemeralSecretPart(CosignerGetEphemeralSecretPartRequest{
ID: ourID,
Height: height,
Round: round,
Step: step,
})
if err != nil {
fmt.Printf("ERROR GetEphemeralSecretPart %s\n", err)
}
// did we timeout or finish elsewhere?
select {
case <-signCtx.Done():
return
default:
}
if err != nil {
signCtxCancel()
return
}
// set the response for ourselves
err = pv.cosigner.SetEphemeralSecretPart(CosignerSetEphemeralSecretPartRequest{
SourceID: ephSecretResp.SourceID,
SourceEphemeralSecretPublicKey: ephSecretResp.SourceEphemeralSecretPublicKey,
EncryptedSharePart: ephSecretResp.EncryptedSharePart,
Height: height,
Round: round,
Step: step,
})
if err != nil {
fmt.Printf("ERROR SetEphemeralSecretPart %s\n", err)
}
// did we timeout or finish elsewhere?
select {
case <-signCtx.Done():
return
default:
}
if err != nil {
signCtxCancel()
return
}
}
// ask the cosigner to sign with their share
sigResp, err := cosigner.Sign(CosignerSignRequest{
SignBytes: signBytes,
})
if err != nil {
fmt.Printf("ERROR Sign %s\n", err)
}
// did we timeout or finish elsewhere?
select {
case <-signCtx.Done():
return
default:
}
if err != nil {
signCtxCancel()
return
}
// The signCtx is done if it times out or if the blockCtx done cancels it
select {
case <-signCtx.Done():
return
default:
}
defer signCtxCancel()
shareSignaturesMutex.Lock()
defer shareSignaturesMutex.Unlock()
shareSignatures[cosignerIdx] = make([]byte, len(sigResp.Signature))
copy(shareSignatures[cosignerIdx], sigResp.Signature)
}()
// the sign context finished or timed out
select {
case <-signCtx.Done():
}
wg.Done()
}
go request(cosigner)
}
// Wait for all cosigners to be complete
// A Cosigner will either respond in time, or be canceled with timeout
wg.Wait()
shareSignaturesMutex.Lock()
defer shareSignaturesMutex.Unlock()
// sign with our share now
signResp, err := pv.cosigner.Sign(CosignerSignRequest{
SignBytes: signBytes,
})
if err != nil {
return nil, stamp, err
}
ephemeralPublic := signResp.EphemeralPublic
shareSignatures[ourID-1] = make([]byte, len(signResp.Signature))
copy(shareSignatures[ourID-1], signResp.Signature)
// collect all valid responses into array of ids and signatures for the threshold lib
sigIds := make([]int, 0)
shareSigs := make([][]byte, 0)
for idx, shareSig := range shareSignatures {
if len(shareSig) == 0 {
continue
}
sigIds = append(sigIds, idx+1)
// we are ok to use the share signatures - complete boolean
// prevents future concurrent access
shareSigs = append(shareSigs, shareSig)
}
if len(sigIds) < pv.threshold {
return nil, stamp, errors.New("Not enough co-signers")
}
// assemble into final signature
combinedSig := tsed25519.CombineShares(total, sigIds, shareSigs)
signature := append(ephemeralPublic, combinedSig...)
// verify the combined signature before saving to watermark
if !pv.pubkey.VerifyBytes(signBytes, signature) {
return nil, stamp, errors.New("Combined signature is not valid")
}
pv.lastSignState.Height = height
pv.lastSignState.Round = round
pv.lastSignState.Step = step
pv.lastSignState.Signature = signature
pv.lastSignState.SignBytes = signBytes
pv.lastSignState.Save()
return signature, stamp, nil
}

11
test/cosigner-key.json Normal file
View File

@ -0,0 +1,11 @@
{
"rsa_key": "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",
"pub_key": "FiTeZCA2QoZNqlsfZLdYAMeCCNsH4dAKxC2sqkGAwcYSIyBgdQ==",
"secret_share": "SPhjpgM6rx4Iv2/XpQ4cDBsdgwkQeF9NnsaFDH+g6QY=",
"id": 3,
"rsa_pubs": [
"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"
]
}