mirror of
https://gitlab.com/pulsechaincom/prysm-pulse.git
synced 2024-12-24 20:37:17 +00:00
9d173dcad2
* Update remote signer for 0.11 * use signing function for aggregate and proof signature Co-authored-by: Raul Jordan <raul@prysmaticlabs.com>
330 lines
10 KiB
Go
330 lines
10 KiB
Go
package keymanager
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1"
|
|
"github.com/prysmaticlabs/prysm/shared/bls"
|
|
"github.com/prysmaticlabs/prysm/shared/bytesutil"
|
|
pb "github.com/wealdtech/eth2-signer-api/pb/v1"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials"
|
|
)
|
|
|
|
const (
|
|
// maxMessageSize is the largest message that can be received over GRPC. Set to 8MB, which handles ~128K keys.
|
|
maxMessageSize = 8 * 1024 * 1024
|
|
)
|
|
|
|
// Remote is a key manager that accesses a remote wallet daemon.
|
|
type Remote struct {
|
|
paths []string
|
|
conn *grpc.ClientConn
|
|
accounts map[[48]byte]*accountInfo
|
|
signClientInitiator func(*grpc.ClientConn)
|
|
}
|
|
|
|
type accountInfo struct {
|
|
Name string `json:"name"`
|
|
PubKey []byte `json:"pubkey"`
|
|
}
|
|
|
|
type remoteOpts struct {
|
|
Location string `json:"location"`
|
|
Accounts []string `json:"accounts"`
|
|
Certificates *remoteCertificateOpts `json:"certificates"`
|
|
}
|
|
|
|
type remoteCertificateOpts struct {
|
|
CACert string `json:"ca_cert"`
|
|
ClientCert string `json:"client_cert"`
|
|
ClientKey string `json:"client_key"`
|
|
}
|
|
|
|
var remoteOptsHelp = `The remote key manager connects to a walletd instance. The options are:
|
|
- location This is the location to look for wallets. If not supplied it will
|
|
use the standard (operating system-dependent) path.
|
|
- accounts This is a list of account specifiers. An account specifier is of
|
|
the form <wallet name>/[account name], where the account name can be a
|
|
regular expression. If the account specifier is just <wallet name> all
|
|
accounts in that wallet will be used. Multiple account specifiers can be
|
|
supplied if required.
|
|
- certificates This provides paths to certificates:
|
|
- ca_cert This is the path to the server's certificate authority certificate file
|
|
- client_cert This is the path to the client's certificate file
|
|
- client_key This is the path to the client's key file
|
|
|
|
An sample keymanager options file (with annotations; these should be removed if
|
|
using this as a template) is:
|
|
|
|
{
|
|
"location": "host.example.com:12345", // Connect to walletd at host.example.com on port 12345
|
|
"accounts": ["Validators/Account.*"] // Use all accounts in the 'Validators' wallet starting with 'Account'
|
|
"certificates": {
|
|
"ca_cert": "/home/eth2/certs/ca.crt" // Certificate file for the CA that signed the server's certificate
|
|
"client_cert": "/home/eth2/certs/client.crt" // Certificate file for this client
|
|
"client_key": "/home/eth2/certs/client.key" // Key file for this client
|
|
}
|
|
}`
|
|
|
|
// NewRemoteWallet creates a key manager populated with the keys from walletd.
|
|
func NewRemoteWallet(input string) (KeyManager, string, error) {
|
|
opts := &remoteOpts{}
|
|
err := json.Unmarshal([]byte(input), opts)
|
|
if err != nil {
|
|
return nil, remoteOptsHelp, err
|
|
}
|
|
|
|
if len(opts.Accounts) == 0 {
|
|
return nil, remoteOptsHelp, errors.New("at least one account specifier is required")
|
|
}
|
|
|
|
// Load the client certificates.
|
|
if opts.Certificates == nil {
|
|
return nil, remoteOptsHelp, errors.New("certificates are required")
|
|
}
|
|
if opts.Certificates.ClientCert == "" {
|
|
return nil, remoteOptsHelp, errors.New("client certificate is required")
|
|
}
|
|
if opts.Certificates.ClientKey == "" {
|
|
return nil, remoteOptsHelp, errors.New("client key is required")
|
|
}
|
|
clientPair, err := tls.LoadX509KeyPair(opts.Certificates.ClientCert, opts.Certificates.ClientKey)
|
|
if err != nil {
|
|
return nil, remoteOptsHelp, errors.Wrap(err, "failed to obtain client's certificate and/or key")
|
|
}
|
|
|
|
// Load the CA for the server certificate if present.
|
|
cp := x509.NewCertPool()
|
|
if opts.Certificates.CACert != "" {
|
|
serverCA, err := ioutil.ReadFile(opts.Certificates.CACert)
|
|
if err != nil {
|
|
return nil, remoteOptsHelp, errors.Wrap(err, "failed to obtain server's CA certificate")
|
|
}
|
|
if !cp.AppendCertsFromPEM(serverCA) {
|
|
return nil, remoteOptsHelp, errors.Wrap(err, "failed to add server's CA certificate to pool")
|
|
}
|
|
}
|
|
|
|
tlsCfg := &tls.Config{
|
|
Certificates: []tls.Certificate{clientPair},
|
|
RootCAs: cp,
|
|
}
|
|
clientCreds := credentials.NewTLS(tlsCfg)
|
|
|
|
grpcOpts := []grpc.DialOption{
|
|
// Require TLS with client certificate.
|
|
grpc.WithTransportCredentials(clientCreds),
|
|
// Receive large messages without erroring.
|
|
grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(maxMessageSize)),
|
|
}
|
|
|
|
conn, err := grpc.Dial(opts.Location, grpcOpts...)
|
|
if err != nil {
|
|
return nil, remoteOptsHelp, errors.New("failed to connect to remote wallet")
|
|
}
|
|
|
|
km := &Remote{
|
|
conn: conn,
|
|
paths: opts.Accounts,
|
|
}
|
|
|
|
err = km.RefreshValidatingKeys()
|
|
if err != nil {
|
|
return nil, remoteOptsHelp, errors.Wrap(err, "failed to fetch accounts from remote wallet")
|
|
}
|
|
|
|
return km, remoteOptsHelp, nil
|
|
}
|
|
|
|
// FetchValidatingKeys fetches the list of public keys that should be used to validate with.
|
|
func (km *Remote) FetchValidatingKeys() ([][48]byte, error) {
|
|
res := make([][48]byte, 0, len(km.accounts))
|
|
for _, accountInfo := range km.accounts {
|
|
res = append(res, bytesutil.ToBytes48(accountInfo.PubKey))
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// Sign without protection is not supported by remote keymanagers.
|
|
func (km *Remote) Sign(pubKey [48]byte, root [32]byte) (*bls.Signature, error) {
|
|
return nil, errors.New("remote keymanager does not support unprotected signing")
|
|
}
|
|
|
|
// SignGeneric signs a generic message for the validator to broadcast.
|
|
func (km *Remote) SignGeneric(pubKey [48]byte, root [32]byte, domain [32]byte) (*bls.Signature, error) {
|
|
accountInfo, exists := km.accounts[pubKey]
|
|
if !exists {
|
|
return nil, ErrNoSuchKey
|
|
}
|
|
|
|
client := pb.NewSignerClient(km.conn)
|
|
req := &pb.SignRequest{
|
|
Id: &pb.SignRequest_Account{Account: accountInfo.Name},
|
|
Data: root[:],
|
|
Domain: domain[:],
|
|
}
|
|
resp, err := client.Sign(context.Background(), req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch resp.State {
|
|
case pb.ResponseState_DENIED:
|
|
return nil, ErrDenied
|
|
case pb.ResponseState_FAILED:
|
|
return nil, ErrCannotSign
|
|
}
|
|
return bls.SignatureFromBytes(resp.Signature)
|
|
}
|
|
|
|
// SignProposal signs a block proposal for the validator to broadcast.
|
|
func (km *Remote) SignProposal(pubKey [48]byte, domain [32]byte, data *ethpb.BeaconBlockHeader) (*bls.Signature, error) {
|
|
accountInfo, exists := km.accounts[pubKey]
|
|
if !exists {
|
|
return nil, ErrNoSuchKey
|
|
}
|
|
|
|
client := pb.NewSignerClient(km.conn)
|
|
req := &pb.SignBeaconProposalRequest{
|
|
Id: &pb.SignBeaconProposalRequest_Account{Account: accountInfo.Name},
|
|
Domain: domain[:],
|
|
Data: &pb.BeaconBlockHeader{
|
|
Slot: data.Slot,
|
|
ProposerIndex: data.ProposerIndex,
|
|
ParentRoot: data.ParentRoot,
|
|
StateRoot: data.StateRoot,
|
|
BodyRoot: data.BodyRoot,
|
|
},
|
|
}
|
|
resp, err := client.SignBeaconProposal(context.Background(), req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch resp.State {
|
|
case pb.ResponseState_DENIED:
|
|
return nil, ErrDenied
|
|
case pb.ResponseState_FAILED:
|
|
return nil, ErrCannotSign
|
|
}
|
|
return bls.SignatureFromBytes(resp.Signature)
|
|
}
|
|
|
|
// SignAttestation signs an attestation for the validator to broadcast.
|
|
func (km *Remote) SignAttestation(pubKey [48]byte, domain [32]byte, data *ethpb.AttestationData) (*bls.Signature, error) {
|
|
accountInfo, exists := km.accounts[pubKey]
|
|
if !exists {
|
|
return nil, ErrNoSuchKey
|
|
}
|
|
|
|
client := pb.NewSignerClient(km.conn)
|
|
req := &pb.SignBeaconAttestationRequest{
|
|
Id: &pb.SignBeaconAttestationRequest_Account{Account: accountInfo.Name},
|
|
Domain: domain[:],
|
|
Data: &pb.AttestationData{
|
|
Slot: data.Slot,
|
|
CommitteeIndex: data.CommitteeIndex,
|
|
BeaconBlockRoot: data.BeaconBlockRoot,
|
|
Source: &pb.Checkpoint{
|
|
Epoch: data.Source.Epoch,
|
|
Root: data.Source.Root,
|
|
},
|
|
Target: &pb.Checkpoint{
|
|
Epoch: data.Target.Epoch,
|
|
Root: data.Target.Root,
|
|
},
|
|
},
|
|
}
|
|
resp, err := client.SignBeaconAttestation(context.Background(), req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch resp.State {
|
|
case pb.ResponseState_DENIED:
|
|
return nil, ErrDenied
|
|
case pb.ResponseState_FAILED:
|
|
return nil, ErrCannotSign
|
|
}
|
|
return bls.SignatureFromBytes(resp.Signature)
|
|
}
|
|
|
|
// RefreshValidatingKeys refreshes the list of validating keys from the remote signer.
|
|
func (km *Remote) RefreshValidatingKeys() error {
|
|
listerClient := pb.NewListerClient(km.conn)
|
|
listAccountsReq := &pb.ListAccountsRequest{
|
|
Paths: km.paths,
|
|
}
|
|
resp, err := listerClient.ListAccounts(context.Background(), listAccountsReq)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if resp.State == pb.ResponseState_DENIED {
|
|
return errors.New("attempt to fetch keys denied")
|
|
}
|
|
if resp.State == pb.ResponseState_FAILED {
|
|
return errors.New("attempt to fetch keys failed")
|
|
}
|
|
|
|
verificationRegexes := pathsToVerificationRegexes(km.paths)
|
|
accounts := make(map[[48]byte]*accountInfo, len(resp.Accounts))
|
|
for _, account := range resp.Accounts {
|
|
verified := false
|
|
for _, verificationRegex := range verificationRegexes {
|
|
if verificationRegex.Match([]byte(account.Name)) {
|
|
verified = true
|
|
break
|
|
}
|
|
}
|
|
if !verified {
|
|
log.WithField("path", account.Name).Warn("Received unwanted account from server; ignoring")
|
|
continue
|
|
}
|
|
account := &accountInfo{
|
|
Name: account.Name,
|
|
PubKey: account.PublicKey,
|
|
}
|
|
accounts[bytesutil.ToBytes48(account.PubKey)] = account
|
|
}
|
|
km.accounts = accounts
|
|
return nil
|
|
}
|
|
|
|
// pathsToVerificationRegexes turns path specifiers in to regexes to ensure accounts we are given are good.
|
|
func pathsToVerificationRegexes(paths []string) []*regexp.Regexp {
|
|
regexes := make([]*regexp.Regexp, 0, len(paths))
|
|
for _, path := range paths {
|
|
log := log.WithField("path", path)
|
|
parts := strings.Split(path, "/")
|
|
if len(parts) == 0 || len(parts[0]) == 0 {
|
|
log.Debug("Invalid path")
|
|
continue
|
|
}
|
|
if len(parts) == 1 {
|
|
parts = append(parts, ".*")
|
|
}
|
|
if strings.HasPrefix(parts[1], "^") {
|
|
parts[1] = parts[1][1:]
|
|
}
|
|
var specifier string
|
|
if strings.HasSuffix(parts[1], "$") {
|
|
specifier = fmt.Sprintf("^%s/%s", parts[0], parts[1])
|
|
} else {
|
|
specifier = fmt.Sprintf("^%s/%s$", parts[0], parts[1])
|
|
}
|
|
regex, err := regexp.Compile(specifier)
|
|
if err != nil {
|
|
log.WithField("specifier", specifier).WithError(err).Warn("Invalid path regex")
|
|
continue
|
|
}
|
|
regexes = append(regexes, regex)
|
|
}
|
|
return regexes
|
|
}
|